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,378 @@
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(f"Junction already exists at position {position} (UUID: {existing.uuid})")
93
+
94
+ # Generate UUID if not provided
95
+ if junction_uuid is None:
96
+ junction_uuid = str(uuid_module.uuid4())
97
+
98
+ # Create junction
99
+ junction = Junction(
100
+ uuid=junction_uuid,
101
+ position=position,
102
+ diameter=diameter
103
+ )
104
+
105
+ # Add to collection using base class method
106
+ return super().add(junction)
107
+
108
+ def get_junction_at_position(
109
+ self,
110
+ position: Union[Point, Tuple[float, float]],
111
+ tolerance: float = 0.0
112
+ ) -> Optional[Junction]:
113
+ """
114
+ Get junction at a specific position.
115
+
116
+ Args:
117
+ position: Position to search at
118
+ tolerance: Position tolerance for matching
119
+
120
+ Returns:
121
+ Junction if found, None otherwise
122
+ """
123
+ self._ensure_indexes_current()
124
+
125
+ if isinstance(position, Point):
126
+ pos_key = (position.x, position.y)
127
+ else:
128
+ pos_key = position
129
+
130
+ if tolerance == 0.0:
131
+ # Exact match
132
+ return self._position_index.get(pos_key)
133
+ else:
134
+ # Tolerance-based search
135
+ target_x, target_y = pos_key
136
+
137
+ for junction in self._items:
138
+ dx = abs(junction.position.x - target_x)
139
+ dy = abs(junction.position.y - target_y)
140
+ distance = (dx ** 2 + dy ** 2) ** 0.5
141
+
142
+ if distance <= tolerance:
143
+ return junction
144
+
145
+ return None
146
+
147
+ def has_junction_at_position(
148
+ self,
149
+ position: Union[Point, Tuple[float, float]],
150
+ tolerance: float = 0.0
151
+ ) -> bool:
152
+ """
153
+ Check if a junction exists at a specific position.
154
+
155
+ Args:
156
+ position: Position to check
157
+ tolerance: Position tolerance for matching
158
+
159
+ Returns:
160
+ True if junction exists at position
161
+ """
162
+ return self.get_junction_at_position(position, tolerance) is not None
163
+
164
+ def find_junctions_in_region(
165
+ self,
166
+ min_x: float,
167
+ min_y: float,
168
+ max_x: float,
169
+ max_y: float
170
+ ) -> List[Junction]:
171
+ """
172
+ Find all junctions within a rectangular region.
173
+
174
+ Args:
175
+ min_x: Minimum X coordinate
176
+ min_y: Minimum Y coordinate
177
+ max_x: Maximum X coordinate
178
+ max_y: Maximum Y coordinate
179
+
180
+ Returns:
181
+ List of junctions in the region
182
+ """
183
+ matching_junctions = []
184
+
185
+ for junction in self._items:
186
+ if (min_x <= junction.position.x <= max_x and
187
+ min_y <= junction.position.y <= max_y):
188
+ matching_junctions.append(junction)
189
+
190
+ return matching_junctions
191
+
192
+ def update_junction_position(
193
+ self,
194
+ junction_uuid: str,
195
+ new_position: Union[Point, Tuple[float, float]]
196
+ ) -> bool:
197
+ """
198
+ Update the position of an existing junction.
199
+
200
+ Args:
201
+ junction_uuid: UUID of junction to update
202
+ new_position: New position
203
+
204
+ Returns:
205
+ True if junction was updated, False if not found
206
+
207
+ Raises:
208
+ ValueError: If another junction exists at new position
209
+ """
210
+ junction = self.get(junction_uuid)
211
+ if not junction:
212
+ return False
213
+
214
+ # Convert tuple to Point if needed
215
+ if isinstance(new_position, tuple):
216
+ new_position = Point(new_position[0], new_position[1])
217
+
218
+ # Check for existing junction at new position
219
+ new_pos_key = (new_position.x, new_position.y)
220
+ if new_pos_key in self._position_index:
221
+ existing = self._position_index[new_pos_key]
222
+ if existing.uuid != junction_uuid:
223
+ raise ValueError(f"Junction already exists at position {new_position}")
224
+
225
+ # Update position
226
+ junction.position = new_position
227
+ self._mark_modified()
228
+ self._mark_indexes_dirty()
229
+
230
+ logger.debug(f"Updated junction {junction_uuid} position to {new_position}")
231
+ return True
232
+
233
+ def update_junction_diameter(self, junction_uuid: str, new_diameter: float) -> bool:
234
+ """
235
+ Update the diameter of an existing junction.
236
+
237
+ Args:
238
+ junction_uuid: UUID of junction to update
239
+ new_diameter: New diameter
240
+
241
+ Returns:
242
+ True if junction was updated, False if not found
243
+
244
+ Raises:
245
+ ValueError: If diameter is not positive
246
+ """
247
+ if new_diameter <= 0:
248
+ raise ValueError("Junction diameter must be positive")
249
+
250
+ junction = self.get(junction_uuid)
251
+ if not junction:
252
+ return False
253
+
254
+ # Update diameter
255
+ junction.diameter = new_diameter
256
+ self._mark_modified()
257
+
258
+ logger.debug(f"Updated junction {junction_uuid} diameter to {new_diameter}")
259
+ return True
260
+
261
+ def get_junction_positions(self) -> List[Point]:
262
+ """
263
+ Get all junction positions.
264
+
265
+ Returns:
266
+ List of all junction positions
267
+ """
268
+ return [junction.position for junction in self._items]
269
+
270
+ def validate_grid_alignment(self, grid_size: float = 1.27) -> List[Junction]:
271
+ """
272
+ Find junctions that are not aligned to the specified grid.
273
+
274
+ Args:
275
+ grid_size: Grid size for alignment check (default KiCAD standard)
276
+
277
+ Returns:
278
+ List of junctions not aligned to grid
279
+ """
280
+ misaligned = []
281
+
282
+ for junction in self._items:
283
+ # Check if position is aligned to grid
284
+ x_remainder = junction.position.x % grid_size
285
+ y_remainder = junction.position.y % grid_size
286
+
287
+ # Allow small tolerance for floating point precision
288
+ tolerance = grid_size * 0.01
289
+ if (x_remainder > tolerance and x_remainder < grid_size - tolerance) or \
290
+ (y_remainder > tolerance and y_remainder < grid_size - tolerance):
291
+ misaligned.append(junction)
292
+
293
+ return misaligned
294
+
295
+ def snap_to_grid(self, grid_size: float = 1.27) -> int:
296
+ """
297
+ Snap all junctions to the specified grid.
298
+
299
+ Args:
300
+ grid_size: Grid size for snapping (default KiCAD standard)
301
+
302
+ Returns:
303
+ Number of junctions that were moved
304
+ """
305
+ moved_count = 0
306
+
307
+ for junction in self._items:
308
+ # Calculate grid-aligned position
309
+ aligned_x = round(junction.position.x / grid_size) * grid_size
310
+ aligned_y = round(junction.position.y / grid_size) * grid_size
311
+
312
+ # Check if position needs to change
313
+ if (abs(junction.position.x - aligned_x) > 0.001 or
314
+ abs(junction.position.y - aligned_y) > 0.001):
315
+
316
+ # Update position
317
+ junction.position = Point(aligned_x, aligned_y)
318
+ moved_count += 1
319
+
320
+ if moved_count > 0:
321
+ self._mark_modified()
322
+ self._mark_indexes_dirty()
323
+
324
+ logger.info(f"Snapped {moved_count} junctions to {grid_size}mm grid")
325
+ return moved_count
326
+
327
+ # Bulk operations
328
+ def remove_junctions_in_region(
329
+ self,
330
+ min_x: float,
331
+ min_y: float,
332
+ max_x: float,
333
+ max_y: float
334
+ ) -> int:
335
+ """
336
+ Remove all junctions within a rectangular region.
337
+
338
+ Args:
339
+ min_x: Minimum X coordinate
340
+ min_y: Minimum Y coordinate
341
+ max_x: Maximum X coordinate
342
+ max_y: Maximum Y coordinate
343
+
344
+ Returns:
345
+ Number of junctions removed
346
+ """
347
+ junctions_to_remove = self.find_junctions_in_region(min_x, min_y, max_x, max_y)
348
+
349
+ for junction in junctions_to_remove:
350
+ self.remove(junction.uuid)
351
+
352
+ logger.info(f"Removed {len(junctions_to_remove)} junctions in region")
353
+ return len(junctions_to_remove)
354
+
355
+ # Collection statistics
356
+ def get_junction_statistics(self) -> Dict[str, Any]:
357
+ """
358
+ Get junction statistics for the collection.
359
+
360
+ Returns:
361
+ Dictionary with junction statistics
362
+ """
363
+ stats = super().get_statistics()
364
+
365
+ # Calculate diameter statistics
366
+ diameters = [junction.diameter for junction in self._items]
367
+ if diameters:
368
+ stats.update({
369
+ "diameter_stats": {
370
+ "min": min(diameters),
371
+ "max": max(diameters),
372
+ "average": sum(diameters) / len(diameters)
373
+ },
374
+ "grid_aligned": len(self._items) - len(self.validate_grid_alignment()),
375
+ "misaligned": len(self.validate_grid_alignment())
376
+ })
377
+
378
+ return stats