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

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,366 @@
1
+ """
2
+ Junction collection with specialized indexing and junction-specific operations.
3
+
4
+ Extends the base IndexedCollection to provide junction-specific features like
5
+ position-based queries and connectivity analysis support.
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 Junction, Point
13
+ from .base import IndexedCollection
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class JunctionCollection(IndexedCollection[Junction]):
19
+ """
20
+ Professional junction collection with enhanced management features.
21
+
22
+ Extends IndexedCollection with junction-specific features:
23
+ - Position-based indexing for fast spatial queries
24
+ - Duplicate position detection
25
+ - Connectivity analysis support
26
+ - Grid alignment validation
27
+ """
28
+
29
+ def __init__(self, junctions: Optional[List[Junction]] = None):
30
+ """
31
+ Initialize junction collection.
32
+
33
+ Args:
34
+ junctions: Initial list of junctions
35
+ """
36
+ self._position_index: Dict[Tuple[float, float], Junction] = {}
37
+
38
+ super().__init__(junctions)
39
+
40
+ # Abstract method implementations
41
+ def _get_item_uuid(self, item: Junction) -> str:
42
+ """Extract UUID from junction."""
43
+ return item.uuid
44
+
45
+ def _create_item(self, **kwargs) -> Junction:
46
+ """Create a new junction with given parameters."""
47
+ # This will be called by add() methods
48
+ raise NotImplementedError("Use add() method instead")
49
+
50
+ def _build_additional_indexes(self) -> None:
51
+ """Build junction-specific indexes."""
52
+ # Clear existing indexes
53
+ self._position_index.clear()
54
+
55
+ # Rebuild indexes from current items
56
+ for junction in self._items:
57
+ # Position index - junctions should be unique per position
58
+ pos_key = (junction.position.x, junction.position.y)
59
+ if pos_key in self._position_index:
60
+ logger.warning(f"Duplicate junction at position {pos_key}")
61
+ self._position_index[pos_key] = junction
62
+
63
+ # Junction-specific methods
64
+ def add(
65
+ self,
66
+ position: Union[Point, Tuple[float, float]],
67
+ diameter: float = 1.27,
68
+ junction_uuid: Optional[str] = None,
69
+ ) -> Junction:
70
+ """
71
+ Add a new junction to the collection.
72
+
73
+ Args:
74
+ position: Junction position
75
+ diameter: Junction diameter (default KiCAD standard)
76
+ junction_uuid: Specific UUID for junction (auto-generated if None)
77
+
78
+ Returns:
79
+ Newly created Junction
80
+
81
+ Raises:
82
+ ValueError: If junction already exists at position
83
+ """
84
+ # Convert tuple to Point if needed
85
+ if isinstance(position, tuple):
86
+ position = Point(position[0], position[1])
87
+
88
+ # Check for existing junction at position
89
+ pos_key = (position.x, position.y)
90
+ if pos_key in self._position_index:
91
+ existing = self._position_index[pos_key]
92
+ raise ValueError(
93
+ f"Junction already exists at position {position} (UUID: {existing.uuid})"
94
+ )
95
+
96
+ # Generate UUID if not provided
97
+ if junction_uuid is None:
98
+ junction_uuid = str(uuid_module.uuid4())
99
+
100
+ # Create junction
101
+ junction = Junction(uuid=junction_uuid, position=position, diameter=diameter)
102
+
103
+ # Add to collection using base class method
104
+ return super().add(junction)
105
+
106
+ def get_junction_at_position(
107
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
108
+ ) -> Optional[Junction]:
109
+ """
110
+ Get junction at a specific position.
111
+
112
+ Args:
113
+ position: Position to search at
114
+ tolerance: Position tolerance for matching
115
+
116
+ Returns:
117
+ Junction if found, None otherwise
118
+ """
119
+ self._ensure_indexes_current()
120
+
121
+ if isinstance(position, Point):
122
+ pos_key = (position.x, position.y)
123
+ else:
124
+ pos_key = position
125
+
126
+ if tolerance == 0.0:
127
+ # Exact match
128
+ return self._position_index.get(pos_key)
129
+ else:
130
+ # Tolerance-based search
131
+ target_x, target_y = pos_key
132
+
133
+ for junction in self._items:
134
+ dx = abs(junction.position.x - target_x)
135
+ dy = abs(junction.position.y - target_y)
136
+ distance = (dx**2 + dy**2) ** 0.5
137
+
138
+ if distance <= tolerance:
139
+ return junction
140
+
141
+ return None
142
+
143
+ def has_junction_at_position(
144
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
145
+ ) -> bool:
146
+ """
147
+ Check if a junction exists at a specific position.
148
+
149
+ Args:
150
+ position: Position to check
151
+ tolerance: Position tolerance for matching
152
+
153
+ Returns:
154
+ True if junction exists at position
155
+ """
156
+ return self.get_junction_at_position(position, tolerance) is not None
157
+
158
+ def find_junctions_in_region(
159
+ self, min_x: float, min_y: float, max_x: float, max_y: float
160
+ ) -> List[Junction]:
161
+ """
162
+ Find all junctions within a rectangular region.
163
+
164
+ Args:
165
+ min_x: Minimum X coordinate
166
+ min_y: Minimum Y coordinate
167
+ max_x: Maximum X coordinate
168
+ max_y: Maximum Y coordinate
169
+
170
+ Returns:
171
+ List of junctions in the region
172
+ """
173
+ matching_junctions = []
174
+
175
+ for junction in self._items:
176
+ if min_x <= junction.position.x <= max_x and min_y <= junction.position.y <= max_y:
177
+ matching_junctions.append(junction)
178
+
179
+ return matching_junctions
180
+
181
+ def update_junction_position(
182
+ self, junction_uuid: str, new_position: Union[Point, Tuple[float, float]]
183
+ ) -> bool:
184
+ """
185
+ Update the position of an existing junction.
186
+
187
+ Args:
188
+ junction_uuid: UUID of junction to update
189
+ new_position: New position
190
+
191
+ Returns:
192
+ True if junction was updated, False if not found
193
+
194
+ Raises:
195
+ ValueError: If another junction exists at new position
196
+ """
197
+ junction = self.get(junction_uuid)
198
+ if not junction:
199
+ return False
200
+
201
+ # Convert tuple to Point if needed
202
+ if isinstance(new_position, tuple):
203
+ new_position = Point(new_position[0], new_position[1])
204
+
205
+ # Check for existing junction at new position
206
+ new_pos_key = (new_position.x, new_position.y)
207
+ if new_pos_key in self._position_index:
208
+ existing = self._position_index[new_pos_key]
209
+ if existing.uuid != junction_uuid:
210
+ raise ValueError(f"Junction already exists at position {new_position}")
211
+
212
+ # Update position
213
+ junction.position = new_position
214
+ self._mark_modified()
215
+ self._mark_indexes_dirty()
216
+
217
+ logger.debug(f"Updated junction {junction_uuid} position to {new_position}")
218
+ return True
219
+
220
+ def update_junction_diameter(self, junction_uuid: str, new_diameter: float) -> bool:
221
+ """
222
+ Update the diameter of an existing junction.
223
+
224
+ Args:
225
+ junction_uuid: UUID of junction to update
226
+ new_diameter: New diameter
227
+
228
+ Returns:
229
+ True if junction was updated, False if not found
230
+
231
+ Raises:
232
+ ValueError: If diameter is not positive
233
+ """
234
+ if new_diameter <= 0:
235
+ raise ValueError("Junction diameter must be positive")
236
+
237
+ junction = self.get(junction_uuid)
238
+ if not junction:
239
+ return False
240
+
241
+ # Update diameter
242
+ junction.diameter = new_diameter
243
+ self._mark_modified()
244
+
245
+ logger.debug(f"Updated junction {junction_uuid} diameter to {new_diameter}")
246
+ return True
247
+
248
+ def get_junction_positions(self) -> List[Point]:
249
+ """
250
+ Get all junction positions.
251
+
252
+ Returns:
253
+ List of all junction positions
254
+ """
255
+ return [junction.position for junction in self._items]
256
+
257
+ def validate_grid_alignment(self, grid_size: float = 1.27) -> List[Junction]:
258
+ """
259
+ Find junctions that are not aligned to the specified grid.
260
+
261
+ Args:
262
+ grid_size: Grid size for alignment check (default KiCAD standard)
263
+
264
+ Returns:
265
+ List of junctions not aligned to grid
266
+ """
267
+ misaligned = []
268
+
269
+ for junction in self._items:
270
+ # Check if position is aligned to grid
271
+ x_remainder = junction.position.x % grid_size
272
+ y_remainder = junction.position.y % grid_size
273
+
274
+ # Allow small tolerance for floating point precision
275
+ tolerance = grid_size * 0.01
276
+ if (x_remainder > tolerance and x_remainder < grid_size - tolerance) or (
277
+ y_remainder > tolerance and y_remainder < grid_size - tolerance
278
+ ):
279
+ misaligned.append(junction)
280
+
281
+ return misaligned
282
+
283
+ def snap_to_grid(self, grid_size: float = 1.27) -> int:
284
+ """
285
+ Snap all junctions to the specified grid.
286
+
287
+ Args:
288
+ grid_size: Grid size for snapping (default KiCAD standard)
289
+
290
+ Returns:
291
+ Number of junctions that were moved
292
+ """
293
+ moved_count = 0
294
+
295
+ for junction in self._items:
296
+ # Calculate grid-aligned position
297
+ aligned_x = round(junction.position.x / grid_size) * grid_size
298
+ aligned_y = round(junction.position.y / grid_size) * grid_size
299
+
300
+ # Check if position needs to change
301
+ if (
302
+ abs(junction.position.x - aligned_x) > 0.001
303
+ or abs(junction.position.y - aligned_y) > 0.001
304
+ ):
305
+
306
+ # Update position
307
+ junction.position = Point(aligned_x, aligned_y)
308
+ moved_count += 1
309
+
310
+ if moved_count > 0:
311
+ self._mark_modified()
312
+ self._mark_indexes_dirty()
313
+
314
+ logger.info(f"Snapped {moved_count} junctions to {grid_size}mm grid")
315
+ return moved_count
316
+
317
+ # Bulk operations
318
+ def remove_junctions_in_region(
319
+ self, min_x: float, min_y: float, max_x: float, max_y: float
320
+ ) -> int:
321
+ """
322
+ Remove all junctions within a rectangular region.
323
+
324
+ Args:
325
+ min_x: Minimum X coordinate
326
+ min_y: Minimum Y coordinate
327
+ max_x: Maximum X coordinate
328
+ max_y: Maximum Y coordinate
329
+
330
+ Returns:
331
+ Number of junctions removed
332
+ """
333
+ junctions_to_remove = self.find_junctions_in_region(min_x, min_y, max_x, max_y)
334
+
335
+ for junction in junctions_to_remove:
336
+ self.remove(junction.uuid)
337
+
338
+ logger.info(f"Removed {len(junctions_to_remove)} junctions in region")
339
+ return len(junctions_to_remove)
340
+
341
+ # Collection statistics
342
+ def get_junction_statistics(self) -> Dict[str, Any]:
343
+ """
344
+ Get junction statistics for the collection.
345
+
346
+ Returns:
347
+ Dictionary with junction statistics
348
+ """
349
+ stats = super().get_statistics()
350
+
351
+ # Calculate diameter statistics
352
+ diameters = [junction.diameter for junction in self._items]
353
+ if diameters:
354
+ stats.update(
355
+ {
356
+ "diameter_stats": {
357
+ "min": min(diameters),
358
+ "max": max(diameters),
359
+ "average": sum(diameters) / len(diameters),
360
+ },
361
+ "grid_aligned": len(self._items) - len(self.validate_grid_alignment()),
362
+ "misaligned": len(self.validate_grid_alignment()),
363
+ }
364
+ )
365
+
366
+ return stats