kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.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.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,8 @@ This module centralizes all magic numbers and configuration values
6
6
  to make them easily configurable and maintainable.
7
7
  """
8
8
 
9
- from dataclasses import dataclass
10
- from typing import Any, Dict, Tuple
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Tuple
11
11
 
12
12
 
13
13
  @dataclass
@@ -57,20 +57,102 @@ class DefaultValues:
57
57
 
58
58
  project_name: str = "untitled"
59
59
  stroke_width: float = 0.0
60
+ stroke_type: str = "default"
61
+ fill_type: str = "none"
60
62
  font_size: float = 1.27
61
63
  pin_name_size: float = 1.27
62
64
  pin_number_size: float = 1.27
63
65
 
64
66
 
67
+ @dataclass
68
+ class FileFormatConstants:
69
+ """KiCAD file format identifiers and version strings."""
70
+
71
+ file_type: str = "kicad_sch"
72
+ generator_default: str = "eeschema"
73
+ version_default: str = "20250114"
74
+ generator_version_default: str = "9.0"
75
+
76
+
77
+ @dataclass
78
+ class PaperSizeConstants:
79
+ """Standard paper size definitions."""
80
+
81
+ default: str = "A4"
82
+ valid_sizes: List[str] = field(
83
+ default_factory=lambda: ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class FieldNames:
89
+ """Common S-expression field names to avoid typos."""
90
+
91
+ # File structure
92
+ version: str = "version"
93
+ generator: str = "generator"
94
+ generator_version: str = "generator_version"
95
+ uuid: str = "uuid"
96
+ paper: str = "paper"
97
+
98
+ # Positioning
99
+ at: str = "at"
100
+ xy: str = "xy"
101
+ pts: str = "pts"
102
+ start: str = "start"
103
+ end: str = "end"
104
+ mid: str = "mid"
105
+ center: str = "center"
106
+ radius: str = "radius"
107
+
108
+ # Styling
109
+ stroke: str = "stroke"
110
+ fill: str = "fill"
111
+ width: str = "width"
112
+ type: str = "type"
113
+ color: str = "color"
114
+
115
+ # Text/Font
116
+ font: str = "font"
117
+ size: str = "size"
118
+ effects: str = "effects"
119
+
120
+ # Components
121
+ pin: str = "pin"
122
+ property: str = "property"
123
+ symbol: str = "symbol"
124
+ lib_id: str = "lib_id"
125
+
126
+ # Graphics
127
+ polyline: str = "polyline"
128
+ arc: str = "arc"
129
+ circle: str = "circle"
130
+ rectangle: str = "rectangle"
131
+ bezier: str = "bezier"
132
+
133
+ # Connection elements
134
+ wire: str = "wire"
135
+ junction: str = "junction"
136
+ no_connect: str = "no_connect"
137
+ label: str = "label"
138
+
139
+ # Hierarchical
140
+ sheet: str = "sheet"
141
+ sheet_instances: str = "sheet_instances"
142
+
143
+
65
144
  class KiCADConfig:
66
145
  """Central configuration class for KiCAD schematic API."""
67
146
 
68
- def __init__(self):
147
+ def __init__(self) -> None:
69
148
  self.properties = PropertyOffsets()
70
149
  self.grid = GridSettings()
71
150
  self.sheet = SheetSettings()
72
151
  self.tolerance = ToleranceSettings()
73
152
  self.defaults = DefaultValues()
153
+ self.file_format = FileFormatConstants()
154
+ self.paper = PaperSizeConstants()
155
+ self.fields = FieldNames()
74
156
 
75
157
  # Names that should not generate title_block (for backward compatibility)
76
158
  # Include test schematic names to maintain reference compatibility
@@ -0,0 +1,5 @@
1
+ """Factory classes for creating schematic elements."""
2
+
3
+ from .element_factory import ElementFactory
4
+
5
+ __all__ = ["ElementFactory"]
@@ -0,0 +1,276 @@
1
+ """
2
+ Element Factory for creating schematic elements from dictionaries.
3
+
4
+ Centralizes object creation logic that was previously duplicated in Schematic.__init__.
5
+ """
6
+
7
+ import uuid
8
+ from typing import Any, Dict, List
9
+
10
+ from ..types import (
11
+ HierarchicalLabelShape,
12
+ Junction,
13
+ Label,
14
+ LabelType,
15
+ Net,
16
+ NoConnect,
17
+ Point,
18
+ Text,
19
+ Wire,
20
+ WireType,
21
+ )
22
+
23
+
24
+ def point_from_dict_or_tuple(position: Any) -> Point:
25
+ """Convert position data (dict or tuple) to Point object."""
26
+ if isinstance(position, dict):
27
+ return Point(position.get("x", 0), position.get("y", 0))
28
+ elif isinstance(position, (list, tuple)):
29
+ return Point(position[0], position[1])
30
+ elif isinstance(position, Point):
31
+ return position
32
+ else:
33
+ return Point(0, 0)
34
+
35
+
36
+ class ElementFactory:
37
+ """Factory for creating schematic elements from dictionary data."""
38
+
39
+ @staticmethod
40
+ def create_wire(wire_dict: Dict[str, Any]) -> Wire:
41
+ """
42
+ Create Wire object from dictionary.
43
+
44
+ Args:
45
+ wire_dict: Dictionary containing wire data
46
+
47
+ Returns:
48
+ Wire object
49
+ """
50
+ points = []
51
+ for point_data in wire_dict.get("points", []):
52
+ if isinstance(point_data, dict):
53
+ points.append(Point(point_data["x"], point_data["y"]))
54
+ elif isinstance(point_data, (list, tuple)):
55
+ points.append(Point(point_data[0], point_data[1]))
56
+ else:
57
+ points.append(point_data)
58
+
59
+ return Wire(
60
+ uuid=wire_dict.get("uuid", str(uuid.uuid4())),
61
+ points=points,
62
+ wire_type=WireType(wire_dict.get("wire_type", "wire")),
63
+ stroke_width=wire_dict.get("stroke_width", 0.0),
64
+ stroke_type=wire_dict.get("stroke_type", "default"),
65
+ )
66
+
67
+ @staticmethod
68
+ def create_junction(junction_dict: Dict[str, Any]) -> Junction:
69
+ """
70
+ Create Junction object from dictionary.
71
+
72
+ Args:
73
+ junction_dict: Dictionary containing junction data
74
+
75
+ Returns:
76
+ Junction object
77
+ """
78
+ position = junction_dict.get("position", {"x": 0, "y": 0})
79
+ pos = point_from_dict_or_tuple(position)
80
+
81
+ return Junction(
82
+ uuid=junction_dict.get("uuid", str(uuid.uuid4())),
83
+ position=pos,
84
+ diameter=junction_dict.get("diameter", 0),
85
+ color=junction_dict.get("color", (0, 0, 0, 0)),
86
+ )
87
+
88
+ @staticmethod
89
+ def create_text(text_dict: Dict[str, Any]) -> Text:
90
+ """
91
+ Create Text object from dictionary.
92
+
93
+ Args:
94
+ text_dict: Dictionary containing text data
95
+
96
+ Returns:
97
+ Text object
98
+ """
99
+ position = text_dict.get("position", {"x": 0, "y": 0})
100
+ pos = point_from_dict_or_tuple(position)
101
+
102
+ return Text(
103
+ uuid=text_dict.get("uuid", str(uuid.uuid4())),
104
+ position=pos,
105
+ text=text_dict.get("text", ""),
106
+ rotation=text_dict.get("rotation", 0.0),
107
+ size=text_dict.get("size", 1.27),
108
+ exclude_from_sim=text_dict.get("exclude_from_sim", False),
109
+ )
110
+
111
+ @staticmethod
112
+ def create_label(label_dict: Dict[str, Any]) -> Label:
113
+ """
114
+ Create Label object from dictionary.
115
+
116
+ Args:
117
+ label_dict: Dictionary containing label data
118
+
119
+ Returns:
120
+ Label object
121
+ """
122
+ position = label_dict.get("position", {"x": 0, "y": 0})
123
+ pos = point_from_dict_or_tuple(position)
124
+
125
+ return Label(
126
+ uuid=label_dict.get("uuid", str(uuid.uuid4())),
127
+ position=pos,
128
+ text=label_dict.get("text", ""),
129
+ label_type=LabelType(label_dict.get("label_type", "local")),
130
+ rotation=label_dict.get("rotation", 0.0),
131
+ size=label_dict.get("size", 1.27),
132
+ shape=(
133
+ HierarchicalLabelShape(label_dict.get("shape"))
134
+ if label_dict.get("shape")
135
+ else None
136
+ ),
137
+ )
138
+
139
+ @staticmethod
140
+ def create_no_connect(no_connect_dict: Dict[str, Any]) -> NoConnect:
141
+ """
142
+ Create NoConnect object from dictionary.
143
+
144
+ Args:
145
+ no_connect_dict: Dictionary containing no-connect data
146
+
147
+ Returns:
148
+ NoConnect object
149
+ """
150
+ position = no_connect_dict.get("position", {"x": 0, "y": 0})
151
+ pos = point_from_dict_or_tuple(position)
152
+
153
+ return NoConnect(
154
+ uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
155
+ position=pos,
156
+ )
157
+
158
+ @staticmethod
159
+ def create_net(net_dict: Dict[str, Any]) -> Net:
160
+ """
161
+ Create Net object from dictionary.
162
+
163
+ Args:
164
+ net_dict: Dictionary containing net data
165
+
166
+ Returns:
167
+ Net object
168
+ """
169
+ return Net(
170
+ name=net_dict.get("name", ""),
171
+ components=net_dict.get("components", []),
172
+ wires=net_dict.get("wires", []),
173
+ labels=net_dict.get("labels", []),
174
+ )
175
+
176
+ @staticmethod
177
+ def create_wires_from_list(wire_data: List[Any]) -> List[Wire]:
178
+ """
179
+ Create list of Wire objects from list of dictionaries.
180
+
181
+ Args:
182
+ wire_data: List of wire dictionaries
183
+
184
+ Returns:
185
+ List of Wire objects
186
+ """
187
+ wires = []
188
+ for wire_dict in wire_data:
189
+ if isinstance(wire_dict, dict):
190
+ wires.append(ElementFactory.create_wire(wire_dict))
191
+ return wires
192
+
193
+ @staticmethod
194
+ def create_junctions_from_list(junction_data: List[Any]) -> List[Junction]:
195
+ """
196
+ Create list of Junction objects from list of dictionaries.
197
+
198
+ Args:
199
+ junction_data: List of junction dictionaries
200
+
201
+ Returns:
202
+ List of Junction objects
203
+ """
204
+ junctions = []
205
+ for junction_dict in junction_data:
206
+ if isinstance(junction_dict, dict):
207
+ junctions.append(ElementFactory.create_junction(junction_dict))
208
+ return junctions
209
+
210
+ @staticmethod
211
+ def create_texts_from_list(text_data: List[Any]) -> List[Text]:
212
+ """
213
+ Create list of Text objects from list of dictionaries.
214
+
215
+ Args:
216
+ text_data: List of text dictionaries
217
+
218
+ Returns:
219
+ List of Text objects
220
+ """
221
+ texts = []
222
+ for text_dict in text_data:
223
+ if isinstance(text_dict, dict):
224
+ texts.append(ElementFactory.create_text(text_dict))
225
+ return texts
226
+
227
+ @staticmethod
228
+ def create_labels_from_list(label_data: List[Any]) -> List[Label]:
229
+ """
230
+ Create list of Label objects from list of dictionaries.
231
+
232
+ Args:
233
+ label_data: List of label dictionaries
234
+
235
+ Returns:
236
+ List of Label objects
237
+ """
238
+ labels = []
239
+ for label_dict in label_data:
240
+ if isinstance(label_dict, dict):
241
+ labels.append(ElementFactory.create_label(label_dict))
242
+ return labels
243
+
244
+ @staticmethod
245
+ def create_no_connects_from_list(no_connect_data: List[Any]) -> List[NoConnect]:
246
+ """
247
+ Create list of NoConnect objects from list of dictionaries.
248
+
249
+ Args:
250
+ no_connect_data: List of no-connect dictionaries
251
+
252
+ Returns:
253
+ List of NoConnect objects
254
+ """
255
+ no_connects = []
256
+ for no_connect_dict in no_connect_data:
257
+ if isinstance(no_connect_dict, dict):
258
+ no_connects.append(ElementFactory.create_no_connect(no_connect_dict))
259
+ return no_connects
260
+
261
+ @staticmethod
262
+ def create_nets_from_list(net_data: List[Any]) -> List[Net]:
263
+ """
264
+ Create list of Net objects from list of dictionaries.
265
+
266
+ Args:
267
+ net_data: List of net dictionaries
268
+
269
+ Returns:
270
+ List of Net objects
271
+ """
272
+ nets = []
273
+ for net_dict in net_data:
274
+ if isinstance(net_dict, dict):
275
+ nets.append(ElementFactory.create_net(net_dict))
276
+ return nets
@@ -125,6 +125,10 @@ class ExactFormatter:
125
125
  self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
126
126
  self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
127
127
 
128
+ # Text elements
129
+ self.rules["text"] = FormatRule(inline=False, quote_indices={1})
130
+ self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
131
+
128
132
  # Effects and text formatting
129
133
  self.rules["effects"] = FormatRule(inline=False)
130
134
  self.rules["font"] = FormatRule(inline=False)
@@ -279,6 +283,8 @@ class ExactFormatter:
279
283
  "junction",
280
284
  "label",
281
285
  "hierarchical_label",
286
+ "text",
287
+ "text_box",
282
288
  "polyline",
283
289
  "rectangle",
284
290
  ):
@@ -384,7 +390,8 @@ class ExactFormatter:
384
390
  result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
385
391
  else:
386
392
  if i in rule.quote_indices and isinstance(element, str):
387
- result += f' "{element}"'
393
+ escaped_element = self._escape_string(element)
394
+ result += f' "{escaped_element}"'
388
395
  else:
389
396
  result += f" {self._format_element(element, 0)}"
390
397
 
@@ -396,7 +403,8 @@ class ExactFormatter:
396
403
  indent = "\t" * indent_level
397
404
  next_indent = "\t" * (indent_level + 1)
398
405
 
399
- result = f"({lst[0]}"
406
+ tag = str(lst[0])
407
+ result = f"({tag}"
400
408
 
401
409
  for i, element in enumerate(lst[1:], 1):
402
410
  if isinstance(element, list):
@@ -425,9 +433,18 @@ class ExactFormatter:
425
433
  return True
426
434
 
427
435
  def _escape_string(self, text: str) -> str:
428
- """Escape quotes in string for S-expression formatting."""
429
- # Replace double quotes with escaped quotes
430
- return text.replace('"', '\\"')
436
+ """Escape special characters in string for S-expression formatting."""
437
+ # Escape backslashes first (must be done before other replacements)
438
+ text = text.replace('\\', '\\\\')
439
+ # Escape double quotes
440
+ text = text.replace('"', '\\"')
441
+ # Escape newlines (convert actual newlines to escaped representation)
442
+ text = text.replace('\n', '\\n')
443
+ # Escape carriage returns
444
+ text = text.replace('\r', '\\r')
445
+ # Escape tabs
446
+ text = text.replace('\t', '\\t')
447
+ return text
431
448
 
432
449
  def _needs_quoting(self, text: str) -> bool:
433
450
  """Check if string needs to be quoted."""
@@ -9,55 +9,34 @@ import logging
9
9
  import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
+ from .collections import BaseCollection
12
13
  from .types import Junction, Point
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class JunctionCollection:
18
+ class JunctionCollection(BaseCollection[Junction]):
18
19
  """
19
20
  Professional junction collection with enhanced management features.
20
21
 
22
+ Inherits from BaseCollection for standard operations and adds junction-specific
23
+ functionality.
24
+
21
25
  Features:
22
- - Fast UUID-based lookup and indexing
26
+ - Fast UUID-based lookup and indexing (inherited)
23
27
  - Position-based junction queries
24
- - Bulk operations for performance
28
+ - Bulk operations for performance (inherited)
25
29
  - Validation and conflict detection
26
30
  """
27
31
 
28
- def __init__(self, junctions: Optional[List[Junction]] = None):
32
+ def __init__(self, junctions: Optional[List[Junction]] = None) -> None:
29
33
  """
30
34
  Initialize junction collection.
31
35
 
32
36
  Args:
33
37
  junctions: Initial list of junctions
34
38
  """
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]]
39
+ super().__init__(junctions, collection_name="junctions")
61
40
 
62
41
  def add(
63
42
  self,
@@ -94,35 +73,12 @@ class JunctionCollection:
94
73
  # Create junction
95
74
  junction = Junction(uuid=uuid, position=position, diameter=diameter, color=color)
96
75
 
97
- # Add to collection
98
- self._junctions.append(junction)
99
- self._uuid_index[uuid] = len(self._junctions) - 1
100
- self._modified = True
76
+ # Add to collection using base class method
77
+ self._add_item(junction)
101
78
 
102
79
  logger.debug(f"Added junction at {position}, UUID={uuid}")
103
80
  return uuid
104
81
 
105
- def remove(self, uuid: str) -> bool:
106
- """
107
- Remove junction by UUID.
108
-
109
- Args:
110
- uuid: Junction UUID to remove
111
-
112
- Returns:
113
- True if junction was removed, False if not found
114
- """
115
- if uuid not in self._uuid_index:
116
- return False
117
-
118
- index = self._uuid_index[uuid]
119
- del self._junctions[index]
120
- self._rebuild_index()
121
- self._modified = True
122
-
123
- logger.debug(f"Removed junction: {uuid}")
124
- return True
125
-
126
82
  def get_at_position(
127
83
  self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
128
84
  ) -> Optional[Junction]:
@@ -139,7 +95,7 @@ class JunctionCollection:
139
95
  if isinstance(position, tuple):
140
96
  position = Point(position[0], position[1])
141
97
 
142
- for junction in self._junctions:
98
+ for junction in self._items:
143
99
  if junction.position.distance_to(position) <= tolerance:
144
100
  return junction
145
101
 
@@ -162,40 +118,35 @@ class JunctionCollection:
162
118
  point = Point(point[0], point[1])
163
119
 
164
120
  matching_junctions = []
165
- for junction in self._junctions:
121
+ for junction in self._items:
166
122
  if junction.position.distance_to(point) <= tolerance:
167
123
  matching_junctions.append(junction)
168
124
 
169
125
  return matching_junctions
170
126
 
171
127
  def get_statistics(self) -> Dict[str, Any]:
172
- """Get junction collection statistics."""
173
- if not self._junctions:
174
- return {"total_junctions": 0, "avg_diameter": 0, "positions": []}
128
+ """Get junction collection statistics (extends base statistics)."""
129
+ base_stats = super().get_statistics()
130
+ if not self._items:
131
+ return {**base_stats, "total_junctions": 0, "avg_diameter": 0, "positions": []}
175
132
 
176
- avg_diameter = sum(j.diameter for j in self._junctions) / len(self._junctions)
177
- positions = [(j.position.x, j.position.y) for j in self._junctions]
133
+ avg_diameter = sum(j.diameter for j in self._items) / len(self._items)
134
+ positions = [(j.position.x, j.position.y) for j in self._items]
178
135
 
179
136
  return {
180
- "total_junctions": len(self._junctions),
137
+ **base_stats,
138
+ "total_junctions": len(self._items),
181
139
  "avg_diameter": avg_diameter,
182
140
  "positions": positions,
183
- "unique_diameters": len(set(j.diameter for j in self._junctions)),
184
- "unique_colors": len(set(j.color for j in self._junctions)),
141
+ "unique_diameters": len(set(j.diameter for j in self._items)),
142
+ "unique_colors": len(set(j.color for j in self._items)),
185
143
  }
186
144
 
187
- def clear(self):
188
- """Remove all junctions from collection."""
189
- self._junctions.clear()
190
- self._uuid_index.clear()
191
- self._modified = True
192
- logger.debug("Cleared all junctions")
193
-
194
145
  @property
195
146
  def modified(self) -> bool:
196
147
  """Check if collection has been modified."""
197
- return self._modified
148
+ return self.is_modified()
198
149
 
199
- def mark_saved(self):
150
+ def mark_saved(self) -> None:
200
151
  """Mark collection as saved (reset modified flag)."""
201
- self._modified = False
152
+ self.reset_modified_flag()