kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.2__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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +6 -2
- kicad_sch_api/cli.py +67 -62
- 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 +190 -24
- 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 +587 -197
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +683 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +44 -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.0.dist-info → kicad_sch_api-0.2.2.dist-info}/METADATA +212 -40
- kicad_sch_api-0.2.2.dist-info/RECORD +31 -0
- kicad_sch_api-0.2.0.dist-info/RECORD +0 -24
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -47,13 +47,15 @@ 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})
|
|
55
57
|
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
56
|
-
self.rules["paper"] = FormatRule(inline=True,
|
|
58
|
+
self.rules["paper"] = FormatRule(inline=True) # No quotes for paper size (A4, A3, etc.)
|
|
57
59
|
|
|
58
60
|
# Title block
|
|
59
61
|
self.rules["title_block"] = FormatRule(inline=False)
|
|
@@ -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)
|
|
@@ -104,6 +112,12 @@ class ExactFormatter:
|
|
|
104
112
|
self.rules["junction"] = FormatRule(inline=False)
|
|
105
113
|
self.rules["diameter"] = FormatRule(inline=True)
|
|
106
114
|
|
|
115
|
+
# Graphical elements
|
|
116
|
+
self.rules["rectangle"] = FormatRule(inline=False)
|
|
117
|
+
self.rules["start"] = FormatRule(inline=True)
|
|
118
|
+
self.rules["end"] = FormatRule(inline=True)
|
|
119
|
+
self.rules["fill"] = FormatRule(inline=False)
|
|
120
|
+
|
|
107
121
|
# Labels
|
|
108
122
|
self.rules["label"] = FormatRule(inline=False, quote_indices={1})
|
|
109
123
|
self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
|
|
@@ -117,6 +131,15 @@ class ExactFormatter:
|
|
|
117
131
|
self.rules["justify"] = FormatRule(inline=True)
|
|
118
132
|
self.rules["hide"] = FormatRule(inline=True)
|
|
119
133
|
|
|
134
|
+
# Graphical elements
|
|
135
|
+
self.rules["rectangle"] = FormatRule(inline=False)
|
|
136
|
+
self.rules["polyline"] = FormatRule(inline=False)
|
|
137
|
+
self.rules["graphics"] = FormatRule(inline=False)
|
|
138
|
+
self.rules["start"] = FormatRule(inline=True)
|
|
139
|
+
self.rules["end"] = FormatRule(inline=True)
|
|
140
|
+
self.rules["fill"] = FormatRule(inline=False)
|
|
141
|
+
self.rules["color"] = FormatRule(inline=True)
|
|
142
|
+
|
|
120
143
|
# Sheet instances and metadata
|
|
121
144
|
self.rules["sheet_instances"] = FormatRule(inline=False)
|
|
122
145
|
self.rules["symbol_instances"] = FormatRule(inline=False)
|
|
@@ -133,7 +156,11 @@ class ExactFormatter:
|
|
|
133
156
|
Returns:
|
|
134
157
|
Formatted string matching KiCAD's output exactly
|
|
135
158
|
"""
|
|
136
|
-
|
|
159
|
+
result = self._format_element(data, 0)
|
|
160
|
+
# Ensure file ends with newline
|
|
161
|
+
if not result.endswith("\n"):
|
|
162
|
+
result += "\n"
|
|
163
|
+
return result
|
|
137
164
|
|
|
138
165
|
def format_preserving_write(self, new_data: Any, original_content: str) -> str:
|
|
139
166
|
"""
|
|
@@ -164,9 +191,33 @@ class ExactFormatter:
|
|
|
164
191
|
if self._needs_quoting(element):
|
|
165
192
|
return f'"{element}"'
|
|
166
193
|
return element
|
|
194
|
+
elif isinstance(element, float):
|
|
195
|
+
# Custom float formatting for KiCAD compatibility
|
|
196
|
+
return self._format_float(element)
|
|
167
197
|
else:
|
|
168
198
|
return str(element)
|
|
169
199
|
|
|
200
|
+
def _format_float(self, value: float) -> str:
|
|
201
|
+
"""Format float values to match KiCAD's precision and format."""
|
|
202
|
+
# Handle special case for zero values in color alpha
|
|
203
|
+
if abs(value) < 1e-10: # Essentially zero
|
|
204
|
+
return "0.0000"
|
|
205
|
+
|
|
206
|
+
# For other values, use minimal precision (remove trailing zeros)
|
|
207
|
+
if value == int(value):
|
|
208
|
+
return str(int(value))
|
|
209
|
+
|
|
210
|
+
# Round to reasonable precision and remove trailing zeros
|
|
211
|
+
rounded = round(value, 6) # Use 6 decimal places for precision
|
|
212
|
+
if rounded == int(rounded):
|
|
213
|
+
return str(int(rounded))
|
|
214
|
+
|
|
215
|
+
# Format and remove trailing zeros, but don't remove the decimal point for values like 0.254
|
|
216
|
+
formatted = f"{rounded:.6f}".rstrip("0")
|
|
217
|
+
if formatted.endswith("."):
|
|
218
|
+
formatted += "0" # Keep at least one decimal place
|
|
219
|
+
return formatted
|
|
220
|
+
|
|
170
221
|
def _format_list(self, lst: List[Any], indent_level: int) -> str:
|
|
171
222
|
"""Format a list (S-expression)."""
|
|
172
223
|
if not lst:
|
|
@@ -216,7 +267,15 @@ class ExactFormatter:
|
|
|
216
267
|
return self._format_property(lst, indent_level)
|
|
217
268
|
elif tag == "pin":
|
|
218
269
|
return self._format_pin(lst, indent_level)
|
|
219
|
-
elif tag in (
|
|
270
|
+
elif tag in (
|
|
271
|
+
"symbol",
|
|
272
|
+
"wire",
|
|
273
|
+
"junction",
|
|
274
|
+
"label",
|
|
275
|
+
"hierarchical_label",
|
|
276
|
+
"polyline",
|
|
277
|
+
"rectangle",
|
|
278
|
+
):
|
|
220
279
|
return self._format_component_like(lst, indent_level, rule)
|
|
221
280
|
else:
|
|
222
281
|
return self._format_generic_multiline(lst, indent_level, rule)
|
|
@@ -248,27 +307,57 @@ class ExactFormatter:
|
|
|
248
307
|
"""Format pin elements with context-aware quoting."""
|
|
249
308
|
if len(lst) < 2:
|
|
250
309
|
return self._format_inline(lst, FormatRule())
|
|
251
|
-
|
|
310
|
+
|
|
252
311
|
indent = "\t" * indent_level
|
|
253
312
|
next_indent = "\t" * (indent_level + 1)
|
|
254
|
-
|
|
255
|
-
# Check if this is a lib_symbols pin (passive/line) or
|
|
256
|
-
if
|
|
313
|
+
|
|
314
|
+
# Check if this is a lib_symbols pin (passive/line) or sheet pin ("NET1" input)
|
|
315
|
+
if (
|
|
316
|
+
len(lst) >= 3
|
|
317
|
+
and isinstance(lst[2], sexpdata.Symbol)
|
|
318
|
+
and str(lst[1])
|
|
319
|
+
in [
|
|
320
|
+
"passive",
|
|
321
|
+
"power_in",
|
|
322
|
+
"power_out",
|
|
323
|
+
"input",
|
|
324
|
+
"output",
|
|
325
|
+
"bidirectional",
|
|
326
|
+
"tri_state",
|
|
327
|
+
"unspecified",
|
|
328
|
+
]
|
|
329
|
+
):
|
|
257
330
|
# lib_symbols context: (pin passive line ...)
|
|
258
331
|
result = f"({lst[0]} {lst[1]} {lst[2]}"
|
|
259
332
|
start_index = 3
|
|
333
|
+
|
|
334
|
+
# Add remaining elements on separate lines with proper indentation
|
|
335
|
+
for element in lst[start_index:]:
|
|
336
|
+
if isinstance(element, list):
|
|
337
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
338
|
+
|
|
339
|
+
result += f"\n{indent})"
|
|
340
|
+
return result
|
|
260
341
|
else:
|
|
261
|
-
# component context: (pin "1" ...)
|
|
262
|
-
|
|
342
|
+
# sheet pin or component pin context: (pin "NET1" input) or (pin "1" ...)
|
|
343
|
+
# Pin name should always be quoted
|
|
344
|
+
pin_name = str(lst[1])
|
|
345
|
+
result = f'({lst[0]} "{pin_name}"'
|
|
263
346
|
start_index = 2
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
347
|
+
|
|
348
|
+
# Add remaining elements (type and others)
|
|
349
|
+
for i, element in enumerate(lst[start_index:], start_index):
|
|
350
|
+
if isinstance(element, list):
|
|
351
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
352
|
+
else:
|
|
353
|
+
# Convert pin type to symbol if it's a string
|
|
354
|
+
if i == 2 and isinstance(element, str):
|
|
355
|
+
result += f" {element}" # Pin type as bare symbol
|
|
356
|
+
else:
|
|
357
|
+
result += f" {self._format_element(element, 0)}"
|
|
358
|
+
|
|
359
|
+
result += f"\n{indent})"
|
|
360
|
+
return result
|
|
272
361
|
|
|
273
362
|
def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
274
363
|
"""Format component-like elements (symbol, wire, etc.)."""
|
|
@@ -340,6 +429,83 @@ class ExactFormatter:
|
|
|
340
429
|
special_chars = "()[]{}#"
|
|
341
430
|
return any(c in text for c in special_chars)
|
|
342
431
|
|
|
432
|
+
def _format_kicad_sch(self, lst: List[Any], indent_level: int) -> str:
|
|
433
|
+
"""
|
|
434
|
+
Custom formatter for kicad_sch root element to handle blank schematic format.
|
|
435
|
+
|
|
436
|
+
Detects blank schematics and formats them exactly like KiCAD reference files.
|
|
437
|
+
"""
|
|
438
|
+
# Check if this is a blank schematic (no components, no UUID, minimal elements)
|
|
439
|
+
has_components = any(
|
|
440
|
+
isinstance(item, list)
|
|
441
|
+
and len(item) > 0
|
|
442
|
+
and str(item[0])
|
|
443
|
+
in ["symbol", "wire", "junction", "text", "sheet", "polyline", "rectangle", "graphics"]
|
|
444
|
+
for item in lst[1:]
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
has_uuid = any(
|
|
448
|
+
isinstance(item, list) and len(item) >= 2 and str(item[0]) == "uuid" for item in lst[1:]
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# If no components and no UUID, format as blank schematic
|
|
452
|
+
if not has_components and not has_uuid:
|
|
453
|
+
header_parts = [str(lst[0])] # kicad_sch
|
|
454
|
+
body_parts = []
|
|
455
|
+
|
|
456
|
+
for item in lst[1:]:
|
|
457
|
+
if isinstance(item, list) and len(item) >= 1:
|
|
458
|
+
tag = str(item[0])
|
|
459
|
+
if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
|
|
460
|
+
if tag in ["generator", "generator_version"]:
|
|
461
|
+
header_parts.append(f'({tag} "{item[1]}")')
|
|
462
|
+
else:
|
|
463
|
+
header_parts.append(f"({tag} {item[1]})")
|
|
464
|
+
else:
|
|
465
|
+
body_parts.append(item)
|
|
466
|
+
|
|
467
|
+
# Build single-line header + body format
|
|
468
|
+
result = f"({' '.join(header_parts)}"
|
|
469
|
+
for item in body_parts:
|
|
470
|
+
if isinstance(item, list) and len(item) == 1:
|
|
471
|
+
result += f"\n ({item[0]})"
|
|
472
|
+
else:
|
|
473
|
+
result += f"\n {self._format_element(item, 1)}"
|
|
474
|
+
result += "\n)\n"
|
|
475
|
+
return result
|
|
476
|
+
|
|
477
|
+
# For normal schematics, use standard multiline formatting
|
|
478
|
+
return self._format_multiline(lst, indent_level, FormatRule())
|
|
479
|
+
|
|
480
|
+
def _format_pts(self, lst: List[Any], indent_level: int) -> str:
|
|
481
|
+
"""Format pts elements with inline xy coordinates on indented line."""
|
|
482
|
+
if len(lst) < 2:
|
|
483
|
+
return self._format_inline(lst, FormatRule())
|
|
484
|
+
|
|
485
|
+
indent = "\t" * indent_level
|
|
486
|
+
next_indent = "\t" * (indent_level + 1)
|
|
487
|
+
|
|
488
|
+
# Format as:
|
|
489
|
+
# (pts
|
|
490
|
+
# (xy x1 y1) (xy x2 y2)
|
|
491
|
+
# )
|
|
492
|
+
result = f"({lst[0]}"
|
|
493
|
+
|
|
494
|
+
# Add xy elements on same indented line
|
|
495
|
+
if len(lst) > 1:
|
|
496
|
+
xy_elements = []
|
|
497
|
+
for element in lst[1:]:
|
|
498
|
+
if isinstance(element, list) and len(element) >= 3 and str(element[0]) == "xy":
|
|
499
|
+
xy_elements.append(self._format_element(element, 0))
|
|
500
|
+
else:
|
|
501
|
+
xy_elements.append(self._format_element(element, 0))
|
|
502
|
+
|
|
503
|
+
if xy_elements:
|
|
504
|
+
result += f"\n{next_indent}{' '.join(xy_elements)}"
|
|
505
|
+
|
|
506
|
+
result += f"\n{indent})"
|
|
507
|
+
return result
|
|
508
|
+
|
|
343
509
|
|
|
344
510
|
class CompactFormatter(ExactFormatter):
|
|
345
511
|
"""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
|