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.
- gitcad_ecad-0.7.7/.gitignore +38 -0
- gitcad_ecad-0.7.7/PKG-INFO +40 -0
- gitcad_ecad-0.7.7/README.md +19 -0
- gitcad_ecad-0.7.7/pyproject.toml +31 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/__init__.py +34 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/annotate.py +63 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/autoroute.py +210 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/board.py +356 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/bom.py +96 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/component.py +71 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/connectivity.py +82 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/drc.py +404 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/envelope.py +127 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/excellon.py +67 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/fab.py +82 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/fpgen.py +114 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/gencad.py +176 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/gerber.py +177 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/ipc2581.py +278 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/ipcd356.py +50 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/kicadout.py +40 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/netderive.py +336 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/odb.py +283 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/route.py +99 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/schematic.py +249 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/schpdf.py +179 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/schsvg.py +372 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/sheetedit.py +298 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/spice.py +163 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/stats.py +99 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/strokefont.py +5 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/sync.py +113 -0
- gitcad_ecad-0.7.7/src/gitcad/ecad/teardrop.py +76 -0
- gitcad_ecad-0.7.7/src/gitcad/importers/altium.py +227 -0
- gitcad_ecad-0.7.7/src/gitcad/importers/eagle.py +75 -0
- gitcad_ecad-0.7.7/src/gitcad/importers/kicad.py +236 -0
- gitcad_ecad-0.7.7/src/gitcad/importers/kicad_sch.py +354 -0
- 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))
|