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)