kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.5__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 (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,407 @@
1
+ """
2
+ Wire collection with specialized indexing and wire-specific operations.
3
+
4
+ Extends the base IndexedCollection to provide wire-specific features like
5
+ endpoint indexing, multi-point wire support, and connectivity management.
6
+ """
7
+
8
+ import logging
9
+ import uuid as uuid_module
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ..core.types import Point, Wire, WireType
13
+ from .base import IndexedCollection
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class WireCollection(IndexedCollection[Wire]):
19
+ """
20
+ Professional wire collection with enhanced management features.
21
+
22
+ Extends IndexedCollection with wire-specific features:
23
+ - Endpoint indexing for connectivity analysis
24
+ - Multi-point wire support
25
+ - Wire type classification (normal, bus, etc.)
26
+ - Bulk operations for performance
27
+ - Junction management integration
28
+ """
29
+
30
+ def __init__(self, wires: Optional[List[Wire]] = None):
31
+ """
32
+ Initialize wire collection.
33
+
34
+ Args:
35
+ wires: Initial list of wires
36
+ """
37
+ self._endpoint_index: Dict[Tuple[float, float], List[Wire]] = {}
38
+ self._type_index: Dict[WireType, List[Wire]] = {}
39
+
40
+ super().__init__(wires)
41
+
42
+ # Abstract method implementations
43
+ def _get_item_uuid(self, item: Wire) -> str:
44
+ """Extract UUID from wire."""
45
+ return item.uuid
46
+
47
+ def _create_item(self, **kwargs) -> Wire:
48
+ """Create a new wire with given parameters."""
49
+ # This will be called by add() methods
50
+ raise NotImplementedError("Use add() method instead")
51
+
52
+ def _build_additional_indexes(self) -> None:
53
+ """Build wire-specific indexes."""
54
+ # Clear existing indexes
55
+ self._endpoint_index.clear()
56
+ self._type_index.clear()
57
+
58
+ # Rebuild indexes from current items
59
+ for wire in self._items:
60
+ # Endpoint index - index by all endpoints
61
+ for point in wire.points:
62
+ endpoint = (point.x, point.y)
63
+ if endpoint not in self._endpoint_index:
64
+ self._endpoint_index[endpoint] = []
65
+ self._endpoint_index[endpoint].append(wire)
66
+
67
+ # Type index
68
+ wire_type = getattr(wire, 'wire_type', WireType.WIRE)
69
+ if wire_type not in self._type_index:
70
+ self._type_index[wire_type] = []
71
+ self._type_index[wire_type].append(wire)
72
+
73
+ # Wire-specific methods
74
+ def add(
75
+ self,
76
+ start: Union[Point, Tuple[float, float]],
77
+ end: Union[Point, Tuple[float, float]],
78
+ wire_type: WireType = WireType.WIRE,
79
+ stroke_width: float = 0.0,
80
+ stroke_type: str = "default",
81
+ wire_uuid: Optional[str] = None,
82
+ ) -> Wire:
83
+ """
84
+ Add a new wire to the collection.
85
+
86
+ Args:
87
+ start: Starting point of the wire
88
+ end: Ending point of the wire
89
+ wire_type: Type of wire (normal, bus, etc.)
90
+ stroke_width: Wire stroke width
91
+ stroke_type: Wire stroke type
92
+ wire_uuid: Specific UUID for wire (auto-generated if None)
93
+
94
+ Returns:
95
+ Newly created Wire
96
+ """
97
+ # Convert tuples to Points if needed
98
+ if isinstance(start, tuple):
99
+ start = Point(start[0], start[1])
100
+ if isinstance(end, tuple):
101
+ end = Point(end[0], end[1])
102
+
103
+ # Validate wire points
104
+ if start == end:
105
+ raise ValueError("Wire start and end points cannot be the same")
106
+
107
+ # Generate UUID if not provided
108
+ if wire_uuid is None:
109
+ wire_uuid = str(uuid_module.uuid4())
110
+
111
+ # Create wire
112
+ wire = Wire(
113
+ uuid=wire_uuid,
114
+ points=[start, end],
115
+ wire_type=wire_type,
116
+ stroke_width=stroke_width,
117
+ stroke_type=stroke_type
118
+ )
119
+
120
+ # Add to collection using base class method
121
+ return super().add(wire)
122
+
123
+ def add_multi_point(
124
+ self,
125
+ points: List[Union[Point, Tuple[float, float]]],
126
+ wire_type: WireType = WireType.WIRE,
127
+ stroke_width: float = 0.0,
128
+ stroke_type: str = "default",
129
+ wire_uuid: Optional[str] = None,
130
+ ) -> Wire:
131
+ """
132
+ Add a multi-point wire to the collection.
133
+
134
+ Args:
135
+ points: List of points defining the wire path
136
+ wire_type: Type of wire (normal, bus, etc.)
137
+ stroke_width: Wire stroke width
138
+ stroke_type: Wire stroke type
139
+ wire_uuid: Specific UUID for wire (auto-generated if None)
140
+
141
+ Returns:
142
+ Newly created Wire
143
+
144
+ Raises:
145
+ ValueError: If less than 2 points provided
146
+ """
147
+ if len(points) < 2:
148
+ raise ValueError("Wire must have at least 2 points")
149
+
150
+ # Convert tuples to Points if needed
151
+ converted_points = []
152
+ for point in points:
153
+ if isinstance(point, tuple):
154
+ converted_points.append(Point(point[0], point[1]))
155
+ else:
156
+ converted_points.append(point)
157
+
158
+ # Generate UUID if not provided
159
+ if wire_uuid is None:
160
+ wire_uuid = str(uuid_module.uuid4())
161
+
162
+ # Create wire
163
+ wire = Wire(
164
+ uuid=wire_uuid,
165
+ points=converted_points,
166
+ wire_type=wire_type,
167
+ stroke_width=stroke_width,
168
+ stroke_type=stroke_type
169
+ )
170
+
171
+ # Add to collection using base class method
172
+ return super().add(wire)
173
+
174
+ def get_wires_at_point(self, point: Union[Point, Tuple[float, float]]) -> List[Wire]:
175
+ """
176
+ Get all wires that pass through a specific point.
177
+
178
+ Args:
179
+ point: Point to search at
180
+
181
+ Returns:
182
+ List of wires passing through the point
183
+ """
184
+ self._ensure_indexes_current()
185
+
186
+ if isinstance(point, Point):
187
+ endpoint = (point.x, point.y)
188
+ else:
189
+ endpoint = point
190
+
191
+ return self._endpoint_index.get(endpoint, []).copy()
192
+
193
+ def get_wires_by_type(self, wire_type: WireType) -> List[Wire]:
194
+ """
195
+ Get all wires of a specific type.
196
+
197
+ Args:
198
+ wire_type: Type of wires to find
199
+
200
+ Returns:
201
+ List of wires of the specified type
202
+ """
203
+ self._ensure_indexes_current()
204
+ return self._type_index.get(wire_type, []).copy()
205
+
206
+ def get_connected_wires(self, wire: Wire) -> List[Wire]:
207
+ """
208
+ Get all wires connected to the given wire.
209
+
210
+ Args:
211
+ wire: Wire to find connections for
212
+
213
+ Returns:
214
+ List of connected wires (excluding the input wire)
215
+ """
216
+ connected = set()
217
+
218
+ # Check connections at all endpoints
219
+ for point in wire.points:
220
+ endpoint_wires = self.get_wires_at_point(point)
221
+ for endpoint_wire in endpoint_wires:
222
+ if endpoint_wire.uuid != wire.uuid:
223
+ connected.add(endpoint_wire)
224
+
225
+ return list(connected)
226
+
227
+ def find_wire_networks(self) -> List[List[Wire]]:
228
+ """
229
+ Find all connected wire networks in the collection.
230
+
231
+ Returns:
232
+ List of wire networks, each network is a list of connected wires
233
+ """
234
+ visited = set()
235
+ networks = []
236
+
237
+ for wire in self._items:
238
+ if wire.uuid in visited:
239
+ continue
240
+
241
+ # Start a new network with this wire
242
+ network = []
243
+ to_visit = [wire]
244
+
245
+ while to_visit:
246
+ current_wire = to_visit.pop()
247
+ if current_wire.uuid in visited:
248
+ continue
249
+
250
+ visited.add(current_wire.uuid)
251
+ network.append(current_wire)
252
+
253
+ # Add all connected wires to visit list
254
+ connected = self.get_connected_wires(current_wire)
255
+ for connected_wire in connected:
256
+ if connected_wire.uuid not in visited:
257
+ to_visit.append(connected_wire)
258
+
259
+ if network:
260
+ networks.append(network)
261
+
262
+ return networks
263
+
264
+ def modify_wire_path(
265
+ self,
266
+ wire_uuid: str,
267
+ new_points: List[Union[Point, Tuple[float, float]]]
268
+ ) -> bool:
269
+ """
270
+ Modify the path of an existing wire.
271
+
272
+ Args:
273
+ wire_uuid: UUID of wire to modify
274
+ new_points: New points for the wire path
275
+
276
+ Returns:
277
+ True if wire was modified, False if not found
278
+
279
+ Raises:
280
+ ValueError: If less than 2 points provided
281
+ """
282
+ if len(new_points) < 2:
283
+ raise ValueError("Wire must have at least 2 points")
284
+
285
+ wire = self.get(wire_uuid)
286
+ if not wire:
287
+ return False
288
+
289
+ # Convert tuples to Points if needed
290
+ converted_points = []
291
+ for point in new_points:
292
+ if isinstance(point, tuple):
293
+ converted_points.append(Point(point[0], point[1]))
294
+ else:
295
+ converted_points.append(point)
296
+
297
+ # Update wire points
298
+ wire.points = converted_points
299
+ self._mark_modified()
300
+ self._mark_indexes_dirty()
301
+
302
+ logger.debug(f"Modified wire {wire_uuid} path with {len(new_points)} points")
303
+ return True
304
+
305
+ # Bulk operations for performance
306
+ def remove_wires_at_point(self, point: Union[Point, Tuple[float, float]]) -> int:
307
+ """
308
+ Remove all wires passing through a specific point.
309
+
310
+ Args:
311
+ point: Point where wires should be removed
312
+
313
+ Returns:
314
+ Number of wires removed
315
+ """
316
+ wires_to_remove = self.get_wires_at_point(point)
317
+
318
+ for wire in wires_to_remove:
319
+ self.remove(wire.uuid)
320
+
321
+ logger.info(f"Removed {len(wires_to_remove)} wires at point {point}")
322
+ return len(wires_to_remove)
323
+
324
+ def bulk_update_stroke(
325
+ self,
326
+ wire_type: Optional[WireType] = None,
327
+ stroke_width: Optional[float] = None,
328
+ stroke_type: Optional[str] = None
329
+ ) -> int:
330
+ """
331
+ Bulk update stroke properties for wires.
332
+
333
+ Args:
334
+ wire_type: Filter by wire type (None for all)
335
+ stroke_width: New stroke width (None to keep existing)
336
+ stroke_type: New stroke type (None to keep existing)
337
+
338
+ Returns:
339
+ Number of wires updated
340
+ """
341
+ # Get wires to update
342
+ if wire_type is not None:
343
+ wires_to_update = self.get_wires_by_type(wire_type)
344
+ else:
345
+ wires_to_update = list(self._items)
346
+
347
+ # Apply updates
348
+ for wire in wires_to_update:
349
+ if stroke_width is not None:
350
+ wire.stroke_width = stroke_width
351
+ if stroke_type is not None:
352
+ wire.stroke_type = stroke_type
353
+
354
+ if wires_to_update:
355
+ self._mark_modified()
356
+
357
+ logger.info(f"Bulk updated stroke properties for {len(wires_to_update)} wires")
358
+ return len(wires_to_update)
359
+
360
+ # Collection statistics
361
+ def get_connection_statistics(self) -> Dict[str, Any]:
362
+ """
363
+ Get connection statistics for the wire collection.
364
+
365
+ Returns:
366
+ Dictionary with connection statistics
367
+ """
368
+ stats = super().get_statistics()
369
+
370
+ # Add wire-specific statistics
371
+ stats.update({
372
+ "endpoint_count": len(self._endpoint_index),
373
+ "wire_types": {
374
+ wire_type.value: len(wires)
375
+ for wire_type, wires in self._type_index.items()
376
+ },
377
+ "networks": len(self.find_wire_networks()),
378
+ "total_length": sum(self._calculate_wire_length(wire) for wire in self._items)
379
+ })
380
+
381
+ return stats
382
+
383
+ def _calculate_wire_length(self, wire: Wire) -> float:
384
+ """
385
+ Calculate the total length of a wire.
386
+
387
+ Args:
388
+ wire: Wire to calculate length for
389
+
390
+ Returns:
391
+ Total wire length
392
+ """
393
+ if len(wire.points) < 2:
394
+ return 0.0
395
+
396
+ total_length = 0.0
397
+ for i in range(len(wire.points) - 1):
398
+ start_point = wire.points[i]
399
+ end_point = wire.points[i + 1]
400
+
401
+ dx = end_point.x - start_point.x
402
+ dy = end_point.y - start_point.y
403
+ segment_length = (dx ** 2 + dy ** 2) ** 0.5
404
+
405
+ total_length += segment_length
406
+
407
+ return total_length
@@ -146,6 +146,9 @@ class ExactFormatter:
146
146
  self.rules["embedded_fonts"] = FormatRule(inline=True)
147
147
  self.rules["page"] = FormatRule(inline=True, quote_indices={1})
148
148
 
149
+ # Image element
150
+ self.rules["image"] = FormatRule(inline=False, custom_handler=self._format_image)
151
+
149
152
  def format(self, data: Any) -> str:
150
153
  """
151
154
  Format S-expression data with exact KiCAD formatting.
@@ -511,6 +514,34 @@ class ExactFormatter:
511
514
  result += f"\n{indent})"
512
515
  return result
513
516
 
517
+ def _format_image(self, lst: List[Any], indent_level: int) -> str:
518
+ """Format image elements with base64 data split across lines."""
519
+ indent = "\t" * indent_level
520
+ next_indent = "\t" * (indent_level + 1)
521
+
522
+ result = f"({lst[0]}"
523
+
524
+ # Process each element
525
+ for element in lst[1:]:
526
+ if isinstance(element, list):
527
+ tag = str(element[0]) if element else ""
528
+ if tag == "data":
529
+ # Special handling for data element
530
+ # First chunk on same line as (data, rest on subsequent lines
531
+ if len(element) > 1:
532
+ result += f'\n{next_indent}({element[0]} "{element[1]}"'
533
+ for chunk in element[2:]:
534
+ result += f'\n{next_indent}\t"{chunk}"'
535
+ result += f"\n{next_indent})"
536
+ else:
537
+ result += f"\n{next_indent}({element[0]})"
538
+ else:
539
+ # Regular element formatting
540
+ result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
541
+
542
+ result += f"\n{indent})"
543
+ return result
544
+
514
545
 
515
546
  class CompactFormatter(ExactFormatter):
516
547
  """Compact formatter for minimal output size."""