156 خطوط
5.6 KiB
Python
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)
|