@@ -0,0 +1,155 @@
|
||||
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)
|
||||
مرجع در شماره جدید
مسدود کردن کاربر