gitcad-ecad 0.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. gitcad_ecad-0.7.7/.gitignore +38 -0
  2. gitcad_ecad-0.7.7/PKG-INFO +40 -0
  3. gitcad_ecad-0.7.7/README.md +19 -0
  4. gitcad_ecad-0.7.7/pyproject.toml +31 -0
  5. gitcad_ecad-0.7.7/src/gitcad/ecad/__init__.py +34 -0
  6. gitcad_ecad-0.7.7/src/gitcad/ecad/annotate.py +63 -0
  7. gitcad_ecad-0.7.7/src/gitcad/ecad/autoroute.py +210 -0
  8. gitcad_ecad-0.7.7/src/gitcad/ecad/board.py +356 -0
  9. gitcad_ecad-0.7.7/src/gitcad/ecad/bom.py +96 -0
  10. gitcad_ecad-0.7.7/src/gitcad/ecad/component.py +71 -0
  11. gitcad_ecad-0.7.7/src/gitcad/ecad/connectivity.py +82 -0
  12. gitcad_ecad-0.7.7/src/gitcad/ecad/drc.py +404 -0
  13. gitcad_ecad-0.7.7/src/gitcad/ecad/envelope.py +127 -0
  14. gitcad_ecad-0.7.7/src/gitcad/ecad/excellon.py +67 -0
  15. gitcad_ecad-0.7.7/src/gitcad/ecad/fab.py +82 -0
  16. gitcad_ecad-0.7.7/src/gitcad/ecad/fpgen.py +114 -0
  17. gitcad_ecad-0.7.7/src/gitcad/ecad/gencad.py +176 -0
  18. gitcad_ecad-0.7.7/src/gitcad/ecad/gerber.py +177 -0
  19. gitcad_ecad-0.7.7/src/gitcad/ecad/ipc2581.py +278 -0
  20. gitcad_ecad-0.7.7/src/gitcad/ecad/ipcd356.py +50 -0
  21. gitcad_ecad-0.7.7/src/gitcad/ecad/kicadout.py +40 -0
  22. gitcad_ecad-0.7.7/src/gitcad/ecad/netderive.py +336 -0
  23. gitcad_ecad-0.7.7/src/gitcad/ecad/odb.py +283 -0
  24. gitcad_ecad-0.7.7/src/gitcad/ecad/route.py +99 -0
  25. gitcad_ecad-0.7.7/src/gitcad/ecad/schematic.py +249 -0
  26. gitcad_ecad-0.7.7/src/gitcad/ecad/schpdf.py +179 -0
  27. gitcad_ecad-0.7.7/src/gitcad/ecad/schsvg.py +372 -0
  28. gitcad_ecad-0.7.7/src/gitcad/ecad/sheetedit.py +298 -0
  29. gitcad_ecad-0.7.7/src/gitcad/ecad/spice.py +163 -0
  30. gitcad_ecad-0.7.7/src/gitcad/ecad/stats.py +99 -0
  31. gitcad_ecad-0.7.7/src/gitcad/ecad/strokefont.py +5 -0
  32. gitcad_ecad-0.7.7/src/gitcad/ecad/sync.py +113 -0
  33. gitcad_ecad-0.7.7/src/gitcad/ecad/teardrop.py +76 -0
  34. gitcad_ecad-0.7.7/src/gitcad/importers/altium.py +227 -0
  35. gitcad_ecad-0.7.7/src/gitcad/importers/eagle.py +75 -0
  36. gitcad_ecad-0.7.7/src/gitcad/importers/kicad.py +236 -0
  37. gitcad_ecad-0.7.7/src/gitcad/importers/kicad_sch.py +354 -0
  38. gitcad_ecad-0.7.7/src/gitcad/importers/sexp.py +71 -0
@@ -0,0 +1,38 @@
1
+ # Generated geometry is a build artifact, NOT source (ADR-0004). Never commit it.
2
+ *.brep
3
+ *.step
4
+ *.stp
5
+ *.stl
6
+ *.gltf
7
+ *.glb
8
+ *.iges
9
+ *.igs
10
+ # Drawings are generated from the model too.
11
+ /build-artifacts/
12
+ *.dxf
13
+ # PDFs generated from drawings (keep hand-authored docs elsewhere if needed).
14
+ /out/
15
+
16
+ # Python
17
+ __pycache__/
18
+ *.py[cod]
19
+ *.egg-info/
20
+ .eggs/
21
+ build/
22
+ dist/
23
+ .venv/
24
+ venv/
25
+
26
+ # Tooling
27
+ .pytest_cache/
28
+ .coverage
29
+ .coverage.*
30
+ htmlcov/
31
+ .mypy_cache/
32
+ .ruff_cache/
33
+
34
+ # Editors / OS
35
+ .vscode/
36
+ .idea/
37
+ .DS_Store
38
+ Thumbs.db
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitcad-ecad
3
+ Version: 0.7.7
4
+ Summary: gitcad electrical: schematic+ERC, board, DRC, connectivity, fab outputs, KiCad import
5
+ Project-URL: Homepage, https://gitcad.xyz
6
+ Project-URL: Repository, https://github.com/gitcad-xyz/gitcad
7
+ Project-URL: Issues, https://github.com/gitcad-xyz/gitcad/issues
8
+ Author-email: Dan Willis <danielcwillis@gmail.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: drc,ecad,eda,erc,gerber,ipc-2581,kicad,pcb,schematic
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Manufacturing
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: gitcad-core==0.7.7
20
+ Description-Content-Type: text/markdown
21
+
22
+ # gitcad-ecad
23
+
24
+ Electrical domain for [**gitcad**](https://gitcad.xyz) — schematic through
25
+ fabrication, one git-native substrate.
26
+
27
+ - **Schematic capture** — netlist-level and sheet-level, with ERC and typed
28
+ electrical-envelope checks (voltage/current specs on interfaces).
29
+ - **Board** — multi-layer stackup, tracks/zones/vias (incl. blind/buried),
30
+ net classes, keepouts, DRC with rule packs, connectivity.
31
+ - **Schematic ↔ board parity** and back-annotation.
32
+ - **Fab outputs** — Gerber (RS-274X), Excellon drill, pick-and-place, IPC-2581,
33
+ BOM.
34
+ - **KiCad import** — `.kicad_sch` / `.kicad_pcb` with real wire geometry and
35
+ hierarchical sheet instances.
36
+
37
+ Pure Python, no third-party runtime dependencies. Install the full system with
38
+ `pip install gitcad`.
39
+
40
+ Apache-2.0 · https://github.com/gitcad-xyz/gitcad
@@ -0,0 +1,19 @@
1
+ # gitcad-ecad
2
+
3
+ Electrical domain for [**gitcad**](https://gitcad.xyz) — schematic through
4
+ fabrication, one git-native substrate.
5
+
6
+ - **Schematic capture** — netlist-level and sheet-level, with ERC and typed
7
+ electrical-envelope checks (voltage/current specs on interfaces).
8
+ - **Board** — multi-layer stackup, tracks/zones/vias (incl. blind/buried),
9
+ net classes, keepouts, DRC with rule packs, connectivity.
10
+ - **Schematic ↔ board parity** and back-annotation.
11
+ - **Fab outputs** — Gerber (RS-274X), Excellon drill, pick-and-place, IPC-2581,
12
+ BOM.
13
+ - **KiCad import** — `.kicad_sch` / `.kicad_pcb` with real wire geometry and
14
+ hierarchical sheet instances.
15
+
16
+ Pure Python, no third-party runtime dependencies. Install the full system with
17
+ `pip install gitcad`.
18
+
19
+ Apache-2.0 · https://github.com/gitcad-xyz/gitcad
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gitcad-ecad"
7
+ version = "0.7.7"
8
+ description = "gitcad electrical: schematic+ERC, board, DRC, connectivity, fab outputs, KiCad import"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ authors = [{name = "Dan Willis", email = "danielcwillis@gmail.com"}]
13
+ keywords = ["eda", "ecad", "pcb", "schematic", "gerber", "drc", "erc", "kicad", "ipc-2581"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Manufacturing",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
22
+ ]
23
+ dependencies = ["gitcad-core==0.7.7"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://gitcad.xyz"
27
+ Repository = "https://github.com/gitcad-xyz/gitcad"
28
+ Issues = "https://github.com/gitcad-xyz/gitcad/issues"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/gitcad"]
@@ -0,0 +1,34 @@
1
+ """gitcad-ecad — the electrical domain.
2
+
3
+ v0.1 scope: a text-first board model plus the fabrication outputs that let a
4
+ user actually order a PCB — Gerber X2 per layer, Excellon drill, pick-and-place.
5
+ Schematic capture, ERC, and the full DRC engine follow (see
6
+ docs/research/feature-map.md Part B).
7
+
8
+ Like the mechanical document, the board is canonical text (ADR-0004): the
9
+ model diffs and merges in git, and every generated output is deterministic —
10
+ the same board text produces byte-identical Gerbers on any machine.
11
+ """
12
+
13
+ from gitcad.ecad.board import Board, Component, Footprint, MountingHole, Pad, Track, Via, Zone
14
+ from gitcad.ecad.fab import export_fab
15
+ from gitcad.ecad.bom import bom, bom_csv, mpn_component
16
+ from gitcad.ecad.component import footprint_from_part, footprint_to_part
17
+ from gitcad.ecad.connectivity import check_connectivity
18
+ from gitcad.ecad.drc import Rule, RulePack, default_rules, run_drc
19
+ from gitcad.ecad.route import pad_position, route
20
+ from gitcad.ecad.schematic import (Pin, SchComponent, Schematic, board_parity,
21
+ merge_schematics)
22
+ from gitcad.ecad.envelope import check_envelopes, net_voltage, power_budget
23
+ from gitcad.ecad.netderive import derive_nets, sheet_parity
24
+ from gitcad.ecad.spice import sim_check, to_spice
25
+ from gitcad.ecad.schsvg import schematic_to_svg
26
+
27
+ __all__ = ["Board", "Component", "Footprint", "MountingHole", "Pad", "Track", "Via", "Zone",
28
+ "export_fab", "Schematic", "SchComponent", "Pin", "board_parity",
29
+ "Rule", "RulePack", "default_rules", "run_drc", "check_connectivity",
30
+ "pad_position", "route", "footprint_to_part", "footprint_from_part",
31
+ "mpn_component", "bom", "bom_csv", "schematic_to_svg",
32
+ "merge_schematics", "derive_nets", "sheet_parity",
33
+ "check_envelopes", "net_voltage", "power_budget",
34
+ "to_spice", "sim_check"]
@@ -0,0 +1,63 @@
1
+ """schematic_annotate — deterministic reference numbering (KiCad-map P4).
2
+
3
+ Components with placeholder refs (``R?``, ``U?`` — the KiCad convention)
4
+ get the lowest free number for their prefix, in a deterministic order:
5
+ sheet position (top-to-bottom, then left-to-right) when placements exist,
6
+ declaration order otherwise. Existing numbered refs are never touched —
7
+ annotation fills gaps, it does not reshuffle a reviewed design. Net pin
8
+ references follow the renames atomically.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ from gitcad.ecad.schematic import Schematic
16
+ from gitcad.errors import GitcadError
17
+
18
+ _REF = re.compile(r"^([A-Za-z_]+)(\?|\d+)$")
19
+
20
+
21
+ def annotate(sch: Schematic) -> dict[str, str]:
22
+ """Assign numbers to ``?`` refs in place; returns {old_label: new_ref}
23
+ keyed by a positional label (``R?@2`` for the third placeholder R).
24
+
25
+ Nets may not reference placeholders — ``R?.1`` is ambiguous when two
26
+ unannotated Rs exist, so we refuse rather than guess: annotate first,
27
+ then connect (or connect by final refs)."""
28
+ for net, prs in sch.nets.items():
29
+ for pr in prs:
30
+ if "?" in pr.split(".", 1)[0]:
31
+ raise GitcadError(
32
+ f"net {net!r} references placeholder ref {pr!r} — "
33
+ "annotate before connecting, or connect by the final ref")
34
+ used: dict[str, set[int]] = {}
35
+ placeholders = []
36
+ decl_counter: dict[str, int] = {}
37
+ for i, comp in enumerate(sch.components):
38
+ m = _REF.match(comp.ref)
39
+ if not m:
40
+ raise GitcadError(f"unparseable ref {comp.ref!r} (want PREFIX+number or PREFIX?)")
41
+ prefix, num = m.groups()
42
+ if num == "?":
43
+ at = comp.attrs.get("at")
44
+ sort_key = (at[1], at[0], i) if at else (float("inf"), float("inf"), i)
45
+ k = decl_counter.get(prefix, 0) # label by DECLARATION order
46
+ decl_counter[prefix] = k + 1
47
+ placeholders.append((prefix, sort_key, f"{prefix}?@{k}", comp))
48
+ else:
49
+ used.setdefault(prefix, set()).add(int(num))
50
+
51
+ renames: dict[str, str] = {}
52
+ counters: dict[str, int] = {}
53
+ # numbering order is READING order (top-to-bottom, left-to-right)
54
+ for prefix, _key, label, comp in sorted(placeholders, key=lambda p: (p[0], p[1])):
55
+ n = counters.get(prefix, 1)
56
+ taken = used.setdefault(prefix, set())
57
+ while n in taken:
58
+ n += 1
59
+ taken.add(n)
60
+ counters[prefix] = n + 1
61
+ renames[label] = f"{prefix}{n}"
62
+ comp.ref = f"{prefix}{n}"
63
+ return renames
@@ -0,0 +1,210 @@
1
+ """Autorouting assist v1 — a grid maze router for agents.
2
+
3
+ Not push-and-shove: a deterministic BFS over a clearance-aware obstacle
4
+ grid (Lee router), one net at a time, through-vias for layer changes.
5
+ The result is ordinary Tracks/Vias appended to the board — the same
6
+ check chain that gates hand routing (DRC, connectivity) gates this.
7
+ Honest refusal: no path means ``autoroute-no-path``, never a
8
+ rules-violating trace.
9
+
10
+ Scope v1: routes between the net's pad centers on the outer layers (or
11
+ any copper layer list you pass), rectilinear steps on a fixed grid.
12
+ Pour-covered boards refuse fast — a pour IS the route.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from gitcad.ecad.board import Board, Track, Via
18
+ from gitcad.errors import GitcadError
19
+
20
+
21
+ def autoroute(board: Board, net: str, *, grid: float = 0.25,
22
+ width: float = 0.25, clearance: float = 0.2,
23
+ layers: tuple[str, ...] | None = None,
24
+ via_drill: float = 0.4, via_diameter: float = 0.8) -> dict:
25
+ """Route every pad of ``net`` into one connected tree. Returns
26
+ {"tracks": n, "vias": n}. Raises GitcadError when no path exists."""
27
+ copper = board.copper_layers()
28
+ layer_list = list(layers) if layers else ["top", "bottom"]
29
+ for ly in layer_list:
30
+ if ly not in copper:
31
+ raise GitcadError(f"unknown layer {ly!r}")
32
+
33
+ minx, miny, maxx, maxy = board.bbox()
34
+ nx = int((maxx - minx) / grid) + 1
35
+ ny = int((maxy - miny) / grid) + 1
36
+ if nx * ny > 1_500_000:
37
+ raise GitcadError(
38
+ f"autoroute grid {nx}x{ny} too large — coarsen `grid`")
39
+
40
+ def cell(x: float, y: float) -> tuple[int, int]:
41
+ return (round((x - minx) / grid), round((y - miny) / grid))
42
+
43
+ def pos(c: tuple[int, int]) -> tuple[float, float]:
44
+ return (minx + c[0] * grid, miny + c[1] * grid)
45
+
46
+ # -- obstacle rasterization (other-net copper, inflated) -------------------
47
+ # tracks need clearance + track half-width; via barrels are wider, so
48
+ # via placement checks a second grid with the bigger margin
49
+ margin = clearance + width / 2
50
+ margin_via = clearance + via_diameter / 2
51
+ blocked: list[set] = [set() for _ in layer_list]
52
+ blocked_via: list[set] = [set() for _ in layer_list]
53
+ li = {name: i for i, name in enumerate(layer_list)}
54
+
55
+ def block_disc(x: float, y: float, r: float, idxs) -> None:
56
+ for grid_set, mg in ((blocked, margin), (blocked_via, margin_via)):
57
+ rr = r + mg
58
+ c0x, c0y = cell(x - rr, y - rr)
59
+ c1x, c1y = cell(x + rr, y + rr)
60
+ for cx in range(max(0, c0x), min(nx, c1x + 1)):
61
+ for cy in range(max(0, c0y), min(ny, c1y + 1)):
62
+ px, py = pos((cx, cy))
63
+ if (px - x) ** 2 + (py - y) ** 2 <= rr * rr:
64
+ for i in idxs:
65
+ grid_set[i].add((cx, cy))
66
+
67
+ def block_seg(x1, y1, x2, y2, half_w, idx) -> None:
68
+ steps = max(1, int(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 / grid) * 2)
69
+ for s in range(steps + 1):
70
+ t = s / steps
71
+ block_disc(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, half_w, [idx])
72
+
73
+ all_idx = list(range(len(layer_list)))
74
+ for comp in board.components:
75
+ for pad, bx, by, rot in comp.placed_pads():
76
+ if comp.nets.get(pad.name, "") == net:
77
+ continue
78
+ w, h = (pad.h, pad.w) if round(rot) % 180 == 90 else (pad.w, pad.h)
79
+ r = max(w, h) / 2
80
+ if pad.drill is not None:
81
+ block_disc(bx, by, r, all_idx)
82
+ elif comp.side in li:
83
+ block_disc(bx, by, r, [li[comp.side]])
84
+ for v in board.vias:
85
+ if v.net == net:
86
+ continue
87
+ sp = set(v.span(copper))
88
+ idxs = [li[ly] for ly in layer_list if ly in sp] or all_idx
89
+ block_disc(v.x, v.y, v.diameter / 2, idxs)
90
+ for t in board.tracks:
91
+ if t.net == net or t.layer not in li:
92
+ continue
93
+ block_seg(t.x1, t.y1, t.x2, t.y2, t.width / 2, li[t.layer])
94
+ for z in board.zones:
95
+ if z.kind == "keepout":
96
+ xs = [p[0] for p in z.polygon]
97
+ ys = [p[1] for p in z.polygon]
98
+ for ly in ([z.layer] if z.layer in li else []):
99
+ c0, c1 = cell(min(xs), min(ys)), cell(max(xs), max(ys))
100
+ for cx in range(max(0, c0[0]), min(nx, c1[0] + 1)):
101
+ for cy in range(max(0, c0[1]), min(ny, c1[1] + 1)):
102
+ blocked[li[ly]].add((cx, cy))
103
+ elif z.kind == "copper" and z.net != net and z.layer in li:
104
+ xs = [p[0] for p in z.polygon]
105
+ ys = [p[1] for p in z.polygon]
106
+ c0, c1 = cell(min(xs), min(ys)), cell(max(xs), max(ys))
107
+ for cx in range(max(0, c0[0]), min(nx, c1[0] + 1)):
108
+ for cy in range(max(0, c0[1]), min(ny, c1[1] + 1)):
109
+ blocked[li[z.layer]].add((cx, cy))
110
+
111
+ # -- terminals -------------------------------------------------------------
112
+ terminals: list[tuple[tuple[int, int], int]] = [] # (cell, layer idx)
113
+ for comp in board.components:
114
+ for pad, bx, by, _rot in comp.placed_pads():
115
+ if comp.nets.get(pad.name, "") != net:
116
+ continue
117
+ if pad.drill is not None:
118
+ terminals.append((cell(bx, by), 0))
119
+ elif comp.side in li:
120
+ terminals.append((cell(bx, by), li[comp.side]))
121
+ if len(terminals) < 2:
122
+ raise GitcadError(f"net {net!r} has fewer than 2 routable pads")
123
+
124
+ # -- sequential Lee routing ------------------------------------------------
125
+ tree: set[tuple[int, int, int]] = set()
126
+ first_cell, first_li = terminals[0]
127
+ tree.add((first_cell[0], first_cell[1], first_li))
128
+ tracks_added = vias_added = 0
129
+
130
+ for term_cell, term_li in terminals[1:]:
131
+ start = (term_cell[0], term_cell[1], term_li)
132
+ if start in tree:
133
+ continue
134
+ import heapq
135
+
136
+ prev: dict = {start: None}
137
+ dist: dict = {start: 0}
138
+ heap: list[tuple[int, tuple]] = [(0, start)]
139
+ via_cost = max(1, int(3.0 / grid)) # a via "costs" ~3 mm
140
+ goal = None
141
+ while heap:
142
+ d, cur = heapq.heappop(heap)
143
+ if d > dist.get(cur, 1 << 30):
144
+ continue
145
+ if cur in tree:
146
+ goal = cur
147
+ break
148
+ cx, cy, cl = cur
149
+ steps = [((cx + 1, cy, cl), 1), ((cx - 1, cy, cl), 1),
150
+ ((cx, cy + 1, cl), 1), ((cx, cy - 1, cl), 1)]
151
+ for other in range(len(layer_list)): # through-via move
152
+ if other != cl:
153
+ steps.append(((cx, cy, other), via_cost))
154
+ for nxt, cost in steps:
155
+ tx, ty, tl = nxt
156
+ if not (0 <= tx < nx and 0 <= ty < ny):
157
+ continue
158
+ if tl != cl: # via: all layers clear
159
+ if any((tx, ty) in blocked_via[i]
160
+ for i in range(len(layer_list))):
161
+ continue
162
+ elif (tx, ty) in blocked[tl]:
163
+ continue
164
+ nd = d + cost
165
+ if nd < dist.get(nxt, 1 << 30):
166
+ dist[nxt] = nd
167
+ prev[nxt] = cur
168
+ heapq.heappush(heap, (nd, nxt))
169
+ if goal is None:
170
+ raise GitcadError(f"autoroute-no-path:{net}")
171
+
172
+ # walk back, merging straight runs into tracks
173
+ path = []
174
+ n_ = goal
175
+ while n_ is not None:
176
+ path.append(n_)
177
+ n_ = prev[n_]
178
+ run_start = path[0]
179
+ for a, b in zip(path, path[1:]):
180
+ if a[2] != b[2]: # layer change: via at a
181
+ if run_start[:2] != a[:2]:
182
+ _emit(board, pos(run_start[:2]), pos(a[:2]),
183
+ layer_list[a[2]], width, net)
184
+ tracks_added += 1
185
+ board.vias.append(Via(*pos(a[:2]), drill=via_drill,
186
+ diameter=via_diameter, net=net))
187
+ vias_added += 1
188
+ run_start = b
189
+ elif _turns(run_start, a, b):
190
+ _emit(board, pos(run_start[:2]), pos(a[:2]),
191
+ layer_list[a[2]], width, net)
192
+ tracks_added += 1
193
+ run_start = a
194
+ last = path[-1]
195
+ if run_start[:2] != last[:2]:
196
+ _emit(board, pos(run_start[:2]), pos(last[:2]),
197
+ layer_list[last[2]], width, net)
198
+ tracks_added += 1
199
+ tree.update(path)
200
+
201
+ return {"tracks": tracks_added, "vias": vias_added}
202
+
203
+
204
+ def _turns(run_start, a, b) -> bool:
205
+ return ((b[0] - run_start[0]) * (a[1] - run_start[1])
206
+ != (b[1] - run_start[1]) * (a[0] - run_start[0]))
207
+
208
+
209
+ def _emit(board: Board, p1, p2, layer: str, width: float, net: str) -> None:
210
+ board.tracks.append(Track(p1[0], p1[1], p2[0], p2[1], width, layer, net))