kcaa 0.2.0__tar.gz → 0.2.2__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.
- {kcaa-0.2.0 → kcaa-0.2.2}/PKG-INFO +3 -2
- {kcaa-0.2.0 → kcaa-0.2.2}/README.md +1 -1
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/netlist_resources.py +9 -5
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/grid_a_star.py +85 -45
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/router.py +535 -121
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/world_model.py +319 -32
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/netlist_tools.py +51 -36
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_edit_tools.py +14 -13
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_group_tools.py +29 -29
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_placement_helpers.py +10 -10
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_placement_tools.py +5 -5
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_query_tools.py +23 -15
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_routing_tools.py +20 -15
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_zone_tools.py +1 -1
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/placement_helpers.py +5 -2
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/schematic_group_tools.py +21 -11
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/sheet_tools.py +2 -2
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/symbol_edit_tools.py +126 -47
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/wire_edit_tools.py +14 -12
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/netlist_parser.py +266 -65
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pcb_board_utils.py +133 -28
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pcb_footprint_utils.py +4 -2
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pcb_library_utils.py +4 -0
- kcaa-0.2.2/kcaa/utils/skip_helpers.py +245 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/symbol_geometry.py +3 -3
- {kcaa-0.2.0 → kcaa-0.2.2}/kicad_plugin/README.md +3 -3
- {kcaa-0.2.0 → kcaa-0.2.2}/pyproject.toml +2 -1
- kcaa-0.2.0/kcaa/utils/skip_helpers.py +0 -196
- {kcaa-0.2.0 → kcaa-0.2.2}/.gitignore +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/LICENSE +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/README_CN.md +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/context.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/prompts/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/prompts/bom_prompts.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/prompts/drc_prompt.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/prompts/pattern_prompts.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/prompts/templates.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/bom_resources.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/drc_resources.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/files.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/pattern_resources.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/resources/projects.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/a_star.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/path_postprocess.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/via_check.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/router/visibility_graph.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/server.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/analysis_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/bom_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/drc_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/export_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/kipy_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pattern_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/pcb_library_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/project_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/skill_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/symbol_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/validation_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/tools/version_tools.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/__init__.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/boundary_validator.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/component_utils.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/config.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/file_utils.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/footprint_database.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/footprint_index_manager.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/ipc_drc.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/kicad_api_detection.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/kicad_cli.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/kicad_utils.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/kipy_reload.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/net_settings.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/path_validator.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pattern_recognition.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pcb_design_rules.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/pcb_sexp_utils.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/schematic_sexp_utils.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/secure_subprocess.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/skip_compat.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/symbol_database.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/symbol_extractor.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/symbol_index_manager.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/symbol_index_reader.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/temp_dir_manager.py +0 -0
- {kcaa-0.2.0 → kcaa-0.2.2}/kcaa/utils/version_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kcaa
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Model Context Protocol (MCP) server for KiCad electronic design automation (EDA) files
|
|
5
5
|
Project-URL: Homepage, https://github.com/paul356/KiCad-AI-Assistant
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/paul356/KiCad-AI-Assistant/issues
|
|
@@ -24,6 +24,7 @@ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA
|
|
|
24
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
25
|
Classifier: Typing :: Typed
|
|
26
26
|
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: certifi>=2026.2.25
|
|
27
28
|
Requires-Dist: defusedxml>=0.7.0
|
|
28
29
|
Requires-Dist: fastmcp>=2.0.0
|
|
29
30
|
Requires-Dist: kicad-python>=0.7.1
|
|
@@ -298,7 +299,7 @@ kcaa
|
|
|
298
299
|
|------|-------------|
|
|
299
300
|
| `get_board_info` | Get basic PCB board information |
|
|
300
301
|
| `list_footprints` | List all footprints placed on the board |
|
|
301
|
-
| `get_footprint` | Get details of a
|
|
302
|
+
| `get_footprint` | Get details of a placed footprint (pads, properties, Edge.Cuts geometry) |
|
|
302
303
|
| `get_footprint_bbox` | Get the courtyard bounding box of a footprint |
|
|
303
304
|
| `get_board_bounding_box` | Get the union bounding box of all footprints |
|
|
304
305
|
| `list_nets` | List all nets on the board |
|
|
@@ -260,7 +260,7 @@ kcaa
|
|
|
260
260
|
|------|-------------|
|
|
261
261
|
| `get_board_info` | Get basic PCB board information |
|
|
262
262
|
| `list_footprints` | List all footprints placed on the board |
|
|
263
|
-
| `get_footprint` | Get details of a
|
|
263
|
+
| `get_footprint` | Get details of a placed footprint (pads, properties, Edge.Cuts geometry) |
|
|
264
264
|
| `get_footprint_bbox` | Get the courtyard bounding box of a footprint |
|
|
265
265
|
| `get_board_bounding_box` | Get the union bounding box of all footprints |
|
|
266
266
|
| `list_nets` | List all nets on the board |
|
|
@@ -7,7 +7,7 @@ import os
|
|
|
7
7
|
from fastmcp import FastMCP
|
|
8
8
|
|
|
9
9
|
from kcaa.utils.file_utils import get_project_files
|
|
10
|
-
from kcaa.utils.netlist_parser import analyze_netlist, extract_netlist
|
|
10
|
+
from kcaa.utils.netlist_parser import analyze_netlist, extract_netlist, iter_component_pins
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def register_netlist_resources(mcp: FastMCP) -> None:
|
|
@@ -210,12 +210,16 @@ def register_netlist_resources(mcp: FastMCP) -> None:
|
|
|
210
210
|
|
|
211
211
|
report += "\n"
|
|
212
212
|
|
|
213
|
-
# Pins section
|
|
214
|
-
|
|
213
|
+
# Pins section — pins are nested per unit (issue #89); list them flat,
|
|
214
|
+
# tagging the unit only for multi-unit components.
|
|
215
|
+
units = component_info.get("units") or {}
|
|
216
|
+
if units:
|
|
217
|
+
multi_unit = len(units) > 1
|
|
215
218
|
report += "## Pins\n\n"
|
|
216
219
|
|
|
217
|
-
for pin in component_info
|
|
218
|
-
|
|
220
|
+
for unit, pin in iter_component_pins(component_info):
|
|
221
|
+
suffix = f" (unit {unit})" if multi_unit else ""
|
|
222
|
+
report += f"- **Pin {pin['num']}{suffix}**: {pin['name']}\n"
|
|
219
223
|
|
|
220
224
|
report += "\n"
|
|
221
225
|
|
|
@@ -56,6 +56,14 @@ _DIRECTIONS = [
|
|
|
56
56
|
_CARD_COST = 1.0
|
|
57
57
|
_DIAG_COST = math.sqrt(2.0)
|
|
58
58
|
|
|
59
|
+
# Turn penalty (mm-equivalent) added when the path changes direction.
|
|
60
|
+
# 0.3 mm ≈ 3 cells at 0.1 mm resolution — discourages zigzag without
|
|
61
|
+
# blocking legitimate detours. Set to 0 to disable (pure shortest path).
|
|
62
|
+
_TURN_PENALTY = 0.3
|
|
63
|
+
|
|
64
|
+
# Number of movement directions (8-directional grid).
|
|
65
|
+
_N_DIRS = 8
|
|
66
|
+
|
|
59
67
|
# Default via cost (mm added to the path for each via transition).
|
|
60
68
|
_VIA_COST = 2.0
|
|
61
69
|
|
|
@@ -231,7 +239,9 @@ def _octile_dist(gx: int, gy: int, ex: int, ey: int) -> float:
|
|
|
231
239
|
"""Octile distance heuristic (admissible for 8-direction movement)."""
|
|
232
240
|
dx = abs(gx - ex)
|
|
233
241
|
dy = abs(gy - ey)
|
|
234
|
-
|
|
242
|
+
if dx > dy:
|
|
243
|
+
return _DIAG_COST * dy + (dx - dy)
|
|
244
|
+
return _DIAG_COST * dx + (dy - dx)
|
|
235
245
|
|
|
236
246
|
|
|
237
247
|
def grid_a_star(
|
|
@@ -243,6 +253,7 @@ def grid_a_star(
|
|
|
243
253
|
via_from: dict[int, set[int]] | None = None,
|
|
244
254
|
via_cost: float = _VIA_COST,
|
|
245
255
|
via_forbidden_zones: list | None = None,
|
|
256
|
+
turn_penalty: float = _TURN_PENALTY,
|
|
246
257
|
) -> AStarResult:
|
|
247
258
|
"""Run A* on one or more walkability grids.
|
|
248
259
|
|
|
@@ -265,6 +276,8 @@ def grid_a_star(
|
|
|
265
276
|
via_cost: Distance-equivalent cost per via transition (mm).
|
|
266
277
|
via_forbidden_zones: Optional list of ``shapely`` Polygon objects
|
|
267
278
|
where vias are forbidden (e.g. start/end pad AABBs).
|
|
279
|
+
turn_penalty: Distance-equivalent cost added when the path
|
|
280
|
+
changes direction. 0 disables (pure shortest path).
|
|
268
281
|
|
|
269
282
|
Returns:
|
|
270
283
|
:class:`AStarResult` with ``path`` as ``list[(x, y, layer_idx)]``,
|
|
@@ -288,13 +301,21 @@ def grid_a_star(
|
|
|
288
301
|
if not grids[end_layer_idx].is_free(ex, ey):
|
|
289
302
|
return AStarResult(path=None)
|
|
290
303
|
|
|
291
|
-
|
|
292
|
-
|
|
304
|
+
# State encoding: (cell, layer, arrival_direction).
|
|
305
|
+
# arrival_direction is 0-7 (index into _DIRECTIONS) or _N_DIRS for
|
|
306
|
+
# the start node (no previous move). This lets A* distinguish
|
|
307
|
+
# reaching a cell from different directions and apply a turn penalty
|
|
308
|
+
# when the direction changes.
|
|
309
|
+
_NO_DIR = _N_DIRS # sentinel for "no previous direction"
|
|
310
|
+
|
|
311
|
+
def _encode(gx: int, gy: int, li: int, di: int) -> int:
|
|
312
|
+
return ((gy * W + gx) * n_layers + li) * (_N_DIRS + 1) + di
|
|
293
313
|
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
# The goal is any state at (ex, ey, end_layer) regardless of direction.
|
|
315
|
+
# We check for the cell-level match inside the loop.
|
|
316
|
+
start_id = _encode(sx, sy, start_layer_idx, _NO_DIR)
|
|
296
317
|
|
|
297
|
-
g_score = {start_id: 0.0}
|
|
318
|
+
g_score: dict[int, float] = {start_id: 0.0}
|
|
298
319
|
parent: dict[int, int | None] = {start_id: None}
|
|
299
320
|
open_heap = [(_octile_dist(sx, sy, ex, ey), start_id)]
|
|
300
321
|
closed: set[int] = set()
|
|
@@ -307,45 +328,55 @@ def grid_a_star(
|
|
|
307
328
|
closed.add(cur_id)
|
|
308
329
|
visited += 1
|
|
309
330
|
|
|
310
|
-
|
|
331
|
+
# Decode state.
|
|
332
|
+
di = cur_id % (_N_DIRS + 1)
|
|
333
|
+
rest = cur_id // (_N_DIRS + 1)
|
|
334
|
+
li = rest % n_layers
|
|
335
|
+
rest2 = rest // n_layers
|
|
336
|
+
cy = rest2 // W
|
|
337
|
+
cx = rest2 % W
|
|
338
|
+
cur_g = g_score[cur_id]
|
|
339
|
+
cur_grid = grids[li]
|
|
340
|
+
|
|
341
|
+
# Goal check: any direction at the target cell on the target layer.
|
|
342
|
+
if cx == ex and cy == ey and li == end_layer_idx:
|
|
311
343
|
path: list[tuple[float, float, int]] = []
|
|
312
|
-
nid = cur_id
|
|
344
|
+
nid: int | None = cur_id
|
|
313
345
|
while nid is not None:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
346
|
+
rest_n = nid // (_N_DIRS + 1)
|
|
347
|
+
li_n = rest_n % n_layers
|
|
348
|
+
rest2_n = rest_n // n_layers
|
|
349
|
+
gy_p = rest2_n // W
|
|
350
|
+
gx_p = rest2_n % W
|
|
318
351
|
wx, wy = ref.to_world(gx_p, gy_p)
|
|
319
|
-
path.append((wx, wy,
|
|
352
|
+
path.append((wx, wy, li_n))
|
|
320
353
|
nid = parent[nid]
|
|
321
354
|
path.reverse()
|
|
322
355
|
|
|
323
|
-
# Compute length from world-coordinate distance (ignore
|
|
324
|
-
# layer — via cost is already baked into g_score).
|
|
325
356
|
total_len = 0.0
|
|
326
357
|
for i in range(1, len(path)):
|
|
327
358
|
px, py, _ = path[i - 1]
|
|
328
|
-
|
|
329
|
-
total_len += math.hypot(
|
|
359
|
+
cx_p, cy_p, _ = path[i]
|
|
360
|
+
total_len += math.hypot(cx_p - px, cy_p - py)
|
|
330
361
|
return AStarResult(path=path, cells_visited=visited, path_length_mm=total_len)
|
|
331
362
|
|
|
332
|
-
li = cur_id % n_layers
|
|
333
|
-
rest = cur_id // n_layers
|
|
334
|
-
cy = rest // W
|
|
335
|
-
cx = rest % W
|
|
336
|
-
cur_g = g_score[cur_id]
|
|
337
|
-
cur_grid = grids[li]
|
|
338
|
-
|
|
339
363
|
# ---- Same-layer 8-dir moves ----
|
|
340
|
-
for dx, dy, is_diag in _DIRECTIONS:
|
|
364
|
+
for dir_idx, (dx, dy, is_diag) in enumerate(_DIRECTIONS):
|
|
341
365
|
nx, ny = cx + dx, cy + dy
|
|
342
366
|
if not cur_grid.is_free(nx, ny):
|
|
343
367
|
continue
|
|
344
|
-
nid = _encode(nx, ny, li)
|
|
368
|
+
nid = _encode(nx, ny, li, dir_idx)
|
|
345
369
|
if nid in closed:
|
|
346
370
|
continue
|
|
347
371
|
step = _DIAG_COST if is_diag else _CARD_COST
|
|
348
|
-
|
|
372
|
+
# Turn penalty: if this move's direction differs from the
|
|
373
|
+
# arrival direction (and we have a previous direction), add
|
|
374
|
+
# the penalty. 45° changes (cardinal→diagonal) cost the
|
|
375
|
+
# same as 90° changes — both are one turn.
|
|
376
|
+
tp = 0.0
|
|
377
|
+
if turn_penalty > 0 and di != _NO_DIR and di != dir_idx:
|
|
378
|
+
tp = turn_penalty
|
|
379
|
+
tentative = cur_g + step + tp
|
|
349
380
|
if tentative < g_score.get(nid, float("inf")):
|
|
350
381
|
g_score[nid] = tentative
|
|
351
382
|
heapq.heappush(
|
|
@@ -364,7 +395,8 @@ def grid_a_star(
|
|
|
364
395
|
wx, wy = ref.to_world(cx, cy)
|
|
365
396
|
if any(z.contains(Point(wx, wy)) for z in via_forbidden_zones):
|
|
366
397
|
continue
|
|
367
|
-
|
|
398
|
+
# Via resets direction (layer change is not a "turn").
|
|
399
|
+
nid = _encode(cx, cy, tgt_li, _NO_DIR)
|
|
368
400
|
if nid in closed:
|
|
369
401
|
continue
|
|
370
402
|
tentative = cur_g + via_cost
|
|
@@ -422,6 +454,7 @@ def hierarchical_a_star(
|
|
|
422
454
|
end_world: tuple[float, float],
|
|
423
455
|
fine_resolution: float = GRID_RESOLUTION,
|
|
424
456
|
route_bbox: tuple[float, float, float, float] | None = None,
|
|
457
|
+
turn_penalty: float = _TURN_PENALTY,
|
|
425
458
|
) -> AStarResult:
|
|
426
459
|
"""Run hierarchical A* with auto-detection of single-pass vs two-pass.
|
|
427
460
|
|
|
@@ -457,11 +490,11 @@ def hierarchical_a_star(
|
|
|
457
490
|
|
|
458
491
|
if fine_cells < _SINGLE_PASS_THRESHOLD:
|
|
459
492
|
grid = build_grid_map(obstacles, bbox, resolution=fine_resolution)
|
|
460
|
-
return grid_a_star([grid], start_world, end_world)
|
|
493
|
+
return grid_a_star([grid], start_world, end_world, turn_penalty=turn_penalty)
|
|
461
494
|
|
|
462
495
|
coarse_res = fine_resolution * COARSE_FACTOR
|
|
463
496
|
coarse_grid = build_grid_map(obstacles, bbox, resolution=coarse_res)
|
|
464
|
-
coarse_result = grid_a_star([coarse_grid], start_world, end_world)
|
|
497
|
+
coarse_result = grid_a_star([coarse_grid], start_world, end_world, turn_penalty=turn_penalty)
|
|
465
498
|
if coarse_result.path is None:
|
|
466
499
|
return AStarResult(path=None, cells_visited=coarse_result.cells_visited)
|
|
467
500
|
|
|
@@ -477,7 +510,7 @@ def hierarchical_a_star(
|
|
|
477
510
|
)
|
|
478
511
|
|
|
479
512
|
fine_grid = build_grid_map(obstacles, band, resolution=fine_resolution)
|
|
480
|
-
result = grid_a_star([fine_grid], start_world, end_world)
|
|
513
|
+
result = grid_a_star([fine_grid], start_world, end_world, turn_penalty=turn_penalty)
|
|
481
514
|
if result.path is not None:
|
|
482
515
|
result.cells_visited += coarse_result.cells_visited
|
|
483
516
|
else:
|
|
@@ -535,6 +568,7 @@ def multi_layer_a_star(
|
|
|
535
568
|
fine_resolution: float = GRID_RESOLUTION,
|
|
536
569
|
via_cost: float = _VIA_COST,
|
|
537
570
|
via_forbidden_zones: list | None = None,
|
|
571
|
+
turn_penalty: float = _TURN_PENALTY,
|
|
538
572
|
) -> MultiLayerAStarResult:
|
|
539
573
|
"""Run A* across multiple copper layers.
|
|
540
574
|
|
|
@@ -583,6 +617,7 @@ def multi_layer_a_star(
|
|
|
583
617
|
via_from=via_from or None,
|
|
584
618
|
via_cost=via_cost,
|
|
585
619
|
via_forbidden_zones=via_forbidden_zones,
|
|
620
|
+
turn_penalty=turn_penalty,
|
|
586
621
|
)
|
|
587
622
|
if result.path is None:
|
|
588
623
|
return MultiLayerAStarResult(path=None, cells_visited=result.cells_visited)
|
|
@@ -683,6 +718,13 @@ def _grid_line_clear(
|
|
|
683
718
|
return True
|
|
684
719
|
|
|
685
720
|
|
|
721
|
+
def _is_45_family(x0: float, y0: float, x1: float, y1: float) -> bool:
|
|
722
|
+
"""True when the segment (x0,y0)-(x1,y1) is axis-aligned or a 45° diagonal."""
|
|
723
|
+
dx = abs(x1 - x0)
|
|
724
|
+
dy = abs(y1 - y0)
|
|
725
|
+
return abs(dx) < 1e-6 or abs(dy) < 1e-6 or abs(dx - dy) < 1e-6
|
|
726
|
+
|
|
727
|
+
|
|
686
728
|
def shortcut_path(
|
|
687
729
|
pts: list[tuple[float, float]],
|
|
688
730
|
obstacles: list,
|
|
@@ -693,16 +735,11 @@ def shortcut_path(
|
|
|
693
735
|
|
|
694
736
|
Builds a walkability grid once, then for each point tries to skip
|
|
695
737
|
as many subsequent points as possible while keeping the line clear.
|
|
696
|
-
Only 0°/45°/90°/135° shortcuts are accepted.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
bbox: ``(min_x, min_y, max_x, max_y)`` of the route area.
|
|
702
|
-
resolution: Grid cell size in mm.
|
|
703
|
-
|
|
704
|
-
Returns:
|
|
705
|
-
Simplified polyline with redundant points removed.
|
|
738
|
+
Only 0°/45°/90°/135° shortcuts are accepted. A shortcut that would
|
|
739
|
+
skip a direction change all the way through (producing a near-45°
|
|
740
|
+
long diagonal) is REJECTED: snapping that back to the 45° family
|
|
741
|
+
later would have to force a Manhattan corner (often a 90° turn), so
|
|
742
|
+
it is better to keep the original 45°+axis-aligned waypoints.
|
|
706
743
|
"""
|
|
707
744
|
if len(pts) <= 2:
|
|
708
745
|
return list(pts)
|
|
@@ -713,10 +750,13 @@ def shortcut_path(
|
|
|
713
750
|
while i < len(pts) - 1:
|
|
714
751
|
best_next = i + 1
|
|
715
752
|
for k in range(len(pts) - 1, i, -1):
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
753
|
+
if not _is_45_family(
|
|
754
|
+
result[-1][0],
|
|
755
|
+
result[-1][1],
|
|
756
|
+
pts[k][0],
|
|
757
|
+
pts[k][1],
|
|
758
|
+
):
|
|
759
|
+
continue
|
|
720
760
|
if _grid_line_clear(
|
|
721
761
|
grid,
|
|
722
762
|
result[-1][0],
|