Files
2026-11-09 12:34:50 +03:30

156 خطوط
5.6 KiB
Python

import math
import re
from app.models.schematic import Point, Bounds
from .geometry import bounds, poly_path, transform_point
from .record_decoder import ParseError
# PCB integer coordinate unit = 1/10000 mil. Common viewer unit = 10 mil.
BINARY_SCALE = 100000
def raw_unit(value):
n = value / BINARY_SCALE
if not math.isfinite(n) or abs(n) > 100000:
raise ParseError("Invalid or out-of-range PCB coordinate")
return n
def unit(value):
match = re.fullmatch(r"\s*([-+\d.eE]+)\s*(mil|mm|in|inch)?\s*", str(value), re.I)
if not match:
raise ParseError(f"Invalid PCB unit: {str(value)[:80]}")
n = float(match[1])
suffix = (match[2] or "mil").lower()
factor = {"mil": 0.1, "mm": 1 / 0.254, "in": 100, "inch": 100}[suffix]
result = n * factor
if not math.isfinite(result) or abs(result) > 100000:
raise ParseError("PCB unit out of bounds")
return result
def xy(x, y):
return Point(x=raw_unit(x), y=-raw_unit(y))
def ellipse_path(cx, cy, rx, ry=None):
ry = rx if ry is None else ry
return f"M{cx - rx:.15g},{cy:.15g} A{rx:.15g},{ry:.15g} 0 1 0 {cx + rx:.15g},{cy:.15g} A{rx:.15g},{ry:.15g} 0 1 0 {cx - rx:.15g},{cy:.15g} Z"
def arc_path(center, radius, start, end):
if not all(math.isfinite(v) for v in (radius,start,end)): raise ParseError("Non-finite arc geometry")
if radius <= 0:
raise ParseError("Arc radius must be positive")
if abs(end - start) >= 359.99999 or start == end:
return ellipse_path(center.x, center.y, radius)
a, b = math.radians(start), math.radians(end)
x1, y1 = center.x + radius * math.cos(a), center.y - radius * math.sin(a)
x2, y2 = center.x + radius * math.cos(b), center.y - radius * math.sin(b)
return f"M{x1:.15g},{y1:.15g} A{radius:.15g},{radius:.15g} 0 {int((end - start) % 360 > 180)} 0 {x2:.15g},{y2:.15g}"
def pad_shape(position, w, h, shape, rotation, radius_percent=0):
if w <= 0 or h <= 0:
raise ParseError("Pad dimensions must be positive")
# Build in local coordinates, then place absolute points. Exact arcs use a path transform.
angle = math.radians(-rotation)
def q(x, y):
return Point(
x=position.x + x * math.cos(angle) - y * math.sin(angle),
y=position.y + x * math.sin(angle) + y * math.cos(angle),
)
def pair(p):
return f"{p.x:.15g},{p.y:.15g}"
if (
shape == 1
): # Circle or oval/obround; unequal sizes form a capsule, not an ellipse.
r = min(w, h) / 2
elif shape == 9:
r = min(w, h) * radius_percent / 200
else:
r = 0
if shape == 3:
# Altium octagon chamfers at 45 degrees.
c = min(w, h) / (2 + math.sqrt(2))
pts = [
q(x, y)
for x, y in [
(-w / 2 + c, -h / 2),
(w / 2 - c, -h / 2),
(w / 2, -h / 2 + c),
(w / 2, h / 2 - c),
(w / 2 - c, h / 2),
(-w / 2 + c, h / 2),
(-w / 2, h / 2 - c),
(-w / 2, -h / 2 + c),
]
]
return poly_path(pts, True), bounds(pts)
if shape not in (1, 2, 9):
raise ParseError(f"Unsupported pad shape {shape}")
corners = [
q(x, y)
for x, y in [(-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2), (-w / 2, h / 2)]
]
if r <= 0:
return poly_path(corners, True), bounds(corners)
pts = [
q(x, y)
for x, y in [
(-w / 2 + r, -h / 2),
(w / 2 - r, -h / 2),
(w / 2, -h / 2 + r),
(w / 2, h / 2 - r),
(w / 2 - r, h / 2),
(-w / 2 + r, h / 2),
(-w / 2, h / 2 - r),
(-w / 2, -h / 2 + r),
]
]
d = f"M{pair(pts[0])} L{pair(pts[1])} A{r:.15g},{r:.15g} {-rotation:.15g} 0 1 {pair(pts[2])} L{pair(pts[3])} A{r:.15g},{r:.15g} 0 0 1 {pair(pts[4])} L{pair(pts[5])} A{r:.15g},{r:.15g} 0 0 1 {pair(pts[6])} L{pair(pts[7])} A{r:.15g},{r:.15g} 0 0 1 {pair(pts[0])} Z"
return d, bounds(corners)
def property_outline(p):
vertices = []
for i in range(20000):
if f"VX{i}" not in p:
break
if f"VY{i}" not in p: raise ParseError("PCB outline vertex is missing Y")
if int(p.get(f"KIND{i}",0)) and not all(f"{key}{i}" in p for key in ("CX","CY","R","SA","EA")):
raise ParseError("PCB outline arc is missing native geometry")
vertices.append(
(
Point(x=unit(p[f"VX{i}"]), y=-unit(p.get(f"VY{i}", "0mil"))),
int(p.get(f"KIND{i}", 0)),
Point(
x=unit(p.get(f"CX{i}", "0mil")), y=-unit(p.get(f"CY{i}", "0mil"))
),
unit(p.get(f"R{i}", "0mil")),
float(p.get(f"SA{i}", 0)),
float(p.get(f"EA{i}", 0)),
)
)
if not vertices:
return "", bounds([])
pts = [v[0] for v in vertices]
commands = [f"M{pts[0].x:.15g},{pts[0].y:.15g}"]
for i, (pos, kind, center, r, sa, ea) in enumerate(vertices):
if kind and r > 0:
# Arc belongs to its starting vertex, ending at the next vertex.
arc = arc_path(center, r, sa, ea)
commands.append("L" + arc.split(" ", 1)[0][1:])
commands.append(arc.split(" ", 1)[1])
pts.extend(
[
Point(x=center.x - r, y=center.y - r),
Point(x=center.x + r, y=center.y + r),
]
)
nxt = vertices[(i + 1) % len(vertices)][0]
commands.append(f"L{nxt.x:.15g},{nxt.y:.15g}")
return " ".join(commands) + " Z", bounds(pts)