kicad-sch-api 0.0.1__py3-none-any.whl → 0.1.0__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 +2 -2
- kicad_sch_api/core/components.py +69 -2
- kicad_sch_api/core/formatter.py +56 -11
- kicad_sch_api/core/ic_manager.py +187 -0
- kicad_sch_api/core/junctions.py +206 -0
- kicad_sch_api/core/parser.py +637 -35
- kicad_sch_api/core/schematic.py +739 -8
- kicad_sch_api/core/types.py +102 -7
- kicad_sch_api/core/wires.py +248 -0
- kicad_sch_api/library/cache.py +321 -10
- kicad_sch_api/utils/validation.py +3 -3
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/METADATA +14 -33
- kicad_sch_api-0.1.0.dist-info/RECORD +21 -0
- kicad_sch_api/mcp/__init__.py +0 -5
- kicad_sch_api/mcp/server.py +0 -500
- kicad_sch_api-0.0.1.dist-info/RECORD +0 -20
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.0.1.dist-info → kicad_sch_api-0.1.0.dist-info}/top_level.txt +0 -0
kicad_sch_api/__init__.py
CHANGED
|
@@ -42,7 +42,7 @@ Advanced Usage:
|
|
|
42
42
|
print(f"Found {len(issues)} validation issues")
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
__version__ = "0.0.
|
|
45
|
+
__version__ = "0.0.2"
|
|
46
46
|
__author__ = "Circuit-Synth"
|
|
47
47
|
__email__ = "info@circuit-synth.com"
|
|
48
48
|
|
|
@@ -54,7 +54,7 @@ from .library.cache import SymbolLibraryCache, get_symbol_cache
|
|
|
54
54
|
from .utils.validation import ValidationError, ValidationIssue
|
|
55
55
|
|
|
56
56
|
# Version info
|
|
57
|
-
VERSION_INFO = (0, 0,
|
|
57
|
+
VERSION_INFO = (0, 0, 2)
|
|
58
58
|
|
|
59
59
|
# Public API
|
|
60
60
|
__all__ = [
|
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.
|
|
@@ -471,9 +534,13 @@ class ComponentCollection:
|
|
|
471
534
|
matching = self.filter(**criteria)
|
|
472
535
|
|
|
473
536
|
for component in matching:
|
|
474
|
-
# Update basic properties
|
|
537
|
+
# Update basic properties and handle special cases
|
|
475
538
|
for key, value in updates.items():
|
|
476
|
-
if
|
|
539
|
+
if key == 'properties' and isinstance(value, dict):
|
|
540
|
+
# Handle properties dictionary specially
|
|
541
|
+
for prop_name, prop_value in value.items():
|
|
542
|
+
component.set_property(prop_name, str(prop_value))
|
|
543
|
+
elif hasattr(component, key) and key not in ['properties']:
|
|
477
544
|
setattr(component, key, value)
|
|
478
545
|
else:
|
|
479
546
|
# Add as custom property
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -81,11 +81,13 @@ class ExactFormatter:
|
|
|
81
81
|
inline=False, quote_indices={1, 2}, custom_handler=self._format_property
|
|
82
82
|
)
|
|
83
83
|
|
|
84
|
-
# Pins and connections
|
|
85
|
-
self.rules["pin"] = FormatRule(inline=False, quote_indices=
|
|
84
|
+
# Pins and connections
|
|
85
|
+
self.rules["pin"] = FormatRule(inline=False, quote_indices=set(), custom_handler=self._format_pin)
|
|
86
|
+
self.rules["number"] = FormatRule(inline=True, quote_indices={1}) # Pin numbers should be quoted
|
|
87
|
+
self.rules["name"] = FormatRule(inline=True, quote_indices={1}) # Pin names should be quoted
|
|
86
88
|
self.rules["instances"] = FormatRule(inline=False)
|
|
87
|
-
self.rules["project"] = FormatRule(inline=
|
|
88
|
-
self.rules["path"] = FormatRule(inline=
|
|
89
|
+
self.rules["project"] = FormatRule(inline=False, quote_indices={1})
|
|
90
|
+
self.rules["path"] = FormatRule(inline=False, quote_indices={1})
|
|
89
91
|
self.rules["reference"] = FormatRule(inline=True, quote_indices={1})
|
|
90
92
|
|
|
91
93
|
# Wire elements
|
|
@@ -113,6 +115,12 @@ class ExactFormatter:
|
|
|
113
115
|
self.rules["justify"] = FormatRule(inline=True)
|
|
114
116
|
self.rules["hide"] = FormatRule(inline=True)
|
|
115
117
|
|
|
118
|
+
# Sheet instances and metadata
|
|
119
|
+
self.rules["sheet_instances"] = FormatRule(inline=False)
|
|
120
|
+
self.rules["symbol_instances"] = FormatRule(inline=False)
|
|
121
|
+
self.rules["embedded_fonts"] = FormatRule(inline=True)
|
|
122
|
+
self.rules["page"] = FormatRule(inline=True, quote_indices={1})
|
|
123
|
+
|
|
116
124
|
def format(self, data: Any) -> str:
|
|
117
125
|
"""
|
|
118
126
|
Format S-expression data with exact KiCAD formatting.
|
|
@@ -181,7 +189,8 @@ class ExactFormatter:
|
|
|
181
189
|
elements = []
|
|
182
190
|
for i, element in enumerate(lst):
|
|
183
191
|
if i in rule.quote_indices and isinstance(element, str):
|
|
184
|
-
|
|
192
|
+
escaped_element = self._escape_string(element)
|
|
193
|
+
elements.append(f'"{escaped_element}"')
|
|
185
194
|
else:
|
|
186
195
|
elements.append(self._format_element(element, 0))
|
|
187
196
|
return f"({' '.join(elements)})"
|
|
@@ -203,7 +212,9 @@ class ExactFormatter:
|
|
|
203
212
|
# Handle different multiline formats based on tag
|
|
204
213
|
if tag == "property":
|
|
205
214
|
return self._format_property(lst, indent_level)
|
|
206
|
-
elif tag
|
|
215
|
+
elif tag == "pin":
|
|
216
|
+
return self._format_pin(lst, indent_level)
|
|
217
|
+
elif tag in ("symbol", "wire", "junction", "label", "hierarchical_label"):
|
|
207
218
|
return self._format_component_like(lst, indent_level, rule)
|
|
208
219
|
else:
|
|
209
220
|
return self._format_generic_multiline(lst, indent_level, rule)
|
|
@@ -217,7 +228,9 @@ class ExactFormatter:
|
|
|
217
228
|
next_indent = "\t" * (indent_level + 1)
|
|
218
229
|
|
|
219
230
|
# Property format: (property "Name" "Value" (at x y rotation) (effects ...))
|
|
220
|
-
|
|
231
|
+
escaped_name = self._escape_string(str(lst[1]))
|
|
232
|
+
escaped_value = self._escape_string(str(lst[2]))
|
|
233
|
+
result = f'({lst[0]} "{escaped_name}" "{escaped_value}"'
|
|
221
234
|
|
|
222
235
|
# Add position and effects on separate lines
|
|
223
236
|
for element in lst[3:]:
|
|
@@ -226,7 +239,33 @@ class ExactFormatter:
|
|
|
226
239
|
else:
|
|
227
240
|
result += f" {element}"
|
|
228
241
|
|
|
229
|
-
result += ")"
|
|
242
|
+
result += f"\n{indent})"
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def _format_pin(self, lst: List[Any], indent_level: int) -> str:
|
|
246
|
+
"""Format pin elements with context-aware quoting."""
|
|
247
|
+
if len(lst) < 2:
|
|
248
|
+
return self._format_inline(lst, FormatRule())
|
|
249
|
+
|
|
250
|
+
indent = "\t" * indent_level
|
|
251
|
+
next_indent = "\t" * (indent_level + 1)
|
|
252
|
+
|
|
253
|
+
# Check if this is a lib_symbols pin (passive/line) or component pin ("1")
|
|
254
|
+
if len(lst) >= 3 and isinstance(lst[2], sexpdata.Symbol):
|
|
255
|
+
# lib_symbols context: (pin passive line ...)
|
|
256
|
+
result = f"({lst[0]} {lst[1]} {lst[2]}"
|
|
257
|
+
start_index = 3
|
|
258
|
+
else:
|
|
259
|
+
# component context: (pin "1" ...)
|
|
260
|
+
result = f'({lst[0]} "{lst[1]}"'
|
|
261
|
+
start_index = 2
|
|
262
|
+
|
|
263
|
+
# Add remaining elements on separate lines
|
|
264
|
+
for element in lst[start_index:]:
|
|
265
|
+
if isinstance(element, list):
|
|
266
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
267
|
+
|
|
268
|
+
result += f"\n{indent})"
|
|
230
269
|
return result
|
|
231
270
|
|
|
232
271
|
def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
@@ -248,7 +287,7 @@ class ExactFormatter:
|
|
|
248
287
|
else:
|
|
249
288
|
result += f" {self._format_element(element, 0)}"
|
|
250
289
|
|
|
251
|
-
result += ")"
|
|
290
|
+
result += f"\n{indent})"
|
|
252
291
|
return result
|
|
253
292
|
|
|
254
293
|
def _format_generic_multiline(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
@@ -263,11 +302,12 @@ class ExactFormatter:
|
|
|
263
302
|
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
264
303
|
else:
|
|
265
304
|
if i in rule.quote_indices and isinstance(element, str):
|
|
266
|
-
|
|
305
|
+
escaped_element = self._escape_string(element)
|
|
306
|
+
result += f' "{escaped_element}"'
|
|
267
307
|
else:
|
|
268
308
|
result += f" {self._format_element(element, 0)}"
|
|
269
309
|
|
|
270
|
-
result += ")"
|
|
310
|
+
result += f"\n{indent})"
|
|
271
311
|
return result
|
|
272
312
|
|
|
273
313
|
def _should_format_inline(self, lst: List[Any], rule: FormatRule) -> bool:
|
|
@@ -283,6 +323,11 @@ class ExactFormatter:
|
|
|
283
323
|
|
|
284
324
|
return True
|
|
285
325
|
|
|
326
|
+
def _escape_string(self, text: str) -> str:
|
|
327
|
+
"""Escape quotes in string for S-expression formatting."""
|
|
328
|
+
# Replace double quotes with escaped quotes
|
|
329
|
+
return text.replace('"', '\\"')
|
|
330
|
+
|
|
286
331
|
def _needs_quoting(self, text: str) -> bool:
|
|
287
332
|
"""Check if string needs to be quoted."""
|
|
288
333
|
# 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
|