kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/core/components.py +63 -0
- kicad_sch_api/core/formatter.py +59 -12
- kicad_sch_api/core/ic_manager.py +187 -0
- kicad_sch_api/core/junctions.py +206 -0
- kicad_sch_api/core/parser.py +622 -31
- kicad_sch_api/core/schematic.py +774 -16
- kicad_sch_api/core/types.py +103 -8
- kicad_sch_api/core/wires.py +248 -0
- kicad_sch_api/library/cache.py +321 -10
- kicad_sch_api/utils/validation.py +1 -1
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.1.dist-info}/METADATA +15 -19
- kicad_sch_api-0.1.1.dist-info/RECORD +21 -0
- kicad_sch_api-0.0.2.dist-info/RECORD +0 -18
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.1.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/components.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
|
12
12
|
from ..library.cache import SymbolDefinition, get_symbol_cache
|
|
13
13
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
14
14
|
from .types import Point, SchematicPin, SchematicSymbol
|
|
15
|
+
from .ic_manager import ICManager
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -290,6 +291,7 @@ class ComponentCollection:
|
|
|
290
291
|
value: str = "",
|
|
291
292
|
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
292
293
|
footprint: Optional[str] = None,
|
|
294
|
+
unit: int = 1,
|
|
293
295
|
**properties,
|
|
294
296
|
) -> Component:
|
|
295
297
|
"""
|
|
@@ -301,6 +303,7 @@ class ComponentCollection:
|
|
|
301
303
|
value: Component value
|
|
302
304
|
position: Component position (auto-placed if None)
|
|
303
305
|
footprint: Component footprint
|
|
306
|
+
unit: Unit number for multi-unit components (1-based)
|
|
304
307
|
**properties: Additional component properties
|
|
305
308
|
|
|
306
309
|
Returns:
|
|
@@ -340,6 +343,7 @@ class ComponentCollection:
|
|
|
340
343
|
reference=reference,
|
|
341
344
|
value=value,
|
|
342
345
|
footprint=footprint,
|
|
346
|
+
unit=unit,
|
|
343
347
|
properties=properties,
|
|
344
348
|
)
|
|
345
349
|
|
|
@@ -359,6 +363,65 @@ class ComponentCollection:
|
|
|
359
363
|
logger.info(f"Added component: {reference} ({lib_id})")
|
|
360
364
|
return component
|
|
361
365
|
|
|
366
|
+
def add_ic(
|
|
367
|
+
self,
|
|
368
|
+
lib_id: str,
|
|
369
|
+
reference_prefix: str,
|
|
370
|
+
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
371
|
+
value: str = "",
|
|
372
|
+
footprint: Optional[str] = None,
|
|
373
|
+
layout_style: str = "vertical",
|
|
374
|
+
**properties,
|
|
375
|
+
) -> ICManager:
|
|
376
|
+
"""
|
|
377
|
+
Add a multi-unit IC with automatic unit placement.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
lib_id: Library identifier for the IC (e.g., "74xx:7400")
|
|
381
|
+
reference_prefix: Base reference (e.g., "U1" → U1A, U1B, etc.)
|
|
382
|
+
position: Base position for auto-layout (auto-placed if None)
|
|
383
|
+
value: IC value (defaults to symbol name)
|
|
384
|
+
footprint: IC footprint
|
|
385
|
+
layout_style: Layout algorithm ("vertical", "grid", "functional")
|
|
386
|
+
**properties: Common properties for all units
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
ICManager object for position overrides and management
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
ic = sch.components.add_ic("74xx:7400", "U1", position=(100, 100))
|
|
393
|
+
ic.place_unit(1, position=(150, 80)) # Override Gate A position
|
|
394
|
+
"""
|
|
395
|
+
# Set default position if not provided
|
|
396
|
+
if position is None:
|
|
397
|
+
position = self._find_available_position()
|
|
398
|
+
elif isinstance(position, tuple):
|
|
399
|
+
position = Point(position[0], position[1])
|
|
400
|
+
|
|
401
|
+
# Set default value to symbol name if not provided
|
|
402
|
+
if not value:
|
|
403
|
+
value = lib_id.split(":")[-1] # "74xx:7400" → "7400"
|
|
404
|
+
|
|
405
|
+
# Create IC manager for this multi-unit component
|
|
406
|
+
ic_manager = ICManager(lib_id, reference_prefix, position, self)
|
|
407
|
+
|
|
408
|
+
# Generate all unit components
|
|
409
|
+
unit_components = ic_manager.generate_components(
|
|
410
|
+
value=value,
|
|
411
|
+
footprint=footprint,
|
|
412
|
+
properties=properties
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Add all units to the collection
|
|
416
|
+
for component_data in unit_components:
|
|
417
|
+
component = Component(component_data, self)
|
|
418
|
+
self._add_to_indexes(component)
|
|
419
|
+
|
|
420
|
+
self._modified = True
|
|
421
|
+
logger.info(f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units")
|
|
422
|
+
|
|
423
|
+
return ic_manager
|
|
424
|
+
|
|
362
425
|
def remove(self, reference: str) -> bool:
|
|
363
426
|
"""
|
|
364
427
|
Remove component by reference.
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -51,13 +51,15 @@ class ExactFormatter:
|
|
|
51
51
|
self.rules["kicad_sch"] = FormatRule(inline=False, indent_level=0)
|
|
52
52
|
self.rules["version"] = FormatRule(inline=True)
|
|
53
53
|
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
54
|
+
self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
|
|
54
55
|
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
56
|
+
self.rules["paper"] = FormatRule(inline=True, quote_indices={1})
|
|
55
57
|
|
|
56
58
|
# Title block
|
|
57
59
|
self.rules["title_block"] = FormatRule(inline=False)
|
|
58
60
|
self.rules["title"] = FormatRule(inline=True, quote_indices={1})
|
|
59
61
|
self.rules["company"] = FormatRule(inline=True, quote_indices={1})
|
|
60
|
-
self.rules["
|
|
62
|
+
self.rules["rev"] = FormatRule(inline=True, quote_indices={1}) # KiCAD uses "rev"
|
|
61
63
|
self.rules["date"] = FormatRule(inline=True, quote_indices={1})
|
|
62
64
|
self.rules["size"] = FormatRule(inline=True, quote_indices={1})
|
|
63
65
|
self.rules["comment"] = FormatRule(inline=True, quote_indices={2})
|
|
@@ -81,11 +83,13 @@ class ExactFormatter:
|
|
|
81
83
|
inline=False, quote_indices={1, 2}, custom_handler=self._format_property
|
|
82
84
|
)
|
|
83
85
|
|
|
84
|
-
# Pins and connections
|
|
85
|
-
self.rules["pin"] = FormatRule(inline=False, quote_indices=
|
|
86
|
+
# Pins and connections
|
|
87
|
+
self.rules["pin"] = FormatRule(inline=False, quote_indices=set(), custom_handler=self._format_pin)
|
|
88
|
+
self.rules["number"] = FormatRule(inline=True, quote_indices={1}) # Pin numbers should be quoted
|
|
89
|
+
self.rules["name"] = FormatRule(inline=True, quote_indices={1}) # Pin names should be quoted
|
|
86
90
|
self.rules["instances"] = FormatRule(inline=False)
|
|
87
|
-
self.rules["project"] = FormatRule(inline=
|
|
88
|
-
self.rules["path"] = FormatRule(inline=
|
|
91
|
+
self.rules["project"] = FormatRule(inline=False, quote_indices={1})
|
|
92
|
+
self.rules["path"] = FormatRule(inline=False, quote_indices={1})
|
|
89
93
|
self.rules["reference"] = FormatRule(inline=True, quote_indices={1})
|
|
90
94
|
|
|
91
95
|
# Wire elements
|
|
@@ -113,6 +117,12 @@ class ExactFormatter:
|
|
|
113
117
|
self.rules["justify"] = FormatRule(inline=True)
|
|
114
118
|
self.rules["hide"] = FormatRule(inline=True)
|
|
115
119
|
|
|
120
|
+
# Sheet instances and metadata
|
|
121
|
+
self.rules["sheet_instances"] = FormatRule(inline=False)
|
|
122
|
+
self.rules["symbol_instances"] = FormatRule(inline=False)
|
|
123
|
+
self.rules["embedded_fonts"] = FormatRule(inline=True)
|
|
124
|
+
self.rules["page"] = FormatRule(inline=True, quote_indices={1})
|
|
125
|
+
|
|
116
126
|
def format(self, data: Any) -> str:
|
|
117
127
|
"""
|
|
118
128
|
Format S-expression data with exact KiCAD formatting.
|
|
@@ -181,7 +191,8 @@ class ExactFormatter:
|
|
|
181
191
|
elements = []
|
|
182
192
|
for i, element in enumerate(lst):
|
|
183
193
|
if i in rule.quote_indices and isinstance(element, str):
|
|
184
|
-
|
|
194
|
+
escaped_element = self._escape_string(element)
|
|
195
|
+
elements.append(f'"{escaped_element}"')
|
|
185
196
|
else:
|
|
186
197
|
elements.append(self._format_element(element, 0))
|
|
187
198
|
return f"({' '.join(elements)})"
|
|
@@ -203,7 +214,9 @@ class ExactFormatter:
|
|
|
203
214
|
# Handle different multiline formats based on tag
|
|
204
215
|
if tag == "property":
|
|
205
216
|
return self._format_property(lst, indent_level)
|
|
206
|
-
elif tag
|
|
217
|
+
elif tag == "pin":
|
|
218
|
+
return self._format_pin(lst, indent_level)
|
|
219
|
+
elif tag in ("symbol", "wire", "junction", "label", "hierarchical_label"):
|
|
207
220
|
return self._format_component_like(lst, indent_level, rule)
|
|
208
221
|
else:
|
|
209
222
|
return self._format_generic_multiline(lst, indent_level, rule)
|
|
@@ -217,7 +230,9 @@ class ExactFormatter:
|
|
|
217
230
|
next_indent = "\t" * (indent_level + 1)
|
|
218
231
|
|
|
219
232
|
# Property format: (property "Name" "Value" (at x y rotation) (effects ...))
|
|
220
|
-
|
|
233
|
+
escaped_name = self._escape_string(str(lst[1]))
|
|
234
|
+
escaped_value = self._escape_string(str(lst[2]))
|
|
235
|
+
result = f'({lst[0]} "{escaped_name}" "{escaped_value}"'
|
|
221
236
|
|
|
222
237
|
# Add position and effects on separate lines
|
|
223
238
|
for element in lst[3:]:
|
|
@@ -226,7 +241,33 @@ class ExactFormatter:
|
|
|
226
241
|
else:
|
|
227
242
|
result += f" {element}"
|
|
228
243
|
|
|
229
|
-
result += ")"
|
|
244
|
+
result += f"\n{indent})"
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
def _format_pin(self, lst: List[Any], indent_level: int) -> str:
|
|
248
|
+
"""Format pin elements with context-aware quoting."""
|
|
249
|
+
if len(lst) < 2:
|
|
250
|
+
return self._format_inline(lst, FormatRule())
|
|
251
|
+
|
|
252
|
+
indent = "\t" * indent_level
|
|
253
|
+
next_indent = "\t" * (indent_level + 1)
|
|
254
|
+
|
|
255
|
+
# Check if this is a lib_symbols pin (passive/line) or component pin ("1")
|
|
256
|
+
if len(lst) >= 3 and isinstance(lst[2], sexpdata.Symbol):
|
|
257
|
+
# lib_symbols context: (pin passive line ...)
|
|
258
|
+
result = f"({lst[0]} {lst[1]} {lst[2]}"
|
|
259
|
+
start_index = 3
|
|
260
|
+
else:
|
|
261
|
+
# component context: (pin "1" ...)
|
|
262
|
+
result = f'({lst[0]} "{lst[1]}"'
|
|
263
|
+
start_index = 2
|
|
264
|
+
|
|
265
|
+
# Add remaining elements on separate lines
|
|
266
|
+
for element in lst[start_index:]:
|
|
267
|
+
if isinstance(element, list):
|
|
268
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
269
|
+
|
|
270
|
+
result += f"\n{indent})"
|
|
230
271
|
return result
|
|
231
272
|
|
|
232
273
|
def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
@@ -248,7 +289,7 @@ class ExactFormatter:
|
|
|
248
289
|
else:
|
|
249
290
|
result += f" {self._format_element(element, 0)}"
|
|
250
291
|
|
|
251
|
-
result += ")"
|
|
292
|
+
result += f"\n{indent})"
|
|
252
293
|
return result
|
|
253
294
|
|
|
254
295
|
def _format_generic_multiline(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
@@ -263,11 +304,12 @@ class ExactFormatter:
|
|
|
263
304
|
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
264
305
|
else:
|
|
265
306
|
if i in rule.quote_indices and isinstance(element, str):
|
|
266
|
-
|
|
307
|
+
escaped_element = self._escape_string(element)
|
|
308
|
+
result += f' "{escaped_element}"'
|
|
267
309
|
else:
|
|
268
310
|
result += f" {self._format_element(element, 0)}"
|
|
269
311
|
|
|
270
|
-
result += ")"
|
|
312
|
+
result += f"\n{indent})"
|
|
271
313
|
return result
|
|
272
314
|
|
|
273
315
|
def _should_format_inline(self, lst: List[Any], rule: FormatRule) -> bool:
|
|
@@ -283,6 +325,11 @@ class ExactFormatter:
|
|
|
283
325
|
|
|
284
326
|
return True
|
|
285
327
|
|
|
328
|
+
def _escape_string(self, text: str) -> str:
|
|
329
|
+
"""Escape quotes in string for S-expression formatting."""
|
|
330
|
+
# Replace double quotes with escaped quotes
|
|
331
|
+
return text.replace('"', '\\"')
|
|
332
|
+
|
|
286
333
|
def _needs_quoting(self, text: str) -> bool:
|
|
287
334
|
"""Check if string needs to be quoted."""
|
|
288
335
|
# Quote if contains spaces, special characters, or is empty
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IC management for multi-unit components with auto-layout capabilities.
|
|
3
|
+
|
|
4
|
+
This module provides intelligent placement and management for multi-unit ICs
|
|
5
|
+
like op-amps, logic gates, and other complex components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid as uuid_module
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from .types import Point, SchematicSymbol
|
|
13
|
+
from ..library.cache import get_symbol_cache
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ICManager:
|
|
19
|
+
"""
|
|
20
|
+
Manager for multi-unit IC components with auto-layout capabilities.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Automatic unit detection and placement
|
|
24
|
+
- Smart layout algorithms (vertical, grid, functional)
|
|
25
|
+
- Individual unit position override
|
|
26
|
+
- Professional spacing and alignment
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, lib_id: str, reference_prefix: str, position: Point,
|
|
30
|
+
component_collection: "ComponentCollection"):
|
|
31
|
+
"""
|
|
32
|
+
Initialize IC manager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
lib_id: IC library ID (e.g., "74xx:7400")
|
|
36
|
+
reference_prefix: Base reference (e.g., "U1" → U1A, U1B, etc.)
|
|
37
|
+
position: Base position for auto-layout
|
|
38
|
+
component_collection: Parent collection for component management
|
|
39
|
+
"""
|
|
40
|
+
self.lib_id = lib_id
|
|
41
|
+
self.reference_prefix = reference_prefix
|
|
42
|
+
self.base_position = position
|
|
43
|
+
self._collection = component_collection
|
|
44
|
+
self._units: Dict[int, SchematicSymbol] = {}
|
|
45
|
+
self._unit_positions: Dict[int, Point] = {}
|
|
46
|
+
|
|
47
|
+
# Detect available units from symbol library
|
|
48
|
+
self._detect_available_units()
|
|
49
|
+
|
|
50
|
+
# Auto-place all units with default layout
|
|
51
|
+
self._auto_layout_units()
|
|
52
|
+
|
|
53
|
+
logger.debug(f"ICManager initialized for {lib_id} with {len(self._units)} units")
|
|
54
|
+
|
|
55
|
+
def _detect_available_units(self):
|
|
56
|
+
"""Detect available units from symbol library definition."""
|
|
57
|
+
cache = get_symbol_cache()
|
|
58
|
+
symbol_def = cache.get_symbol(self.lib_id)
|
|
59
|
+
|
|
60
|
+
if not symbol_def or not hasattr(symbol_def, 'raw_kicad_data'):
|
|
61
|
+
logger.warning(f"Could not detect units for {self.lib_id}")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Parse symbol data to find unit definitions
|
|
65
|
+
symbol_data = symbol_def.raw_kicad_data
|
|
66
|
+
if isinstance(symbol_data, list):
|
|
67
|
+
for item in symbol_data[1:]:
|
|
68
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
69
|
+
if item[0] == getattr(item[0], 'value', str(item[0])) == 'symbol':
|
|
70
|
+
unit_name = str(item[1]).strip('"')
|
|
71
|
+
# Extract unit number from name like "7400_1_1" → unit 1
|
|
72
|
+
unit_num = self._extract_unit_number(unit_name)
|
|
73
|
+
if unit_num and unit_num not in self._units:
|
|
74
|
+
logger.debug(f"Detected unit {unit_num} from {unit_name}")
|
|
75
|
+
|
|
76
|
+
def _extract_unit_number(self, unit_name: str) -> Optional[int]:
|
|
77
|
+
"""Extract unit number from symbol unit name like '7400_1_1' → 1."""
|
|
78
|
+
parts = unit_name.split('_')
|
|
79
|
+
if len(parts) >= 3:
|
|
80
|
+
try:
|
|
81
|
+
return int(parts[-2]) # Second to last part is unit number
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def _auto_layout_units(self):
|
|
87
|
+
"""Auto-place all units using vertical layout algorithm."""
|
|
88
|
+
# Define unit layout based on detected units
|
|
89
|
+
self._place_default_units()
|
|
90
|
+
|
|
91
|
+
def _place_default_units(self):
|
|
92
|
+
"""Place default units for common IC types."""
|
|
93
|
+
# For 74xx logic ICs, typically have units 1-4 (logic) + unit 5 (power)
|
|
94
|
+
unit_spacing = 12.7 # Tight vertical spacing (0.5 inch in mm)
|
|
95
|
+
power_offset = (25.4, 0) # Power unit offset (1 inch right)
|
|
96
|
+
|
|
97
|
+
# Place logic units (1-4) vertically in a tight column
|
|
98
|
+
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
|
+
)
|
|
103
|
+
self._unit_positions[unit] = unit_pos
|
|
104
|
+
|
|
105
|
+
# Place power unit (5) to the right of logic units
|
|
106
|
+
if 5 not in self._unit_positions:
|
|
107
|
+
power_pos = Point(
|
|
108
|
+
self.base_position.x + power_offset[0],
|
|
109
|
+
self.base_position.y + unit_spacing # Align with second gate
|
|
110
|
+
)
|
|
111
|
+
self._unit_positions[5] = power_pos
|
|
112
|
+
|
|
113
|
+
logger.debug(f"Auto-placed {len(self._unit_positions)} units with tight spacing")
|
|
114
|
+
|
|
115
|
+
def place_unit(self, unit: int, position: Union[Point, Tuple[float, float]]):
|
|
116
|
+
"""
|
|
117
|
+
Override the position of a specific unit.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
unit: Unit number to place
|
|
121
|
+
position: New position for the unit
|
|
122
|
+
"""
|
|
123
|
+
if isinstance(position, tuple):
|
|
124
|
+
position = Point(position[0], position[1])
|
|
125
|
+
|
|
126
|
+
self._unit_positions[unit] = position
|
|
127
|
+
|
|
128
|
+
# If component already exists, update its position
|
|
129
|
+
if unit in self._units:
|
|
130
|
+
self._units[unit].position = position
|
|
131
|
+
self._collection._mark_modified()
|
|
132
|
+
|
|
133
|
+
logger.debug(f"Placed unit {unit} at {position}")
|
|
134
|
+
|
|
135
|
+
def get_unit_position(self, unit: int) -> Optional[Point]:
|
|
136
|
+
"""Get the position of a specific unit."""
|
|
137
|
+
return self._unit_positions.get(unit)
|
|
138
|
+
|
|
139
|
+
def get_all_units(self) -> Dict[int, Point]:
|
|
140
|
+
"""Get all unit positions."""
|
|
141
|
+
return self._unit_positions.copy()
|
|
142
|
+
|
|
143
|
+
def generate_components(self, **common_properties) -> List[SchematicSymbol]:
|
|
144
|
+
"""
|
|
145
|
+
Generate all component instances for this IC.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
**common_properties: Properties to apply to all units
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of component symbols for all units
|
|
152
|
+
"""
|
|
153
|
+
components = []
|
|
154
|
+
|
|
155
|
+
for unit, position in self._unit_positions.items():
|
|
156
|
+
# Generate unit reference (U1 → U1A, U1B, etc.)
|
|
157
|
+
if unit <= 4:
|
|
158
|
+
unit_ref = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1A, U1B, U1C, U1D
|
|
159
|
+
else:
|
|
160
|
+
unit_ref = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1E for power
|
|
161
|
+
|
|
162
|
+
component = SchematicSymbol(
|
|
163
|
+
uuid=str(uuid_module.uuid4()),
|
|
164
|
+
lib_id=self.lib_id,
|
|
165
|
+
position=position,
|
|
166
|
+
reference=unit_ref,
|
|
167
|
+
value=common_properties.get('value', self.lib_id.split(':')[-1]),
|
|
168
|
+
footprint=common_properties.get('footprint'),
|
|
169
|
+
unit=unit,
|
|
170
|
+
properties=common_properties.get('properties', {})
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
components.append(component)
|
|
174
|
+
self._units[unit] = component
|
|
175
|
+
|
|
176
|
+
logger.debug(f"Generated {len(components)} component instances")
|
|
177
|
+
return components
|
|
178
|
+
|
|
179
|
+
def get_unit_references(self) -> Dict[int, str]:
|
|
180
|
+
"""Get mapping of unit numbers to references."""
|
|
181
|
+
references = {}
|
|
182
|
+
for unit in self._unit_positions.keys():
|
|
183
|
+
if unit <= 4:
|
|
184
|
+
references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
|
|
185
|
+
else:
|
|
186
|
+
references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
|
|
187
|
+
return references
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Junction collection and management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides enhanced junction management for wire intersections
|
|
5
|
+
and connection points with performance optimization and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid as uuid_module
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from .types import Point, Junction
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JunctionCollection:
|
|
18
|
+
"""
|
|
19
|
+
Professional junction collection with enhanced management features.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Fast UUID-based lookup and indexing
|
|
23
|
+
- Position-based junction queries
|
|
24
|
+
- Bulk operations for performance
|
|
25
|
+
- Validation and conflict detection
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, junctions: Optional[List[Junction]] = None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize junction collection.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
junctions: Initial list of junctions
|
|
34
|
+
"""
|
|
35
|
+
self._junctions: List[Junction] = junctions or []
|
|
36
|
+
self._uuid_index: Dict[str, int] = {}
|
|
37
|
+
self._modified = False
|
|
38
|
+
|
|
39
|
+
# Build UUID index
|
|
40
|
+
self._rebuild_index()
|
|
41
|
+
|
|
42
|
+
logger.debug(f"JunctionCollection initialized with {len(self._junctions)} junctions")
|
|
43
|
+
|
|
44
|
+
def _rebuild_index(self):
|
|
45
|
+
"""Rebuild UUID index for fast lookups."""
|
|
46
|
+
self._uuid_index = {junction.uuid: i for i, junction in enumerate(self._junctions)}
|
|
47
|
+
|
|
48
|
+
def __len__(self) -> int:
|
|
49
|
+
"""Number of junctions in collection."""
|
|
50
|
+
return len(self._junctions)
|
|
51
|
+
|
|
52
|
+
def __iter__(self):
|
|
53
|
+
"""Iterate over junctions."""
|
|
54
|
+
return iter(self._junctions)
|
|
55
|
+
|
|
56
|
+
def __getitem__(self, uuid: str) -> Junction:
|
|
57
|
+
"""Get junction by UUID."""
|
|
58
|
+
if uuid not in self._uuid_index:
|
|
59
|
+
raise KeyError(f"Junction with UUID '{uuid}' not found")
|
|
60
|
+
return self._junctions[self._uuid_index[uuid]]
|
|
61
|
+
|
|
62
|
+
def add(
|
|
63
|
+
self,
|
|
64
|
+
position: Union[Point, Tuple[float, float]],
|
|
65
|
+
diameter: float = 0,
|
|
66
|
+
color: Tuple[int, int, int, int] = (0, 0, 0, 0),
|
|
67
|
+
uuid: Optional[str] = None
|
|
68
|
+
) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Add a junction to the collection.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
position: Junction position
|
|
74
|
+
diameter: Junction diameter (0 is KiCAD default)
|
|
75
|
+
color: RGBA color tuple (0,0,0,0 is default)
|
|
76
|
+
uuid: Optional UUID (auto-generated if not provided)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
UUID of the created junction
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If UUID already exists
|
|
83
|
+
"""
|
|
84
|
+
# Generate UUID if not provided
|
|
85
|
+
if uuid is None:
|
|
86
|
+
uuid = str(uuid_module.uuid4())
|
|
87
|
+
elif uuid in self._uuid_index:
|
|
88
|
+
raise ValueError(f"Junction with UUID '{uuid}' already exists")
|
|
89
|
+
|
|
90
|
+
# Convert position
|
|
91
|
+
if isinstance(position, tuple):
|
|
92
|
+
position = Point(position[0], position[1])
|
|
93
|
+
|
|
94
|
+
# Create junction
|
|
95
|
+
junction = Junction(
|
|
96
|
+
uuid=uuid,
|
|
97
|
+
position=position,
|
|
98
|
+
diameter=diameter,
|
|
99
|
+
color=color
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Add to collection
|
|
103
|
+
self._junctions.append(junction)
|
|
104
|
+
self._uuid_index[uuid] = len(self._junctions) - 1
|
|
105
|
+
self._modified = True
|
|
106
|
+
|
|
107
|
+
logger.debug(f"Added junction at {position}, UUID={uuid}")
|
|
108
|
+
return uuid
|
|
109
|
+
|
|
110
|
+
def remove(self, uuid: str) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Remove junction by UUID.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
uuid: Junction UUID to remove
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if junction was removed, False if not found
|
|
119
|
+
"""
|
|
120
|
+
if uuid not in self._uuid_index:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
index = self._uuid_index[uuid]
|
|
124
|
+
del self._junctions[index]
|
|
125
|
+
self._rebuild_index()
|
|
126
|
+
self._modified = True
|
|
127
|
+
|
|
128
|
+
logger.debug(f"Removed junction: {uuid}")
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
def get_at_position(self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01) -> Optional[Junction]:
|
|
132
|
+
"""
|
|
133
|
+
Find junction at or near a specific position.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
position: Position to search
|
|
137
|
+
tolerance: Distance tolerance for matching
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Junction if found, None otherwise
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(position, tuple):
|
|
143
|
+
position = Point(position[0], position[1])
|
|
144
|
+
|
|
145
|
+
for junction in self._junctions:
|
|
146
|
+
if junction.position.distance_to(position) <= tolerance:
|
|
147
|
+
return junction
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def get_by_point(self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01) -> List[Junction]:
|
|
152
|
+
"""
|
|
153
|
+
Find all junctions near a point.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
point: Point to search near
|
|
157
|
+
tolerance: Distance tolerance
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of junctions near the point
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(point, tuple):
|
|
163
|
+
point = Point(point[0], point[1])
|
|
164
|
+
|
|
165
|
+
matching_junctions = []
|
|
166
|
+
for junction in self._junctions:
|
|
167
|
+
if junction.position.distance_to(point) <= tolerance:
|
|
168
|
+
matching_junctions.append(junction)
|
|
169
|
+
|
|
170
|
+
return matching_junctions
|
|
171
|
+
|
|
172
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
173
|
+
"""Get junction collection statistics."""
|
|
174
|
+
if not self._junctions:
|
|
175
|
+
return {
|
|
176
|
+
"total_junctions": 0,
|
|
177
|
+
"avg_diameter": 0,
|
|
178
|
+
"positions": []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
avg_diameter = sum(j.diameter for j in self._junctions) / len(self._junctions)
|
|
182
|
+
positions = [(j.position.x, j.position.y) for j in self._junctions]
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"total_junctions": len(self._junctions),
|
|
186
|
+
"avg_diameter": avg_diameter,
|
|
187
|
+
"positions": positions,
|
|
188
|
+
"unique_diameters": len(set(j.diameter for j in self._junctions)),
|
|
189
|
+
"unique_colors": len(set(j.color for j in self._junctions))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def clear(self):
|
|
193
|
+
"""Remove all junctions from collection."""
|
|
194
|
+
self._junctions.clear()
|
|
195
|
+
self._uuid_index.clear()
|
|
196
|
+
self._modified = True
|
|
197
|
+
logger.debug("Cleared all junctions")
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def modified(self) -> bool:
|
|
201
|
+
"""Check if collection has been modified."""
|
|
202
|
+
return self._modified
|
|
203
|
+
|
|
204
|
+
def mark_saved(self):
|
|
205
|
+
"""Mark collection as saved (reset modified flag)."""
|
|
206
|
+
self._modified = False
|