kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.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.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  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/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,391 @@
1
+ """
2
+ ERC validators: PinType, Connectivity, Component, Power.
3
+
4
+ Individual validators for different categories of electrical rules.
5
+ """
6
+
7
+ import re
8
+ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
9
+
10
+ from kicad_sch_api.validation.erc_models import ERCViolation
11
+ from kicad_sch_api.validation.pin_matrix import PinConflictMatrix, PinSeverity
12
+
13
+ if TYPE_CHECKING:
14
+ from kicad_sch_api.core.schematic import Schematic
15
+
16
+
17
+ class BaseValidator:
18
+ """Base class for ERC validators."""
19
+
20
+ def __init__(self, schematic: "Schematic") -> None:
21
+ """Initialize validator.
22
+
23
+ Args:
24
+ schematic: Schematic to validate
25
+ """
26
+ self.schematic = schematic
27
+
28
+ def validate(self) -> List[ERCViolation]:
29
+ """Run validation and return violations.
30
+
31
+ Returns:
32
+ List of violations found
33
+ """
34
+ raise NotImplementedError("Subclasses must implement validate()")
35
+
36
+
37
+ class PinTypeValidator(BaseValidator):
38
+ """Validates pin-to-pin connections for electrical conflicts.
39
+
40
+ Checks all nets for pin type compatibility using the pin conflict matrix.
41
+ """
42
+
43
+ def __init__(self, schematic: "Schematic", pin_matrix: Optional[PinConflictMatrix] = None) -> None:
44
+ """Initialize pin type validator.
45
+
46
+ Args:
47
+ schematic: Schematic to validate
48
+ pin_matrix: Optional custom pin conflict matrix
49
+ """
50
+ super().__init__(schematic)
51
+ self.pin_matrix = pin_matrix or PinConflictMatrix()
52
+
53
+ def validate(self) -> List[ERCViolation]:
54
+ """Validate pin connections on all nets.
55
+
56
+ Returns:
57
+ List of pin conflict violations
58
+ """
59
+ violations: List[ERCViolation] = []
60
+
61
+ # Build nets from wires and components
62
+ nets = self._build_nets()
63
+
64
+ # Check each net for pin conflicts
65
+ for net_name, pins in nets.items():
66
+ net_violations = self._check_net_pins(net_name, pins)
67
+ violations.extend(net_violations)
68
+
69
+ return violations
70
+
71
+ def _build_nets(self) -> Dict[str, List[Tuple[str, str, str]]]:
72
+ """Build net connectivity map.
73
+
74
+ Returns:
75
+ Dict mapping net name to list of (component_ref, pin_num, pin_type) tuples
76
+ """
77
+ # TODO: Implement net tracing from wires and components
78
+ # For now, return placeholder
79
+ # This will be implemented when we have full net connectivity analysis
80
+ return {}
81
+
82
+ def _check_net_pins(self, net_name: str, pins: List[Tuple[str, str, str]]) -> List[ERCViolation]:
83
+ """Check all pin pairs on a net for conflicts.
84
+
85
+ Args:
86
+ net_name: Net name
87
+ pins: List of (component_ref, pin_num, pin_type) tuples
88
+
89
+ Returns:
90
+ List of violations found on this net
91
+ """
92
+ violations: List[ERCViolation] = []
93
+
94
+ # Check all pairs of pins
95
+ for i, (ref1, pin1_num, pin1_type) in enumerate(pins):
96
+ for ref2, pin2_num, pin2_type in pins[i + 1:]:
97
+ severity = self.pin_matrix.check_connection(pin1_type, pin2_type)
98
+
99
+ if severity == PinSeverity.ERROR:
100
+ violations.append(ERCViolation(
101
+ violation_type="pin_conflict",
102
+ severity="error",
103
+ message=f"Pin conflict: {pin1_type} ({ref1}) connected to {pin2_type} ({ref2})",
104
+ component_refs=[ref1, ref2],
105
+ net_name=net_name,
106
+ pin_numbers=[pin1_num, pin2_num],
107
+ error_code="E001",
108
+ suggested_fix=f"Remove one output or add buffer between {ref1} and {ref2}"
109
+ ))
110
+ elif severity == PinSeverity.WARNING:
111
+ violations.append(ERCViolation(
112
+ violation_type="pin_conflict",
113
+ severity="warning",
114
+ message=f"Pin warning: {pin1_type} ({ref1}) connected to {pin2_type} ({ref2})",
115
+ component_refs=[ref1, ref2],
116
+ net_name=net_name,
117
+ pin_numbers=[pin1_num, pin2_num],
118
+ error_code="W005",
119
+ suggested_fix="Verify this connection is intentional"
120
+ ))
121
+
122
+ return violations
123
+
124
+
125
+ class ConnectivityValidator(BaseValidator):
126
+ """Validates wire connectivity and net driving.
127
+
128
+ Checks for dangling wires, unconnected pins, and undriven nets.
129
+ """
130
+
131
+ def validate(self) -> List[ERCViolation]:
132
+ """Validate connectivity.
133
+
134
+ Returns:
135
+ List of connectivity violations
136
+ """
137
+ violations: List[ERCViolation] = []
138
+
139
+ violations.extend(self.find_dangling_wires())
140
+ violations.extend(self.find_unconnected_pins())
141
+ violations.extend(self.find_undriven_nets())
142
+
143
+ return violations
144
+
145
+ def find_dangling_wires(self) -> List[ERCViolation]:
146
+ """Find wires with only one connection.
147
+
148
+ Returns:
149
+ List of dangling wire violations
150
+ """
151
+ violations: List[ERCViolation] = []
152
+
153
+ for wire in self.schematic.wires:
154
+ # Check endpoints for connections
155
+ start_connections = self._count_connections_at_point(wire.start)
156
+ end_connections = self._count_connections_at_point(wire.end)
157
+
158
+ if start_connections < 2 or end_connections < 2:
159
+ violations.append(ERCViolation(
160
+ violation_type="dangling_wire",
161
+ severity="warning",
162
+ message=f"Wire has unconnected endpoint at ({wire.start.x}, {wire.start.y})",
163
+ component_refs=[],
164
+ location=wire.start if start_connections < 2 else wire.end,
165
+ error_code="W002",
166
+ suggested_fix="Connect wire to component pin or remove if unused"
167
+ ))
168
+
169
+ return violations
170
+
171
+ def find_unconnected_pins(self) -> List[ERCViolation]:
172
+ """Find input pins with no connections.
173
+
174
+ Returns:
175
+ List of unconnected pin violations
176
+ """
177
+ violations: List[ERCViolation] = []
178
+
179
+ for component in self.schematic.components:
180
+ # TODO: Get pin types from symbol library
181
+ # For now, check if any pins have no wires
182
+ pass
183
+
184
+ return violations
185
+
186
+ def find_undriven_nets(self) -> List[ERCViolation]:
187
+ """Find nets with only input pins (no output driver).
188
+
189
+ Returns:
190
+ List of undriven net violations
191
+ """
192
+ violations: List[ERCViolation] = []
193
+
194
+ # TODO: Implement net tracing and driver detection
195
+ # This requires full net connectivity analysis
196
+
197
+ return violations
198
+
199
+ def _count_connections_at_point(self, point) -> int:
200
+ """Count number of connections at a point.
201
+
202
+ Args:
203
+ point: Point to check
204
+
205
+ Returns:
206
+ Number of wires/pins at this point
207
+ """
208
+ # TODO: Implement proper connection counting
209
+ # For now, return 2 (assume connected)
210
+ return 2
211
+
212
+
213
+ class ComponentValidator(BaseValidator):
214
+ """Validates component properties and references.
215
+
216
+ Checks for duplicate references, missing values, invalid formats.
217
+ """
218
+
219
+ # Valid reference format: Letter(s) followed by number(s)
220
+ REFERENCE_PATTERN = re.compile(r'^[A-Z]+[0-9]+$', re.IGNORECASE)
221
+
222
+ def validate(self) -> List[ERCViolation]:
223
+ """Validate components.
224
+
225
+ Returns:
226
+ List of component violations
227
+ """
228
+ violations: List[ERCViolation] = []
229
+
230
+ violations.extend(self.find_duplicate_references())
231
+ violations.extend(self.validate_component_properties())
232
+
233
+ return violations
234
+
235
+ def find_duplicate_references(self) -> List[ERCViolation]:
236
+ """Find components with duplicate reference designators.
237
+
238
+ Returns:
239
+ List of duplicate reference violations
240
+ """
241
+ violations: List[ERCViolation] = []
242
+
243
+ # Build reference count map
244
+ ref_to_components: Dict[str, List[str]] = {}
245
+
246
+ for component in self.schematic.components:
247
+ ref = component.reference
248
+ if ref not in ref_to_components:
249
+ ref_to_components[ref] = []
250
+ ref_to_components[ref].append(ref)
251
+
252
+ # Find duplicates
253
+ for ref, components in ref_to_components.items():
254
+ if len(components) > 1:
255
+ violations.append(ERCViolation(
256
+ violation_type="duplicate_reference",
257
+ severity="error",
258
+ message=f"Duplicate reference designator: {ref}",
259
+ component_refs=[ref] * len(components),
260
+ error_code="E004",
261
+ suggested_fix=f"Rename duplicate components (e.g., {ref}, {ref}A, {ref}B)"
262
+ ))
263
+
264
+ return violations
265
+
266
+ def validate_component_properties(self) -> List[ERCViolation]:
267
+ """Validate component properties (value, footprint, etc.).
268
+
269
+ Returns:
270
+ List of property violations
271
+ """
272
+ violations: List[ERCViolation] = []
273
+
274
+ for component in self.schematic.components:
275
+ # Check for missing value
276
+ if not component.value or component.value.strip() == "":
277
+ violations.append(ERCViolation(
278
+ violation_type="missing_value",
279
+ severity="warning",
280
+ message=f"Component {component.reference} has no value",
281
+ component_refs=[component.reference],
282
+ error_code="W008",
283
+ suggested_fix=f"Add value to {component.reference}"
284
+ ))
285
+
286
+ # Check for missing footprint
287
+ if not component.footprint or component.footprint.strip() == "":
288
+ violations.append(ERCViolation(
289
+ violation_type="missing_footprint",
290
+ severity="warning",
291
+ message=f"Component {component.reference} has no footprint",
292
+ component_refs=[component.reference],
293
+ error_code="W007",
294
+ suggested_fix=f"Assign footprint to {component.reference}"
295
+ ))
296
+
297
+ # Check reference format
298
+ if not self.REFERENCE_PATTERN.match(component.reference):
299
+ violations.append(ERCViolation(
300
+ violation_type="invalid_reference",
301
+ severity="error",
302
+ message=f"Invalid reference format: {component.reference}",
303
+ component_refs=[component.reference],
304
+ error_code="E005",
305
+ suggested_fix="Use format like R1, U1, C1 (letter + number)"
306
+ ))
307
+
308
+ return violations
309
+
310
+
311
+ class PowerValidator(BaseValidator):
312
+ """Validates power supply connections.
313
+
314
+ Checks for power flags, power input drivers, and power conflicts.
315
+ """
316
+
317
+ # Common power net names
318
+ POWER_NET_NAMES = {
319
+ "VCC", "VDD", "V+", "+5V", "+3V3", "+12V", "+24V",
320
+ "GND", "GNDA", "GNDD", "VSS", "V-",
321
+ }
322
+
323
+ def validate(self) -> List[ERCViolation]:
324
+ """Validate power connections.
325
+
326
+ Returns:
327
+ List of power violations
328
+ """
329
+ violations: List[ERCViolation] = []
330
+
331
+ violations.extend(self.validate_power_flags())
332
+ violations.extend(self.check_power_continuity())
333
+
334
+ return violations
335
+
336
+ def validate_power_flags(self) -> List[ERCViolation]:
337
+ """Check for missing PWR_FLAG on power nets.
338
+
339
+ Returns:
340
+ List of missing power flag violations
341
+ """
342
+ violations: List[ERCViolation] = []
343
+
344
+ # TODO: Implement power net detection and PWR_FLAG checking
345
+ # This requires:
346
+ # 1. Identify power nets (by name or power input pins)
347
+ # 2. Check for PWR_FLAG symbol or power output on net
348
+ # 3. Generate WARNING (not ERROR per requirements) if missing
349
+
350
+ return violations
351
+
352
+ def check_power_continuity(self) -> List[ERCViolation]:
353
+ """Check that power inputs are driven by power outputs.
354
+
355
+ Returns:
356
+ List of power continuity violations
357
+ """
358
+ violations: List[ERCViolation] = []
359
+
360
+ # TODO: Implement power driver checking
361
+ # This requires full net tracing with pin type detection
362
+
363
+ return violations
364
+
365
+ def is_power_net(self, net_name: str) -> bool:
366
+ """Check if net name suggests it's a power net.
367
+
368
+ Args:
369
+ net_name: Net name to check
370
+
371
+ Returns:
372
+ True if likely a power net
373
+ """
374
+ if not net_name:
375
+ return False
376
+
377
+ net_upper = net_name.upper().strip()
378
+
379
+ # Check against known power names
380
+ if net_upper in self.POWER_NET_NAMES:
381
+ return True
382
+
383
+ # Check for common patterns
384
+ if any(pattern in net_upper for pattern in ["VCC", "VDD", "GND", "VSS"]):
385
+ return True
386
+
387
+ # Check for voltage patterns (+5V, +3.3V, etc.)
388
+ if re.match(r'^\+?\d+\.?\d*V$', net_upper):
389
+ return True
390
+
391
+ return False
@@ -0,0 +1,14 @@
1
+ """
2
+ Wrapper classes for schematic elements.
3
+
4
+ Provides enhanced element access with validation, parent tracking,
5
+ and automatic change notification.
6
+ """
7
+
8
+ from .base import ElementWrapper
9
+ from .wire import WireWrapper
10
+
11
+ __all__ = [
12
+ "ElementWrapper",
13
+ "WireWrapper",
14
+ ]
@@ -0,0 +1,89 @@
1
+ """Base wrapper class for schematic elements."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar
5
+
6
+ if TYPE_CHECKING:
7
+ from ..collections.base import IndexedCollection
8
+
9
+ # Type variable for the wrapped data type
10
+ T = TypeVar("T")
11
+
12
+
13
+ class ElementWrapper(ABC, Generic[T]):
14
+ """Base class for all schematic element wrappers.
15
+
16
+ Wrappers enhance raw dataclasses with:
17
+ - Validation on property setters
18
+ - Parent collection tracking for automatic index updates
19
+ - Convenient methods and computed properties
20
+ - Consistent API across different element types
21
+ """
22
+
23
+ def __init__(self, data: T, parent_collection: Optional["IndexedCollection[Any]"]):
24
+ """Initialize the wrapper.
25
+
26
+ Args:
27
+ data: The underlying dataclass instance
28
+ parent_collection: The collection this element belongs to (can be None)
29
+ """
30
+ self._data = data
31
+ self._collection = parent_collection
32
+
33
+ @property
34
+ def data(self) -> T:
35
+ """Get the underlying data object.
36
+
37
+ Returns:
38
+ The wrapped dataclass instance
39
+ """
40
+ return self._data
41
+
42
+ @property
43
+ @abstractmethod
44
+ def uuid(self) -> str:
45
+ """Get the UUID of the element.
46
+
47
+ Returns:
48
+ UUID string
49
+ """
50
+ pass
51
+
52
+ def __eq__(self, other: object) -> bool:
53
+ """Compare wrappers by UUID.
54
+
55
+ Args:
56
+ other: Another wrapper to compare with
57
+
58
+ Returns:
59
+ True if UUIDs match
60
+ """
61
+ if not isinstance(other, ElementWrapper):
62
+ return False
63
+ return self.uuid == other.uuid
64
+
65
+ def __hash__(self) -> int:
66
+ """Hash wrapper by UUID.
67
+
68
+ Returns:
69
+ Hash of UUID
70
+ """
71
+ return hash(self.uuid)
72
+
73
+ def __repr__(self) -> str:
74
+ """Get string representation of wrapper.
75
+
76
+ Returns:
77
+ String representation
78
+ """
79
+ return f"{self.__class__.__name__}({self._data})"
80
+
81
+ def _mark_modified(self) -> None:
82
+ """Mark the parent collection as modified."""
83
+ if self._collection is not None:
84
+ self._collection._mark_modified()
85
+
86
+ def _invalidate_indexes(self) -> None:
87
+ """Invalidate parent collection indexes."""
88
+ if self._collection is not None:
89
+ self._collection._dirty_indexes = True
@@ -0,0 +1,198 @@
1
+ """Wire wrapper class for enhanced wire manipulation."""
2
+
3
+ from typing import TYPE_CHECKING, List, Optional
4
+
5
+ from ..core.types import Point, Wire, WireType
6
+ from .base import ElementWrapper
7
+
8
+ if TYPE_CHECKING:
9
+ from ..collections.wires import WireCollection
10
+
11
+
12
+ class WireWrapper(ElementWrapper[Wire]):
13
+ """Enhanced wrapper for Wire with validation and parent tracking.
14
+
15
+ Provides:
16
+ - Validation on property setters (e.g., minimum 2 points for wires)
17
+ - Automatic parent collection notification on changes
18
+ - Convenient access to wire properties
19
+ - Type-safe operations
20
+ """
21
+
22
+ def __init__(self, wire: Wire, parent_collection: Optional["WireCollection"] = None):
23
+ """Initialize wire wrapper.
24
+
25
+ Args:
26
+ wire: The underlying Wire dataclass
27
+ parent_collection: Parent collection for modification tracking (optional)
28
+ """
29
+ super().__init__(wire, parent_collection)
30
+
31
+ @property
32
+ def uuid(self) -> str:
33
+ """Get wire UUID.
34
+
35
+ Returns:
36
+ Wire UUID string
37
+ """
38
+ return self._data.uuid
39
+
40
+ @property
41
+ def points(self) -> List[Point]:
42
+ """Get wire points.
43
+
44
+ Returns:
45
+ List of Point objects defining the wire path
46
+ """
47
+ return self._data.points
48
+
49
+ @points.setter
50
+ def points(self, value: List[Point]) -> None:
51
+ """Set wire points with validation.
52
+
53
+ Args:
54
+ value: List of points (must have at least 2 points)
55
+
56
+ Raises:
57
+ ValueError: If less than 2 points provided
58
+ """
59
+ if len(value) < 2:
60
+ raise ValueError("Wire must have at least 2 points")
61
+
62
+ # Create new Wire with updated points
63
+ self._data = Wire(
64
+ uuid=self._data.uuid,
65
+ points=value,
66
+ wire_type=self._data.wire_type,
67
+ stroke_width=self._data.stroke_width,
68
+ stroke_type=self._data.stroke_type,
69
+ )
70
+ self._mark_modified()
71
+
72
+ @property
73
+ def start(self) -> Point:
74
+ """Get start point (first point of wire).
75
+
76
+ Returns:
77
+ First point in the wire path
78
+ """
79
+ return self._data.points[0]
80
+
81
+ @property
82
+ def end(self) -> Point:
83
+ """Get end point (last point of wire).
84
+
85
+ Returns:
86
+ Last point in the wire path
87
+ """
88
+ return self._data.points[-1]
89
+
90
+ @property
91
+ def wire_type(self) -> WireType:
92
+ """Get wire type (WIRE or BUS).
93
+
94
+ Returns:
95
+ WireType enum value
96
+ """
97
+ return self._data.wire_type
98
+
99
+ @wire_type.setter
100
+ def wire_type(self, value: WireType) -> None:
101
+ """Set wire type.
102
+
103
+ Args:
104
+ value: WireType enum value (WIRE or BUS)
105
+ """
106
+ self._data = Wire(
107
+ uuid=self._data.uuid,
108
+ points=self._data.points,
109
+ wire_type=value,
110
+ stroke_width=self._data.stroke_width,
111
+ stroke_type=self._data.stroke_type,
112
+ )
113
+ self._mark_modified()
114
+
115
+ @property
116
+ def stroke_width(self) -> float:
117
+ """Get stroke width.
118
+
119
+ Returns:
120
+ Stroke width in mm
121
+ """
122
+ return self._data.stroke_width
123
+
124
+ @stroke_width.setter
125
+ def stroke_width(self, value: float) -> None:
126
+ """Set stroke width.
127
+
128
+ Args:
129
+ value: Stroke width in mm
130
+ """
131
+ self._data = Wire(
132
+ uuid=self._data.uuid,
133
+ points=self._data.points,
134
+ wire_type=self._data.wire_type,
135
+ stroke_width=value,
136
+ stroke_type=self._data.stroke_type,
137
+ )
138
+ self._mark_modified()
139
+
140
+ @property
141
+ def stroke_type(self) -> str:
142
+ """Get stroke type.
143
+
144
+ Returns:
145
+ Stroke type string
146
+ """
147
+ return self._data.stroke_type
148
+
149
+ @stroke_type.setter
150
+ def stroke_type(self, value: str) -> None:
151
+ """Set stroke type.
152
+
153
+ Args:
154
+ value: Stroke type string
155
+ """
156
+ self._data = Wire(
157
+ uuid=self._data.uuid,
158
+ points=self._data.points,
159
+ wire_type=self._data.wire_type,
160
+ stroke_width=self._data.stroke_width,
161
+ stroke_type=value,
162
+ )
163
+ self._mark_modified()
164
+
165
+ # Delegate methods to underlying Wire dataclass
166
+
167
+ @property
168
+ def length(self) -> float:
169
+ """Get total wire length.
170
+
171
+ Returns:
172
+ Total length of all wire segments in mm
173
+ """
174
+ return self._data.length
175
+
176
+ def is_simple(self) -> bool:
177
+ """Check if wire is a simple 2-point wire.
178
+
179
+ Returns:
180
+ True if wire has exactly 2 points, False otherwise
181
+ """
182
+ return self._data.is_simple()
183
+
184
+ def is_horizontal(self) -> bool:
185
+ """Check if wire is horizontal (delegates to Wire).
186
+
187
+ Returns:
188
+ True if wire is horizontal
189
+ """
190
+ return self._data.is_horizontal()
191
+
192
+ def is_vertical(self) -> bool:
193
+ """Check if wire is vertical (delegates to Wire).
194
+
195
+ Returns:
196
+ True if wire is vertical
197
+ """
198
+ return self._data.is_vertical()