kicad-sch-api 0.1.7__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- kicad_sch_api/__init__.py +6 -2
- kicad_sch_api/cli.py +119 -272
- kicad_sch_api/core/component_bounds.py +477 -0
- kicad_sch_api/core/components.py +22 -10
- kicad_sch_api/core/config.py +127 -0
- kicad_sch_api/core/formatter.py +183 -23
- kicad_sch_api/core/geometry.py +111 -0
- kicad_sch_api/core/ic_manager.py +43 -37
- kicad_sch_api/core/junctions.py +17 -22
- kicad_sch_api/core/manhattan_routing.py +430 -0
- kicad_sch_api/core/parser.py +495 -196
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +630 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +9 -4
- kicad_sch_api/core/wire_routing.py +380 -0
- kicad_sch_api/core/wires.py +29 -25
- kicad_sch_api/discovery/__init__.py +1 -1
- kicad_sch_api/discovery/search_index.py +142 -107
- kicad_sch_api/library/cache.py +70 -62
- kicad_sch_api-0.2.1.dist-info/METADATA +483 -0
- kicad_sch_api-0.2.1.dist-info/RECORD +31 -0
- {kicad_sch_api-0.1.7.dist-info → kicad_sch_api-0.2.1.dist-info}/entry_points.txt +0 -1
- kicad_sch_api/mcp/__init__.py +0 -7
- kicad_sch_api/mcp/server.py +0 -1511
- kicad_sch_api-0.1.7.dist-info/METADATA +0 -322
- kicad_sch_api-0.1.7.dist-info/RECORD +0 -26
- {kicad_sch_api-0.1.7.dist-info → kicad_sch_api-0.2.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.1.7.dist-info → kicad_sch_api-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.1.7.dist-info → kicad_sch_api-0.2.1.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -47,8 +47,10 @@ class ExactFormatter:
|
|
|
47
47
|
def _initialize_kicad_rules(self):
|
|
48
48
|
"""Initialize formatting rules that match KiCAD's output exactly."""
|
|
49
49
|
|
|
50
|
-
#
|
|
51
|
-
self.rules["kicad_sch"] = FormatRule(
|
|
50
|
+
# Root element - custom formatting for specific test cases
|
|
51
|
+
self.rules["kicad_sch"] = FormatRule(
|
|
52
|
+
inline=False, indent_level=0, custom_handler=self._format_kicad_sch
|
|
53
|
+
)
|
|
52
54
|
self.rules["version"] = FormatRule(inline=True)
|
|
53
55
|
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
54
56
|
self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
|
|
@@ -83,10 +85,16 @@ class ExactFormatter:
|
|
|
83
85
|
inline=False, quote_indices={1, 2}, custom_handler=self._format_property
|
|
84
86
|
)
|
|
85
87
|
|
|
86
|
-
# Pins and connections
|
|
87
|
-
self.rules["pin"] = FormatRule(
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
# Pins and connections
|
|
89
|
+
self.rules["pin"] = FormatRule(
|
|
90
|
+
inline=False, quote_indices=set(), custom_handler=self._format_pin
|
|
91
|
+
)
|
|
92
|
+
self.rules["number"] = FormatRule(
|
|
93
|
+
inline=False, quote_indices={1}
|
|
94
|
+
) # Pin numbers should be quoted
|
|
95
|
+
self.rules["name"] = FormatRule(
|
|
96
|
+
inline=False, quote_indices={1}
|
|
97
|
+
) # Pin names should be quoted
|
|
90
98
|
self.rules["instances"] = FormatRule(inline=False)
|
|
91
99
|
self.rules["project"] = FormatRule(inline=False, quote_indices={1})
|
|
92
100
|
self.rules["path"] = FormatRule(inline=False, quote_indices={1})
|
|
@@ -94,7 +102,7 @@ class ExactFormatter:
|
|
|
94
102
|
|
|
95
103
|
# Wire elements
|
|
96
104
|
self.rules["wire"] = FormatRule(inline=False)
|
|
97
|
-
self.rules["pts"] = FormatRule(inline=False)
|
|
105
|
+
self.rules["pts"] = FormatRule(inline=False, custom_handler=self._format_pts)
|
|
98
106
|
self.rules["xy"] = FormatRule(inline=True)
|
|
99
107
|
self.rules["stroke"] = FormatRule(inline=False)
|
|
100
108
|
self.rules["width"] = FormatRule(inline=True)
|
|
@@ -117,6 +125,15 @@ class ExactFormatter:
|
|
|
117
125
|
self.rules["justify"] = FormatRule(inline=True)
|
|
118
126
|
self.rules["hide"] = FormatRule(inline=True)
|
|
119
127
|
|
|
128
|
+
# Graphical elements
|
|
129
|
+
self.rules["rectangle"] = FormatRule(inline=False)
|
|
130
|
+
self.rules["polyline"] = FormatRule(inline=False)
|
|
131
|
+
self.rules["graphics"] = FormatRule(inline=False)
|
|
132
|
+
self.rules["start"] = FormatRule(inline=True)
|
|
133
|
+
self.rules["end"] = FormatRule(inline=True)
|
|
134
|
+
self.rules["fill"] = FormatRule(inline=False)
|
|
135
|
+
self.rules["color"] = FormatRule(inline=True)
|
|
136
|
+
|
|
120
137
|
# Sheet instances and metadata
|
|
121
138
|
self.rules["sheet_instances"] = FormatRule(inline=False)
|
|
122
139
|
self.rules["symbol_instances"] = FormatRule(inline=False)
|
|
@@ -133,7 +150,11 @@ class ExactFormatter:
|
|
|
133
150
|
Returns:
|
|
134
151
|
Formatted string matching KiCAD's output exactly
|
|
135
152
|
"""
|
|
136
|
-
|
|
153
|
+
result = self._format_element(data, 0)
|
|
154
|
+
# Ensure file ends with newline
|
|
155
|
+
if not result.endswith("\n"):
|
|
156
|
+
result += "\n"
|
|
157
|
+
return result
|
|
137
158
|
|
|
138
159
|
def format_preserving_write(self, new_data: Any, original_content: str) -> str:
|
|
139
160
|
"""
|
|
@@ -164,9 +185,33 @@ class ExactFormatter:
|
|
|
164
185
|
if self._needs_quoting(element):
|
|
165
186
|
return f'"{element}"'
|
|
166
187
|
return element
|
|
188
|
+
elif isinstance(element, float):
|
|
189
|
+
# Custom float formatting for KiCAD compatibility
|
|
190
|
+
return self._format_float(element)
|
|
167
191
|
else:
|
|
168
192
|
return str(element)
|
|
169
193
|
|
|
194
|
+
def _format_float(self, value: float) -> str:
|
|
195
|
+
"""Format float values to match KiCAD's precision and format."""
|
|
196
|
+
# Handle special case for zero values in color alpha
|
|
197
|
+
if abs(value) < 1e-10: # Essentially zero
|
|
198
|
+
return "0.0000"
|
|
199
|
+
|
|
200
|
+
# For other values, use minimal precision (remove trailing zeros)
|
|
201
|
+
if value == int(value):
|
|
202
|
+
return str(int(value))
|
|
203
|
+
|
|
204
|
+
# Round to reasonable precision and remove trailing zeros
|
|
205
|
+
rounded = round(value, 6) # Use 6 decimal places for precision
|
|
206
|
+
if rounded == int(rounded):
|
|
207
|
+
return str(int(rounded))
|
|
208
|
+
|
|
209
|
+
# Format and remove trailing zeros, but don't remove the decimal point for values like 0.254
|
|
210
|
+
formatted = f"{rounded:.6f}".rstrip("0")
|
|
211
|
+
if formatted.endswith("."):
|
|
212
|
+
formatted += "0" # Keep at least one decimal place
|
|
213
|
+
return formatted
|
|
214
|
+
|
|
170
215
|
def _format_list(self, lst: List[Any], indent_level: int) -> str:
|
|
171
216
|
"""Format a list (S-expression)."""
|
|
172
217
|
if not lst:
|
|
@@ -216,7 +261,15 @@ class ExactFormatter:
|
|
|
216
261
|
return self._format_property(lst, indent_level)
|
|
217
262
|
elif tag == "pin":
|
|
218
263
|
return self._format_pin(lst, indent_level)
|
|
219
|
-
elif tag in (
|
|
264
|
+
elif tag in (
|
|
265
|
+
"symbol",
|
|
266
|
+
"wire",
|
|
267
|
+
"junction",
|
|
268
|
+
"label",
|
|
269
|
+
"hierarchical_label",
|
|
270
|
+
"polyline",
|
|
271
|
+
"rectangle",
|
|
272
|
+
):
|
|
220
273
|
return self._format_component_like(lst, indent_level, rule)
|
|
221
274
|
else:
|
|
222
275
|
return self._format_generic_multiline(lst, indent_level, rule)
|
|
@@ -248,27 +301,57 @@ class ExactFormatter:
|
|
|
248
301
|
"""Format pin elements with context-aware quoting."""
|
|
249
302
|
if len(lst) < 2:
|
|
250
303
|
return self._format_inline(lst, FormatRule())
|
|
251
|
-
|
|
304
|
+
|
|
252
305
|
indent = "\t" * indent_level
|
|
253
306
|
next_indent = "\t" * (indent_level + 1)
|
|
254
|
-
|
|
255
|
-
# Check if this is a lib_symbols pin (passive/line) or
|
|
256
|
-
if
|
|
307
|
+
|
|
308
|
+
# Check if this is a lib_symbols pin (passive/line) or sheet pin ("NET1" input)
|
|
309
|
+
if (
|
|
310
|
+
len(lst) >= 3
|
|
311
|
+
and isinstance(lst[2], sexpdata.Symbol)
|
|
312
|
+
and str(lst[1])
|
|
313
|
+
in [
|
|
314
|
+
"passive",
|
|
315
|
+
"power_in",
|
|
316
|
+
"power_out",
|
|
317
|
+
"input",
|
|
318
|
+
"output",
|
|
319
|
+
"bidirectional",
|
|
320
|
+
"tri_state",
|
|
321
|
+
"unspecified",
|
|
322
|
+
]
|
|
323
|
+
):
|
|
257
324
|
# lib_symbols context: (pin passive line ...)
|
|
258
325
|
result = f"({lst[0]} {lst[1]} {lst[2]}"
|
|
259
326
|
start_index = 3
|
|
327
|
+
|
|
328
|
+
# Add remaining elements on separate lines with proper indentation
|
|
329
|
+
for element in lst[start_index:]:
|
|
330
|
+
if isinstance(element, list):
|
|
331
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
332
|
+
|
|
333
|
+
result += f"\n{indent})"
|
|
334
|
+
return result
|
|
260
335
|
else:
|
|
261
|
-
# component context: (pin "1" ...)
|
|
262
|
-
|
|
336
|
+
# sheet pin or component pin context: (pin "NET1" input) or (pin "1" ...)
|
|
337
|
+
# Pin name should always be quoted
|
|
338
|
+
pin_name = str(lst[1])
|
|
339
|
+
result = f'({lst[0]} "{pin_name}"'
|
|
263
340
|
start_index = 2
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
341
|
+
|
|
342
|
+
# Add remaining elements (type and others)
|
|
343
|
+
for i, element in enumerate(lst[start_index:], start_index):
|
|
344
|
+
if isinstance(element, list):
|
|
345
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
346
|
+
else:
|
|
347
|
+
# Convert pin type to symbol if it's a string
|
|
348
|
+
if i == 2 and isinstance(element, str):
|
|
349
|
+
result += f" {element}" # Pin type as bare symbol
|
|
350
|
+
else:
|
|
351
|
+
result += f" {self._format_element(element, 0)}"
|
|
352
|
+
|
|
353
|
+
result += f"\n{indent})"
|
|
354
|
+
return result
|
|
272
355
|
|
|
273
356
|
def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
274
357
|
"""Format component-like elements (symbol, wire, etc.)."""
|
|
@@ -340,6 +423,83 @@ class ExactFormatter:
|
|
|
340
423
|
special_chars = "()[]{}#"
|
|
341
424
|
return any(c in text for c in special_chars)
|
|
342
425
|
|
|
426
|
+
def _format_kicad_sch(self, lst: List[Any], indent_level: int) -> str:
|
|
427
|
+
"""
|
|
428
|
+
Custom formatter for kicad_sch root element to handle blank schematic format.
|
|
429
|
+
|
|
430
|
+
Detects blank schematics and formats them exactly like KiCAD reference files.
|
|
431
|
+
"""
|
|
432
|
+
# Check if this is a blank schematic (no components, no UUID, minimal elements)
|
|
433
|
+
has_components = any(
|
|
434
|
+
isinstance(item, list)
|
|
435
|
+
and len(item) > 0
|
|
436
|
+
and str(item[0])
|
|
437
|
+
in ["symbol", "wire", "junction", "text", "sheet", "polyline", "rectangle", "graphics"]
|
|
438
|
+
for item in lst[1:]
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
has_uuid = any(
|
|
442
|
+
isinstance(item, list) and len(item) >= 2 and str(item[0]) == "uuid" for item in lst[1:]
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# If no components and no UUID, format as blank schematic
|
|
446
|
+
if not has_components and not has_uuid:
|
|
447
|
+
header_parts = [str(lst[0])] # kicad_sch
|
|
448
|
+
body_parts = []
|
|
449
|
+
|
|
450
|
+
for item in lst[1:]:
|
|
451
|
+
if isinstance(item, list) and len(item) >= 1:
|
|
452
|
+
tag = str(item[0])
|
|
453
|
+
if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
|
|
454
|
+
if tag in ["generator", "generator_version"]:
|
|
455
|
+
header_parts.append(f'({tag} "{item[1]}")')
|
|
456
|
+
else:
|
|
457
|
+
header_parts.append(f"({tag} {item[1]})")
|
|
458
|
+
else:
|
|
459
|
+
body_parts.append(item)
|
|
460
|
+
|
|
461
|
+
# Build single-line header + body format
|
|
462
|
+
result = f"({' '.join(header_parts)}"
|
|
463
|
+
for item in body_parts:
|
|
464
|
+
if isinstance(item, list) and len(item) == 1:
|
|
465
|
+
result += f"\n ({item[0]})"
|
|
466
|
+
else:
|
|
467
|
+
result += f"\n {self._format_element(item, 1)}"
|
|
468
|
+
result += "\n)\n"
|
|
469
|
+
return result
|
|
470
|
+
|
|
471
|
+
# For normal schematics, use standard multiline formatting
|
|
472
|
+
return self._format_multiline(lst, indent_level, FormatRule())
|
|
473
|
+
|
|
474
|
+
def _format_pts(self, lst: List[Any], indent_level: int) -> str:
|
|
475
|
+
"""Format pts elements with inline xy coordinates on indented line."""
|
|
476
|
+
if len(lst) < 2:
|
|
477
|
+
return self._format_inline(lst, FormatRule())
|
|
478
|
+
|
|
479
|
+
indent = "\t" * indent_level
|
|
480
|
+
next_indent = "\t" * (indent_level + 1)
|
|
481
|
+
|
|
482
|
+
# Format as:
|
|
483
|
+
# (pts
|
|
484
|
+
# (xy x1 y1) (xy x2 y2)
|
|
485
|
+
# )
|
|
486
|
+
result = f"({lst[0]}"
|
|
487
|
+
|
|
488
|
+
# Add xy elements on same indented line
|
|
489
|
+
if len(lst) > 1:
|
|
490
|
+
xy_elements = []
|
|
491
|
+
for element in lst[1:]:
|
|
492
|
+
if isinstance(element, list) and len(element) >= 3 and str(element[0]) == "xy":
|
|
493
|
+
xy_elements.append(self._format_element(element, 0))
|
|
494
|
+
else:
|
|
495
|
+
xy_elements.append(self._format_element(element, 0))
|
|
496
|
+
|
|
497
|
+
if xy_elements:
|
|
498
|
+
result += f"\n{next_indent}{' '.join(xy_elements)}"
|
|
499
|
+
|
|
500
|
+
result += f"\n{indent})"
|
|
501
|
+
return result
|
|
502
|
+
|
|
343
503
|
|
|
344
504
|
class CompactFormatter(ExactFormatter):
|
|
345
505
|
"""Compact formatter for minimal output size."""
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometry utilities for KiCAD schematic manipulation.
|
|
3
|
+
|
|
4
|
+
Provides coordinate transformation, pin positioning, and geometric calculations
|
|
5
|
+
migrated from circuit-synth for improved maintainability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
from typing import Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from .types import Point
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def snap_to_grid(position: Tuple[float, float], grid_size: float = 2.54) -> Tuple[float, float]:
|
|
18
|
+
"""
|
|
19
|
+
Snap a position to the nearest grid point.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
position: (x, y) coordinate
|
|
23
|
+
grid_size: Grid size in mm (default 2.54mm = 0.1 inch)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Grid-aligned (x, y) coordinate
|
|
27
|
+
"""
|
|
28
|
+
x, y = position
|
|
29
|
+
aligned_x = round(x / grid_size) * grid_size
|
|
30
|
+
aligned_y = round(y / grid_size) * grid_size
|
|
31
|
+
return (aligned_x, aligned_y)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def points_equal(p1: Point, p2: Point, tolerance: float = 0.01) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Check if two points are equal within tolerance.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
p1: First point
|
|
40
|
+
p2: Second point
|
|
41
|
+
tolerance: Distance tolerance
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if points are equal within tolerance
|
|
45
|
+
"""
|
|
46
|
+
return abs(p1.x - p2.x) < tolerance and abs(p1.y - p2.y) < tolerance
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def distance_between_points(p1: Tuple[float, float], p2: Tuple[float, float]) -> float:
|
|
50
|
+
"""
|
|
51
|
+
Calculate distance between two points.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
p1: First point (x, y)
|
|
55
|
+
p2: Second point (x, y)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Distance between points
|
|
59
|
+
"""
|
|
60
|
+
return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def apply_transformation(
|
|
64
|
+
point: Tuple[float, float],
|
|
65
|
+
origin: Point,
|
|
66
|
+
rotation: float,
|
|
67
|
+
mirror: Optional[str] = None,
|
|
68
|
+
) -> Tuple[float, float]:
|
|
69
|
+
"""
|
|
70
|
+
Apply rotation and mirroring transformation to a point.
|
|
71
|
+
|
|
72
|
+
Migrated from circuit-synth for accurate pin position calculation.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
point: Point to transform (x, y) relative to origin
|
|
76
|
+
origin: Component origin point
|
|
77
|
+
rotation: Rotation in degrees (0, 90, 180, 270)
|
|
78
|
+
mirror: Mirror axis ("x" or "y" or None)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Transformed absolute position (x, y)
|
|
82
|
+
"""
|
|
83
|
+
x, y = point
|
|
84
|
+
|
|
85
|
+
logger.debug(f"Transforming point ({x}, {y}) with rotation={rotation}°, mirror={mirror}")
|
|
86
|
+
|
|
87
|
+
# Apply mirroring first
|
|
88
|
+
if mirror == "x":
|
|
89
|
+
x = -x
|
|
90
|
+
logger.debug(f"After X mirror: ({x}, {y})")
|
|
91
|
+
elif mirror == "y":
|
|
92
|
+
y = -y
|
|
93
|
+
logger.debug(f"After Y mirror: ({x}, {y})")
|
|
94
|
+
|
|
95
|
+
# Apply rotation
|
|
96
|
+
if rotation == 90:
|
|
97
|
+
x, y = -y, x
|
|
98
|
+
logger.debug(f"After 90° rotation: ({x}, {y})")
|
|
99
|
+
elif rotation == 180:
|
|
100
|
+
x, y = -x, -y
|
|
101
|
+
logger.debug(f"After 180° rotation: ({x}, {y})")
|
|
102
|
+
elif rotation == 270:
|
|
103
|
+
x, y = y, -x
|
|
104
|
+
logger.debug(f"After 270° rotation: ({x}, {y})")
|
|
105
|
+
|
|
106
|
+
# Translate to absolute position
|
|
107
|
+
final_x = origin.x + x
|
|
108
|
+
final_y = origin.y + y
|
|
109
|
+
|
|
110
|
+
logger.debug(f"Final absolute position: ({final_x}, {final_y})")
|
|
111
|
+
return (final_x, final_y)
|
kicad_sch_api/core/ic_manager.py
CHANGED
|
@@ -9,8 +9,8 @@ import logging
|
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
-
from .types import Point, SchematicSymbol
|
|
13
12
|
from ..library.cache import get_symbol_cache
|
|
13
|
+
from .types import Point, SchematicSymbol
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
class ICManager:
|
|
19
19
|
"""
|
|
20
20
|
Manager for multi-unit IC components with auto-layout capabilities.
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Features:
|
|
23
23
|
- Automatic unit detection and placement
|
|
24
24
|
- Smart layout algorithms (vertical, grid, functional)
|
|
@@ -26,11 +26,16 @@ class ICManager:
|
|
|
26
26
|
- Professional spacing and alignment
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(
|
|
30
|
-
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
lib_id: str,
|
|
32
|
+
reference_prefix: str,
|
|
33
|
+
position: Point,
|
|
34
|
+
component_collection: "ComponentCollection",
|
|
35
|
+
):
|
|
31
36
|
"""
|
|
32
37
|
Initialize IC manager.
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
Args:
|
|
35
40
|
lib_id: IC library ID (e.g., "74xx:7400")
|
|
36
41
|
reference_prefix: Base reference (e.g., "U1" → U1A, U1B, etc.)
|
|
@@ -43,30 +48,30 @@ class ICManager:
|
|
|
43
48
|
self._collection = component_collection
|
|
44
49
|
self._units: Dict[int, SchematicSymbol] = {}
|
|
45
50
|
self._unit_positions: Dict[int, Point] = {}
|
|
46
|
-
|
|
51
|
+
|
|
47
52
|
# Detect available units from symbol library
|
|
48
53
|
self._detect_available_units()
|
|
49
|
-
|
|
54
|
+
|
|
50
55
|
# Auto-place all units with default layout
|
|
51
56
|
self._auto_layout_units()
|
|
52
|
-
|
|
57
|
+
|
|
53
58
|
logger.debug(f"ICManager initialized for {lib_id} with {len(self._units)} units")
|
|
54
59
|
|
|
55
60
|
def _detect_available_units(self):
|
|
56
61
|
"""Detect available units from symbol library definition."""
|
|
57
62
|
cache = get_symbol_cache()
|
|
58
63
|
symbol_def = cache.get_symbol(self.lib_id)
|
|
59
|
-
|
|
60
|
-
if not symbol_def or not hasattr(symbol_def,
|
|
64
|
+
|
|
65
|
+
if not symbol_def or not hasattr(symbol_def, "raw_kicad_data"):
|
|
61
66
|
logger.warning(f"Could not detect units for {self.lib_id}")
|
|
62
67
|
return
|
|
63
|
-
|
|
68
|
+
|
|
64
69
|
# Parse symbol data to find unit definitions
|
|
65
70
|
symbol_data = symbol_def.raw_kicad_data
|
|
66
71
|
if isinstance(symbol_data, list):
|
|
67
72
|
for item in symbol_data[1:]:
|
|
68
73
|
if isinstance(item, list) and len(item) >= 2:
|
|
69
|
-
if item[0] == getattr(item[0],
|
|
74
|
+
if item[0] == getattr(item[0], "value", str(item[0])) == "symbol":
|
|
70
75
|
unit_name = str(item[1]).strip('"')
|
|
71
76
|
# Extract unit number from name like "7400_1_1" → unit 1
|
|
72
77
|
unit_num = self._extract_unit_number(unit_name)
|
|
@@ -75,7 +80,7 @@ class ICManager:
|
|
|
75
80
|
|
|
76
81
|
def _extract_unit_number(self, unit_name: str) -> Optional[int]:
|
|
77
82
|
"""Extract unit number from symbol unit name like '7400_1_1' → 1."""
|
|
78
|
-
parts = unit_name.split(
|
|
83
|
+
parts = unit_name.split("_")
|
|
79
84
|
if len(parts) >= 3:
|
|
80
85
|
try:
|
|
81
86
|
return int(parts[-2]) # Second to last part is unit number
|
|
@@ -91,22 +96,21 @@ class ICManager:
|
|
|
91
96
|
def _place_default_units(self):
|
|
92
97
|
"""Place default units for common IC types."""
|
|
93
98
|
# For 74xx logic ICs, typically have units 1-4 (logic) + unit 5 (power)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
from .config import config
|
|
100
|
+
|
|
101
|
+
unit_spacing = config.grid.unit_spacing # Tight vertical spacing (0.5 inch in mm)
|
|
102
|
+
power_offset = config.grid.power_offset # Power unit offset (1 inch right)
|
|
103
|
+
|
|
97
104
|
# Place logic units (1-4) vertically in a tight column
|
|
98
105
|
for unit in range(1, 5):
|
|
99
|
-
unit_pos = Point(
|
|
100
|
-
self.base_position.x,
|
|
101
|
-
self.base_position.y + (unit - 1) * unit_spacing
|
|
102
|
-
)
|
|
106
|
+
unit_pos = Point(self.base_position.x, self.base_position.y + (unit - 1) * unit_spacing)
|
|
103
107
|
self._unit_positions[unit] = unit_pos
|
|
104
|
-
|
|
108
|
+
|
|
105
109
|
# Place power unit (5) to the right of logic units
|
|
106
110
|
if 5 not in self._unit_positions:
|
|
107
111
|
power_pos = Point(
|
|
108
112
|
self.base_position.x + power_offset[0],
|
|
109
|
-
self.base_position.y + unit_spacing # Align with second gate
|
|
113
|
+
self.base_position.y + unit_spacing, # Align with second gate
|
|
110
114
|
)
|
|
111
115
|
self._unit_positions[5] = power_pos
|
|
112
116
|
|
|
@@ -115,21 +119,21 @@ class ICManager:
|
|
|
115
119
|
def place_unit(self, unit: int, position: Union[Point, Tuple[float, float]]):
|
|
116
120
|
"""
|
|
117
121
|
Override the position of a specific unit.
|
|
118
|
-
|
|
122
|
+
|
|
119
123
|
Args:
|
|
120
124
|
unit: Unit number to place
|
|
121
125
|
position: New position for the unit
|
|
122
126
|
"""
|
|
123
127
|
if isinstance(position, tuple):
|
|
124
128
|
position = Point(position[0], position[1])
|
|
125
|
-
|
|
129
|
+
|
|
126
130
|
self._unit_positions[unit] = position
|
|
127
|
-
|
|
131
|
+
|
|
128
132
|
# If component already exists, update its position
|
|
129
133
|
if unit in self._units:
|
|
130
134
|
self._units[unit].position = position
|
|
131
135
|
self._collection._mark_modified()
|
|
132
|
-
|
|
136
|
+
|
|
133
137
|
logger.debug(f"Placed unit {unit} at {position}")
|
|
134
138
|
|
|
135
139
|
def get_unit_position(self, unit: int) -> Optional[Point]:
|
|
@@ -143,36 +147,38 @@ class ICManager:
|
|
|
143
147
|
def generate_components(self, **common_properties) -> List[SchematicSymbol]:
|
|
144
148
|
"""
|
|
145
149
|
Generate all component instances for this IC.
|
|
146
|
-
|
|
150
|
+
|
|
147
151
|
Args:
|
|
148
152
|
**common_properties: Properties to apply to all units
|
|
149
|
-
|
|
153
|
+
|
|
150
154
|
Returns:
|
|
151
155
|
List of component symbols for all units
|
|
152
156
|
"""
|
|
153
157
|
components = []
|
|
154
|
-
|
|
158
|
+
|
|
155
159
|
for unit, position in self._unit_positions.items():
|
|
156
160
|
# Generate unit reference (U1 → U1A, U1B, etc.)
|
|
157
161
|
if unit <= 4:
|
|
158
|
-
unit_ref =
|
|
162
|
+
unit_ref = (
|
|
163
|
+
f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1A, U1B, U1C, U1D
|
|
164
|
+
)
|
|
159
165
|
else:
|
|
160
166
|
unit_ref = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1E for power
|
|
161
|
-
|
|
167
|
+
|
|
162
168
|
component = SchematicSymbol(
|
|
163
169
|
uuid=str(uuid_module.uuid4()),
|
|
164
170
|
lib_id=self.lib_id,
|
|
165
171
|
position=position,
|
|
166
172
|
reference=unit_ref,
|
|
167
|
-
value=common_properties.get(
|
|
168
|
-
footprint=common_properties.get(
|
|
173
|
+
value=common_properties.get("value", self.lib_id.split(":")[-1]),
|
|
174
|
+
footprint=common_properties.get("footprint"),
|
|
169
175
|
unit=unit,
|
|
170
|
-
properties=common_properties.get(
|
|
176
|
+
properties=common_properties.get("properties", {}),
|
|
171
177
|
)
|
|
172
|
-
|
|
178
|
+
|
|
173
179
|
components.append(component)
|
|
174
180
|
self._units[unit] = component
|
|
175
|
-
|
|
181
|
+
|
|
176
182
|
logger.debug(f"Generated {len(components)} component instances")
|
|
177
183
|
return components
|
|
178
184
|
|
|
@@ -184,4 +190,4 @@ class ICManager:
|
|
|
184
190
|
references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
|
|
185
191
|
else:
|
|
186
192
|
references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
|
|
187
|
-
return references
|
|
193
|
+
return references
|
kicad_sch_api/core/junctions.py
CHANGED
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
-
from .types import
|
|
12
|
+
from .types import Junction, Point
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -35,10 +35,10 @@ class JunctionCollection:
|
|
|
35
35
|
self._junctions: List[Junction] = junctions or []
|
|
36
36
|
self._uuid_index: Dict[str, int] = {}
|
|
37
37
|
self._modified = False
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
# Build UUID index
|
|
40
40
|
self._rebuild_index()
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
logger.debug(f"JunctionCollection initialized with {len(self._junctions)} junctions")
|
|
43
43
|
|
|
44
44
|
def _rebuild_index(self):
|
|
@@ -64,7 +64,7 @@ class JunctionCollection:
|
|
|
64
64
|
position: Union[Point, Tuple[float, float]],
|
|
65
65
|
diameter: float = 0,
|
|
66
66
|
color: Tuple[int, int, int, int] = (0, 0, 0, 0),
|
|
67
|
-
uuid: Optional[str] = None
|
|
67
|
+
uuid: Optional[str] = None,
|
|
68
68
|
) -> str:
|
|
69
69
|
"""
|
|
70
70
|
Add a junction to the collection.
|
|
@@ -92,12 +92,7 @@ class JunctionCollection:
|
|
|
92
92
|
position = Point(position[0], position[1])
|
|
93
93
|
|
|
94
94
|
# Create junction
|
|
95
|
-
junction = Junction(
|
|
96
|
-
uuid=uuid,
|
|
97
|
-
position=position,
|
|
98
|
-
diameter=diameter,
|
|
99
|
-
color=color
|
|
100
|
-
)
|
|
95
|
+
junction = Junction(uuid=uuid, position=position, diameter=diameter, color=color)
|
|
101
96
|
|
|
102
97
|
# Add to collection
|
|
103
98
|
self._junctions.append(junction)
|
|
@@ -128,7 +123,9 @@ class JunctionCollection:
|
|
|
128
123
|
logger.debug(f"Removed junction: {uuid}")
|
|
129
124
|
return True
|
|
130
125
|
|
|
131
|
-
def get_at_position(
|
|
126
|
+
def get_at_position(
|
|
127
|
+
self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
128
|
+
) -> Optional[Junction]:
|
|
132
129
|
"""
|
|
133
130
|
Find junction at or near a specific position.
|
|
134
131
|
|
|
@@ -145,10 +142,12 @@ class JunctionCollection:
|
|
|
145
142
|
for junction in self._junctions:
|
|
146
143
|
if junction.position.distance_to(position) <= tolerance:
|
|
147
144
|
return junction
|
|
148
|
-
|
|
145
|
+
|
|
149
146
|
return None
|
|
150
147
|
|
|
151
|
-
def get_by_point(
|
|
148
|
+
def get_by_point(
|
|
149
|
+
self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
150
|
+
) -> List[Junction]:
|
|
152
151
|
"""
|
|
153
152
|
Find all junctions near a point.
|
|
154
153
|
|
|
@@ -172,21 +171,17 @@ class JunctionCollection:
|
|
|
172
171
|
def get_statistics(self) -> Dict[str, Any]:
|
|
173
172
|
"""Get junction collection statistics."""
|
|
174
173
|
if not self._junctions:
|
|
175
|
-
return {
|
|
176
|
-
|
|
177
|
-
"avg_diameter": 0,
|
|
178
|
-
"positions": []
|
|
179
|
-
}
|
|
180
|
-
|
|
174
|
+
return {"total_junctions": 0, "avg_diameter": 0, "positions": []}
|
|
175
|
+
|
|
181
176
|
avg_diameter = sum(j.diameter for j in self._junctions) / len(self._junctions)
|
|
182
177
|
positions = [(j.position.x, j.position.y) for j in self._junctions]
|
|
183
|
-
|
|
178
|
+
|
|
184
179
|
return {
|
|
185
180
|
"total_junctions": len(self._junctions),
|
|
186
181
|
"avg_diameter": avg_diameter,
|
|
187
182
|
"positions": positions,
|
|
188
183
|
"unique_diameters": len(set(j.diameter for j in self._junctions)),
|
|
189
|
-
"unique_colors": len(set(j.color for j in self._junctions))
|
|
184
|
+
"unique_colors": len(set(j.color for j in self._junctions)),
|
|
190
185
|
}
|
|
191
186
|
|
|
192
187
|
def clear(self):
|
|
@@ -203,4 +198,4 @@ class JunctionCollection:
|
|
|
203
198
|
|
|
204
199
|
def mark_saved(self):
|
|
205
200
|
"""Mark collection as saved (reset modified flag)."""
|
|
206
|
-
self._modified = False
|
|
201
|
+
self._modified = False
|