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 CHANGED
@@ -42,7 +42,7 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.0.1"
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, 1)
57
+ VERSION_INFO = (0, 0, 2)
58
58
 
59
59
  # Public API
60
60
  __all__ = [
@@ -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 hasattr(component, key):
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
@@ -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={1, 2})
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=True, quote_indices={1})
88
- self.rules["path"] = FormatRule(inline=True, quote_indices={1})
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
- elements.append(f'"{element}"')
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 in ("symbol", "wire", "junction", "label"):
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
- result = f'({lst[0]} "{lst[1]}" "{lst[2]}"'
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
- result += f' "{element}"'
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