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.
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +294 -0
- kicad_sch_api/collections/components.py +434 -0
- kicad_sch_api/collections/junctions.py +366 -0
- kicad_sch_api/collections/labels.py +404 -0
- kicad_sch_api/collections/wires.py +406 -0
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +243 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +268 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +474 -0
- kicad_sch_api/core/managers/wire.py +346 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +276 -0
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +904 -1074
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +13 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +56 -43
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/symbol_parser.py +222 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
- kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
- kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|