breakneck 0.2.1__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.
- breakneck-0.2.1/.envrc +3 -0
- breakneck-0.2.1/.gitignore +10 -0
- breakneck-0.2.1/.python-version +1 -0
- breakneck-0.2.1/LICENSE +21 -0
- breakneck-0.2.1/PKG-INFO +92 -0
- breakneck-0.2.1/README.md +71 -0
- breakneck-0.2.1/breakneck/__init__.py +0 -0
- breakneck-0.2.1/breakneck/base.py +48 -0
- breakneck-0.2.1/breakneck/cli.py +305 -0
- breakneck-0.2.1/breakneck/conversions.py +370 -0
- breakneck-0.2.1/breakneck/footprint.py +234 -0
- breakneck-0.2.1/breakneck/test_polyfinder.py +32 -0
- breakneck-0.2.1/breakneck/track.py +222 -0
- breakneck-0.2.1/busy_test.py +8 -0
- breakneck-0.2.1/crash_test.py +10 -0
- breakneck-0.2.1/hello.py +6 -0
- breakneck-0.2.1/pyproject.toml +37 -0
- breakneck-0.2.1/scratchpad.ipynb +1196 -0
- breakneck-0.2.1/selection.ipynb +193 -0
- breakneck-0.2.1/stitching-via-screenshot.png +0 -0
- breakneck-0.2.1/uv.lock +1047 -0
- breakneck-0.2.1/viacheck.ipynb +296 -0
breakneck-0.2.1/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
breakneck-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Hat Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
breakneck-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: breakneck
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Stitching via visualization and manual forced neckdowns for KiCad
|
|
5
|
+
Project-URL: Homepage, https://github.com/hatlabs/breakneck
|
|
6
|
+
Author-email: Matti Airas <mairas@iki.fi>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: EDA,KiCad,PCB,breakneck,neckdown,stitching
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: kicad-python>=0.2.0
|
|
17
|
+
Requires-Dist: loguru>=0.7.3
|
|
18
|
+
Requires-Dist: shapely>=2.0.7
|
|
19
|
+
Requires-Dist: svgpathtools>=1.6.1
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Breakneck: Via Stitching and Manual Neckdown for KiCad Layouts
|
|
23
|
+
|
|
24
|
+
## Introduction
|
|
25
|
+
|
|
26
|
+
### Via Stitching
|
|
27
|
+
|
|
28
|
+
Ground via stitching is a common technique to reduce EMI and improve signal integrity in PCB designs. It involves placing GND vias next to signal vias to provide a return signal
|
|
29
|
+
path between reference ground planes and to minimize the loop area. This is important
|
|
30
|
+
for any signal with a high edge rate; nowadays basically every digital signal in a design.
|
|
31
|
+
|
|
32
|
+
Breakneck visualizes stitching via requirements by drawing lines between signal vias and their closest GND vias if the nearest GND via is further away than a specified distance. The user can then manually place the GND vias to meet the requirements. The lines are drawn on the User.Eco2 layer which should not be used for any other purpose (any existing content on this layer will be overwritten).
|
|
33
|
+
|
|
34
|
+
The screenshot below shows a design with routed signals lacking stitching vias. The yellow lines indicate the signal vias missing a GND via within a distance of 2mm.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
### Manual Neckdown
|
|
39
|
+
|
|
40
|
+
**NB:** [This `kicad-python` bug](https://gitlab.com/kicad/code/kicad-python/-/issues/18) prevents manual neckdown from being properly used at the moment.
|
|
41
|
+
|
|
42
|
+
Neckdown, or its inverse, fanout, refers to narrowing down of PCB tracks and their clearances when routing tracks to
|
|
43
|
+
fine-pitch components such as BGAs or QFNs. KiCad does not provide a built-in feature to automatically neckdown tracks,
|
|
44
|
+
and while the KiCad Custom Rules are powerful, they do not support neckdowns. It is possible to define rules for
|
|
45
|
+
tracks intersecting footprint courtyards, but the rule applies to the entire length of the track segment, not just the
|
|
46
|
+
part that intersects the courtyard.
|
|
47
|
+
|
|
48
|
+
Breakneck is a Python script that communicates with KiCad and cuts tracks at a specified distance from the intersection with a footprint courtyard.
|
|
49
|
+
After these track cuts, custom rules apply to the expected track segments. The layout isn't otherwise modified.
|
|
50
|
+
Automatic re-healing of broken tracks is prevented by nudging the track widths by one nanometer, making adjacent
|
|
51
|
+
tracks different widths.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
As a prerequisite, [install uv](https://docs.astral.sh/uv/getting-started/installation/).
|
|
56
|
+
|
|
57
|
+
To run breakneck without actually installing it:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
uvx breakneck <command> [options...]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This actually works really fast and the only downside is that you have to type `uvx` every time. If you want to install breakneck, run:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
uv tool install breakneck
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
**NOTE:** If you are running breakneck with `uvx`, prepend all breakneck commands below with `uvx`.
|
|
72
|
+
|
|
73
|
+
Run `breakneck -h` to see the available options.
|
|
74
|
+
|
|
75
|
+
`breakneck gndvia` draws stitching via lines on the User.Eco2 layer. The default distance is 2mm, but it can be changed with the `--distance` option.
|
|
76
|
+
|
|
77
|
+
It is possible to run `breakneck gndvia` repeatedly using `watch` to provide semi-real time updates while placing the GND vias:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
watch -n 1 breakneck gndvia
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**NOTE:** It turns out, every execution will bump the undo buffer, so until breakneck is made a bit smarter, it will become very difficult to undo any actual changes.
|
|
84
|
+
|
|
85
|
+
Breakneck has basic support for filtering the affected tracks and components by layer, netclass or selection.
|
|
86
|
+
|
|
87
|
+
## Limitations
|
|
88
|
+
|
|
89
|
+
- Component classes are not supported due to API limitations.
|
|
90
|
+
- Grouped tracks and footprints are ignored due to API limitations. It is possible to enter a group and
|
|
91
|
+
run `breakneck --selection` to process the group members.
|
|
92
|
+
- Multiple ground layers are not supported in `gndvia`. If you have isolated board sections, you will have a lot of bogus lines to the closest regular GND via.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Breakneck: Via Stitching and Manual Neckdown for KiCad Layouts
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
### Via Stitching
|
|
6
|
+
|
|
7
|
+
Ground via stitching is a common technique to reduce EMI and improve signal integrity in PCB designs. It involves placing GND vias next to signal vias to provide a return signal
|
|
8
|
+
path between reference ground planes and to minimize the loop area. This is important
|
|
9
|
+
for any signal with a high edge rate; nowadays basically every digital signal in a design.
|
|
10
|
+
|
|
11
|
+
Breakneck visualizes stitching via requirements by drawing lines between signal vias and their closest GND vias if the nearest GND via is further away than a specified distance. The user can then manually place the GND vias to meet the requirements. The lines are drawn on the User.Eco2 layer which should not be used for any other purpose (any existing content on this layer will be overwritten).
|
|
12
|
+
|
|
13
|
+
The screenshot below shows a design with routed signals lacking stitching vias. The yellow lines indicate the signal vias missing a GND via within a distance of 2mm.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### Manual Neckdown
|
|
18
|
+
|
|
19
|
+
**NB:** [This `kicad-python` bug](https://gitlab.com/kicad/code/kicad-python/-/issues/18) prevents manual neckdown from being properly used at the moment.
|
|
20
|
+
|
|
21
|
+
Neckdown, or its inverse, fanout, refers to narrowing down of PCB tracks and their clearances when routing tracks to
|
|
22
|
+
fine-pitch components such as BGAs or QFNs. KiCad does not provide a built-in feature to automatically neckdown tracks,
|
|
23
|
+
and while the KiCad Custom Rules are powerful, they do not support neckdowns. It is possible to define rules for
|
|
24
|
+
tracks intersecting footprint courtyards, but the rule applies to the entire length of the track segment, not just the
|
|
25
|
+
part that intersects the courtyard.
|
|
26
|
+
|
|
27
|
+
Breakneck is a Python script that communicates with KiCad and cuts tracks at a specified distance from the intersection with a footprint courtyard.
|
|
28
|
+
After these track cuts, custom rules apply to the expected track segments. The layout isn't otherwise modified.
|
|
29
|
+
Automatic re-healing of broken tracks is prevented by nudging the track widths by one nanometer, making adjacent
|
|
30
|
+
tracks different widths.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
As a prerequisite, [install uv](https://docs.astral.sh/uv/getting-started/installation/).
|
|
35
|
+
|
|
36
|
+
To run breakneck without actually installing it:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
uvx breakneck <command> [options...]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This actually works really fast and the only downside is that you have to type `uvx` every time. If you want to install breakneck, run:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
uv tool install breakneck
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
**NOTE:** If you are running breakneck with `uvx`, prepend all breakneck commands below with `uvx`.
|
|
51
|
+
|
|
52
|
+
Run `breakneck -h` to see the available options.
|
|
53
|
+
|
|
54
|
+
`breakneck gndvia` draws stitching via lines on the User.Eco2 layer. The default distance is 2mm, but it can be changed with the `--distance` option.
|
|
55
|
+
|
|
56
|
+
It is possible to run `breakneck gndvia` repeatedly using `watch` to provide semi-real time updates while placing the GND vias:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
watch -n 1 breakneck gndvia
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**NOTE:** It turns out, every execution will bump the undo buffer, so until breakneck is made a bit smarter, it will become very difficult to undo any actual changes.
|
|
63
|
+
|
|
64
|
+
Breakneck has basic support for filtering the affected tracks and components by layer, netclass or selection.
|
|
65
|
+
|
|
66
|
+
## Limitations
|
|
67
|
+
|
|
68
|
+
- Component classes are not supported due to API limitations.
|
|
69
|
+
- Grouped tracks and footprints are ignored due to API limitations. It is possible to enter a group and
|
|
70
|
+
run `breakneck --selection` to process the group members.
|
|
71
|
+
- Multiple ground layers are not supported in `gndvia`. If you have isolated board sections, you will have a lot of bogus lines to the closest regular GND via.
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Coords2D:
|
|
7
|
+
x: int
|
|
8
|
+
y: int
|
|
9
|
+
|
|
10
|
+
def __post_init__(self):
|
|
11
|
+
object.__setattr__(self, "x", int(self.x))
|
|
12
|
+
object.__setattr__(self, "y", int(self.y))
|
|
13
|
+
|
|
14
|
+
def __hash__(self):
|
|
15
|
+
return hash((self.x, self.y))
|
|
16
|
+
|
|
17
|
+
def __eq__(self, other):
|
|
18
|
+
return self.x == other.x and self.y == other.y
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, idx):
|
|
21
|
+
if idx == 0:
|
|
22
|
+
return self.x
|
|
23
|
+
elif idx == 1:
|
|
24
|
+
return self.y
|
|
25
|
+
else:
|
|
26
|
+
raise IndexError("Index out of range")
|
|
27
|
+
|
|
28
|
+
def __iter__(self):
|
|
29
|
+
yield self.x
|
|
30
|
+
yield self.y
|
|
31
|
+
|
|
32
|
+
def __len__(self):
|
|
33
|
+
return 2
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"Coords({self.x}, {self.y})"
|
|
37
|
+
|
|
38
|
+
def as_tol(self, tol_nm):
|
|
39
|
+
return Coords2D(round(self.x / tol_nm), round(self.y / tol_nm))
|
|
40
|
+
|
|
41
|
+
def as_tuple(self):
|
|
42
|
+
return self.x, self.y
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Sides(str, Enum):
|
|
46
|
+
front = "front"
|
|
47
|
+
back = "back"
|
|
48
|
+
both = "both"
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
import kipy.board
|
|
5
|
+
import kipy.board_types
|
|
6
|
+
import shapely.geometry
|
|
7
|
+
from kipy.board import BoardLayer as BL
|
|
8
|
+
from kipy.proto.board import board_types_pb2
|
|
9
|
+
from kipy.proto.common import base_types_pb2
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
import breakneck.conversions
|
|
13
|
+
import breakneck.footprint
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def break_tracks(
|
|
17
|
+
board: kipy.board.Board,
|
|
18
|
+
footprints: Sequence[breakneck.footprint.BNFootprint],
|
|
19
|
+
tracks: Sequence[kipy.board_types.Track | kipy.board_types.ArcTrack],
|
|
20
|
+
neck_length: float = 10,
|
|
21
|
+
dry_run: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Break tracks crossing the courtyards of footprints on a board.
|
|
25
|
+
"""
|
|
26
|
+
all_tracks = {t.id.value: t for t in tracks}
|
|
27
|
+
logger.debug("Getting all courtyards")
|
|
28
|
+
|
|
29
|
+
max_width = max(t.width for t in all_tracks.values())
|
|
30
|
+
remove_dict = {}
|
|
31
|
+
create_dict = {}
|
|
32
|
+
|
|
33
|
+
neck_length_nm = int(neck_length * 1000000) # mm to nm
|
|
34
|
+
|
|
35
|
+
for fpc in footprints:
|
|
36
|
+
logger.debug(f"Breaking tracks for {fpc.ref}")
|
|
37
|
+
items_to_remove, items_to_create = fpc.break_tracks(
|
|
38
|
+
list(all_tracks.values()), max_width, neck_length_nm
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# remove items from all_tracks and create_list
|
|
42
|
+
for item in items_to_remove:
|
|
43
|
+
all_tracks.pop(item.id.value)
|
|
44
|
+
create_dict.pop(item.id.value, None)
|
|
45
|
+
|
|
46
|
+
# add new items to all_tracks and create_list
|
|
47
|
+
for item in items_to_create:
|
|
48
|
+
all_tracks[item.id.value] = item
|
|
49
|
+
create_dict[item.id.value] = item
|
|
50
|
+
|
|
51
|
+
# add items to remove_list
|
|
52
|
+
for item in items_to_remove:
|
|
53
|
+
remove_dict[item.id.value] = item
|
|
54
|
+
|
|
55
|
+
commit = board.begin_commit()
|
|
56
|
+
board.remove_items(list(remove_dict.values()))
|
|
57
|
+
board.create_items(list(create_dict.values()))
|
|
58
|
+
if dry_run:
|
|
59
|
+
board.drop_commit(commit)
|
|
60
|
+
else:
|
|
61
|
+
board.push_commit(commit)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def execute_cut(args):
|
|
65
|
+
kicad = kipy.KiCad()
|
|
66
|
+
board = kicad.get_board()
|
|
67
|
+
|
|
68
|
+
tracks = board.get_tracks()
|
|
69
|
+
footprints = board.get_footprints()
|
|
70
|
+
|
|
71
|
+
num_all_tracks = len(tracks)
|
|
72
|
+
num_all_footprints = len(footprints)
|
|
73
|
+
|
|
74
|
+
if args.selection:
|
|
75
|
+
items = board.get_selection()
|
|
76
|
+
|
|
77
|
+
if len(items) == 0:
|
|
78
|
+
logger.error("No items selected")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# get tracks from selection
|
|
82
|
+
sel_tracks = [
|
|
83
|
+
item
|
|
84
|
+
for item in items
|
|
85
|
+
if isinstance(item, kipy.board_types.Track)
|
|
86
|
+
or isinstance(item, kipy.board_types.ArcTrack)
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# get footprints from selection
|
|
90
|
+
sel_footprints = [
|
|
91
|
+
item
|
|
92
|
+
for item in items
|
|
93
|
+
if isinstance(item, kipy.board_types.FootprintInstance)
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# If selection has no tracks or footprints, use all tracks and footprints
|
|
97
|
+
|
|
98
|
+
if sel_tracks:
|
|
99
|
+
tracks = sel_tracks
|
|
100
|
+
if sel_footprints:
|
|
101
|
+
footprints = sel_footprints
|
|
102
|
+
|
|
103
|
+
bnfootprints = breakneck.footprint.get_bn_footprints(footprints)
|
|
104
|
+
|
|
105
|
+
if args.sides:
|
|
106
|
+
if args.sides == "front":
|
|
107
|
+
tracks = [t for t in tracks if t.layer == BL.BL_F_Cu]
|
|
108
|
+
bnfootprints = [
|
|
109
|
+
fpc for fpc in bnfootprints if not fpc.front_courtyard.is_empty
|
|
110
|
+
]
|
|
111
|
+
elif args.sides == "back":
|
|
112
|
+
tracks = [t for t in tracks if t.layer == BL.BL_B_Cu]
|
|
113
|
+
bnfootprints = [
|
|
114
|
+
fpc for fpc in bnfootprints if not fpc.back_courtyard.is_empty
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if args.netclass:
|
|
118
|
+
nets = board.get_nets(args.netclass)
|
|
119
|
+
net_names = set(n.name for n in nets)
|
|
120
|
+
tracks = [t for t in tracks if t.net in net_names]
|
|
121
|
+
|
|
122
|
+
num_tracks = len(tracks)
|
|
123
|
+
num_footprints = len(bnfootprints)
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
f"Breaking tracks on {num_tracks}/{num_all_tracks} tracks "
|
|
127
|
+
f"and {num_footprints}/{num_all_footprints} footprints"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if num_tracks == 0:
|
|
131
|
+
logger.warning("No tracks to break")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if num_footprints == 0:
|
|
135
|
+
logger.warning("No footprints to break tracks on")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
break_tracks(board, bnfootprints, tracks, dry_run=args.dry_run)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def execute_gndvia_check(args):
|
|
142
|
+
max_distance = args.distance * 1000000 # mm to nm
|
|
143
|
+
|
|
144
|
+
kicad = kipy.KiCad()
|
|
145
|
+
board = kicad.get_board()
|
|
146
|
+
|
|
147
|
+
vias = board.get_vias()
|
|
148
|
+
|
|
149
|
+
all_pads = board.get_pads()
|
|
150
|
+
gnd_tht_pads = [
|
|
151
|
+
pad
|
|
152
|
+
for pad in all_pads
|
|
153
|
+
if pad.net.name == "GND" and pad.proto.type == kipy.board_types.PadType.PT_PTH
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# We treat GND THT pads as vias as well
|
|
157
|
+
gnd_vias = [via for via in vias if via.net.name == "GND"] + gnd_tht_pads
|
|
158
|
+
|
|
159
|
+
gnd_via_coords = [
|
|
160
|
+
breakneck.conversions.vector2_as_coords(via.position) for via in gnd_vias
|
|
161
|
+
]
|
|
162
|
+
gnd_via_points = [shapely.geometry.Point(coords) for coords in gnd_via_coords]
|
|
163
|
+
|
|
164
|
+
gnd_via_tree = shapely.STRtree(gnd_via_points)
|
|
165
|
+
|
|
166
|
+
non_gnd_vias = [via for via in vias if via.net.name != "GND"]
|
|
167
|
+
non_gnd_via_coords = [
|
|
168
|
+
breakneck.conversions.vector2_as_coords(via.position) for via in non_gnd_vias
|
|
169
|
+
]
|
|
170
|
+
non_gnd_via_points = [
|
|
171
|
+
shapely.geometry.Point(coords) for coords in non_gnd_via_coords
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
nearests_idxs = [gnd_via_tree.nearest(pt) for pt in non_gnd_via_points]
|
|
175
|
+
|
|
176
|
+
nearests = [gnd_via_points[idx] for idx in nearests_idxs]
|
|
177
|
+
distances = [
|
|
178
|
+
shapely.distance(nearest, non_gnd_via)
|
|
179
|
+
for nearest, non_gnd_via in zip(nearests, non_gnd_via_points)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# Get those non-gnd vias that are more than 3000000 nm away from the nearest gnd via
|
|
183
|
+
lone_via_idxs = [
|
|
184
|
+
i for i, distance in enumerate(distances) if distance > max_distance
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
new_lines = []
|
|
188
|
+
for idx in lone_via_idxs:
|
|
189
|
+
# Draw a line from the lone via to the nearest gnd via on layer BL.BL_Eco2_User
|
|
190
|
+
# with thickness 0.05mm = 50000 nm
|
|
191
|
+
ngvc = non_gnd_via_coords[idx]
|
|
192
|
+
gvc = gnd_via_coords[nearests_idxs[idx]]
|
|
193
|
+
start_proto_vector = base_types_pb2.Vector2(x_nm=ngvc[0], y_nm=-ngvc[1])
|
|
194
|
+
end_proto_vector = base_types_pb2.Vector2(x_nm=gvc[0], y_nm=-gvc[1])
|
|
195
|
+
|
|
196
|
+
shape = base_types_pb2.GraphicShape(
|
|
197
|
+
segment=base_types_pb2.GraphicSegmentAttributes(
|
|
198
|
+
start=start_proto_vector, end=end_proto_vector
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
board_graphic_shape = board_types_pb2.BoardGraphicShape(shape=shape)
|
|
203
|
+
|
|
204
|
+
segment = kipy.board_types.BoardSegment(proto=board_graphic_shape)
|
|
205
|
+
segment.layer = BL.BL_Eco2_User
|
|
206
|
+
segment.attributes.stroke.width = 100000
|
|
207
|
+
new_lines.append(segment)
|
|
208
|
+
|
|
209
|
+
shapes = board.get_shapes()
|
|
210
|
+
eco2_shapes = [shape for shape in shapes if shape.layer == BL.BL_Eco2_User]
|
|
211
|
+
|
|
212
|
+
commit = board.begin_commit()
|
|
213
|
+
board.remove_items(eco2_shapes)
|
|
214
|
+
board.create_items(new_lines)
|
|
215
|
+
if args.dry_run:
|
|
216
|
+
board.drop_commit(commit)
|
|
217
|
+
else:
|
|
218
|
+
board.push_commit(commit)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_args():
|
|
222
|
+
parser = argparse.ArgumentParser()
|
|
223
|
+
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"--dry-run",
|
|
226
|
+
"-n",
|
|
227
|
+
action="store_true",
|
|
228
|
+
help="Do everything except commit the changes",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
232
|
+
|
|
233
|
+
cut_parser = subparsers.add_parser("cut", help="Add neckdowns to tracks")
|
|
234
|
+
|
|
235
|
+
cut_parser.add_argument(
|
|
236
|
+
"--selection", action="store_true", help="Use selected items"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
cut_parser.add_argument(
|
|
240
|
+
"--sides",
|
|
241
|
+
choices=["front", "back", "both"],
|
|
242
|
+
default="both",
|
|
243
|
+
help="Side of the board to break tracks on",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
cut_parser.add_argument(
|
|
247
|
+
"--length",
|
|
248
|
+
"-l",
|
|
249
|
+
type=float,
|
|
250
|
+
default=10,
|
|
251
|
+
help="Neckdown length outside of the courtyard, in mm",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
cut_parser.add_argument("--netclass", type=str, help="Netclass to break tracks on")
|
|
255
|
+
|
|
256
|
+
gnd_via_parser = subparsers.add_parser("gndvia", help="Visualize GND via distances")
|
|
257
|
+
|
|
258
|
+
gnd_via_parser.add_argument(
|
|
259
|
+
"--distance",
|
|
260
|
+
"-d",
|
|
261
|
+
type=float,
|
|
262
|
+
default=2,
|
|
263
|
+
help="Maximum distance to GND via, in mm",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
subparsers.add_parser("clear", help="Remove annotations from User.Eco2 layer")
|
|
267
|
+
|
|
268
|
+
return parser.parse_args()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def execute_clear(args):
|
|
272
|
+
kicad = kipy.KiCad()
|
|
273
|
+
board = kicad.get_board()
|
|
274
|
+
|
|
275
|
+
shapes = board.get_shapes()
|
|
276
|
+
eco2_shapes = [shape for shape in shapes if shape.layer == BL.BL_Eco2_User]
|
|
277
|
+
|
|
278
|
+
if not shapes:
|
|
279
|
+
logger.info("No shapes to clear")
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
commit = board.begin_commit()
|
|
283
|
+
board.remove_items(eco2_shapes)
|
|
284
|
+
if args.dry_run:
|
|
285
|
+
board.drop_commit(commit)
|
|
286
|
+
else:
|
|
287
|
+
board.push_commit(commit)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def main():
|
|
291
|
+
args = parse_args()
|
|
292
|
+
|
|
293
|
+
match args.command:
|
|
294
|
+
case "cut":
|
|
295
|
+
execute_cut(args)
|
|
296
|
+
|
|
297
|
+
case "gndvia":
|
|
298
|
+
execute_gndvia_check(args)
|
|
299
|
+
|
|
300
|
+
case "clear":
|
|
301
|
+
execute_clear(args)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
main()
|