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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- 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/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -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/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced wire management with IndexRegistry integration.
|
|
3
|
+
|
|
4
|
+
Provides WireCollection using BaseCollection infrastructure with
|
|
5
|
+
endpoint indexing and wire geometry queries.
|
|
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 BaseCollection, IndexSpec, ValidationLevel
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WireCollection(BaseCollection[Wire]):
|
|
19
|
+
"""
|
|
20
|
+
Wire collection with endpoint indexing and geometry queries.
|
|
21
|
+
|
|
22
|
+
Inherits from BaseCollection for UUID indexing and adds wire-specific
|
|
23
|
+
functionality for endpoint queries and wire type filtering.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Fast UUID lookup via IndexRegistry
|
|
27
|
+
- Multi-point wire support
|
|
28
|
+
- Endpoint-based queries
|
|
29
|
+
- Horizontal/vertical wire detection
|
|
30
|
+
- Lazy index rebuilding
|
|
31
|
+
- Batch mode support
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
wires: Optional[List[Wire]] = None,
|
|
37
|
+
validation_level: ValidationLevel = ValidationLevel.NORMAL,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize wire collection.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
wires: Initial list of wires
|
|
44
|
+
validation_level: Validation level for operations
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(validation_level=validation_level)
|
|
47
|
+
|
|
48
|
+
# Add initial wires
|
|
49
|
+
if wires:
|
|
50
|
+
with self.batch_mode():
|
|
51
|
+
for wire in wires:
|
|
52
|
+
super().add(wire)
|
|
53
|
+
|
|
54
|
+
logger.debug(f"WireCollection initialized with {len(self)} wires")
|
|
55
|
+
|
|
56
|
+
# BaseCollection abstract method implementations
|
|
57
|
+
def _get_item_uuid(self, item: Wire) -> str:
|
|
58
|
+
"""Extract UUID from wire."""
|
|
59
|
+
return item.uuid
|
|
60
|
+
|
|
61
|
+
def _create_item(self, **kwargs) -> Wire:
|
|
62
|
+
"""Create a new wire (not typically used directly)."""
|
|
63
|
+
raise NotImplementedError("Use add() method to create wires")
|
|
64
|
+
|
|
65
|
+
def _get_index_specs(self) -> List[IndexSpec]:
|
|
66
|
+
"""Get index specifications for wire collection."""
|
|
67
|
+
return [
|
|
68
|
+
IndexSpec(
|
|
69
|
+
name="uuid",
|
|
70
|
+
key_func=lambda w: w.uuid,
|
|
71
|
+
unique=True,
|
|
72
|
+
description="UUID index for fast lookups",
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Wire-specific add method
|
|
77
|
+
def add(
|
|
78
|
+
self,
|
|
79
|
+
start: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
80
|
+
end: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
81
|
+
points: Optional[List[Union[Point, Tuple[float, float]]]] = None,
|
|
82
|
+
wire_type: WireType = WireType.WIRE,
|
|
83
|
+
stroke_width: float = 0.0,
|
|
84
|
+
uuid: Optional[str] = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Add a wire to the collection.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
start: Start point (for simple wires)
|
|
91
|
+
end: End point (for simple wires)
|
|
92
|
+
points: List of points (for multi-point wires)
|
|
93
|
+
wire_type: Wire type (wire or bus)
|
|
94
|
+
stroke_width: Line width
|
|
95
|
+
uuid: Optional UUID (auto-generated if not provided)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
UUID of the created wire
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If neither start/end nor points are provided
|
|
102
|
+
ValueError: If UUID already exists
|
|
103
|
+
"""
|
|
104
|
+
# Generate UUID if not provided
|
|
105
|
+
if uuid is None:
|
|
106
|
+
uuid = str(uuid_module.uuid4())
|
|
107
|
+
else:
|
|
108
|
+
# Check for duplicate
|
|
109
|
+
self._ensure_indexes_current()
|
|
110
|
+
if self._index_registry.has_key("uuid", uuid):
|
|
111
|
+
raise ValueError(f"Wire with UUID '{uuid}' already exists")
|
|
112
|
+
|
|
113
|
+
# Convert points
|
|
114
|
+
wire_points = []
|
|
115
|
+
if points:
|
|
116
|
+
# Multi-point wire
|
|
117
|
+
for point in points:
|
|
118
|
+
if isinstance(point, tuple):
|
|
119
|
+
wire_points.append(Point(point[0], point[1]))
|
|
120
|
+
else:
|
|
121
|
+
wire_points.append(point)
|
|
122
|
+
elif start is not None and end is not None:
|
|
123
|
+
# Simple 2-point wire
|
|
124
|
+
if isinstance(start, tuple):
|
|
125
|
+
start = Point(start[0], start[1])
|
|
126
|
+
if isinstance(end, tuple):
|
|
127
|
+
end = Point(end[0], end[1])
|
|
128
|
+
wire_points = [start, end]
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError("Must provide either start/end points or points list")
|
|
131
|
+
|
|
132
|
+
# Validate wire has at least 2 points
|
|
133
|
+
if len(wire_points) < 2:
|
|
134
|
+
raise ValueError("Wire must have at least 2 points")
|
|
135
|
+
|
|
136
|
+
# Create wire
|
|
137
|
+
wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
|
|
138
|
+
|
|
139
|
+
# Add to collection
|
|
140
|
+
super().add(wire)
|
|
141
|
+
|
|
142
|
+
logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
|
|
143
|
+
return uuid
|
|
144
|
+
|
|
145
|
+
# Endpoint-based queries
|
|
146
|
+
def get_by_endpoint(
|
|
147
|
+
self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
148
|
+
) -> List[Wire]:
|
|
149
|
+
"""
|
|
150
|
+
Find all wires with an endpoint near a given point.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
point: Point to search for
|
|
154
|
+
tolerance: Distance tolerance for matching
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of wires with endpoint near the point
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(point, tuple):
|
|
160
|
+
point = Point(point[0], point[1])
|
|
161
|
+
|
|
162
|
+
matching_wires = []
|
|
163
|
+
for wire in self._items:
|
|
164
|
+
# Check first and last point (endpoints)
|
|
165
|
+
if (wire.points[0].distance_to(point) <= tolerance or
|
|
166
|
+
wire.points[-1].distance_to(point) <= tolerance):
|
|
167
|
+
matching_wires.append(wire)
|
|
168
|
+
|
|
169
|
+
return matching_wires
|
|
170
|
+
|
|
171
|
+
def get_at_point(
|
|
172
|
+
self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
173
|
+
) -> List[Wire]:
|
|
174
|
+
"""
|
|
175
|
+
Find all wires that pass through or near a point.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
point: Point to search for
|
|
179
|
+
tolerance: Distance tolerance for matching
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of wires passing through the point
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(point, tuple):
|
|
185
|
+
point = Point(point[0], point[1])
|
|
186
|
+
|
|
187
|
+
matching_wires = []
|
|
188
|
+
for wire in self._items:
|
|
189
|
+
# Check if any point in wire is near the search point
|
|
190
|
+
for wire_point in wire.points:
|
|
191
|
+
if wire_point.distance_to(point) <= tolerance:
|
|
192
|
+
matching_wires.append(wire)
|
|
193
|
+
break # Found match, move to next wire
|
|
194
|
+
|
|
195
|
+
return matching_wires
|
|
196
|
+
|
|
197
|
+
# Wire geometry queries
|
|
198
|
+
def get_horizontal(self) -> List[Wire]:
|
|
199
|
+
"""
|
|
200
|
+
Get all horizontal wires (Y coordinates equal).
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of horizontal wires
|
|
204
|
+
"""
|
|
205
|
+
horizontal = []
|
|
206
|
+
for wire in self._items:
|
|
207
|
+
if len(wire.points) == 2:
|
|
208
|
+
# Simple 2-point wire
|
|
209
|
+
if abs(wire.points[0].y - wire.points[1].y) < 0.01:
|
|
210
|
+
horizontal.append(wire)
|
|
211
|
+
|
|
212
|
+
return horizontal
|
|
213
|
+
|
|
214
|
+
def get_vertical(self) -> List[Wire]:
|
|
215
|
+
"""
|
|
216
|
+
Get all vertical wires (X coordinates equal).
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of vertical wires
|
|
220
|
+
"""
|
|
221
|
+
vertical = []
|
|
222
|
+
for wire in self._items:
|
|
223
|
+
if len(wire.points) == 2:
|
|
224
|
+
# Simple 2-point wire
|
|
225
|
+
if abs(wire.points[0].x - wire.points[1].x) < 0.01:
|
|
226
|
+
vertical.append(wire)
|
|
227
|
+
|
|
228
|
+
return vertical
|
|
229
|
+
|
|
230
|
+
def get_by_type(self, wire_type: WireType) -> List[Wire]:
|
|
231
|
+
"""
|
|
232
|
+
Get all wires of a specific type.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
wire_type: Wire type to filter by
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of wires matching the type
|
|
239
|
+
"""
|
|
240
|
+
return [w for w in self._items if w.wire_type == wire_type]
|
|
241
|
+
|
|
242
|
+
# Statistics
|
|
243
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Get wire collection statistics.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Dictionary with wire statistics
|
|
249
|
+
"""
|
|
250
|
+
if not self._items:
|
|
251
|
+
base_stats = super().get_statistics()
|
|
252
|
+
base_stats.update({
|
|
253
|
+
"total_wires": 0,
|
|
254
|
+
"total_segments": 0,
|
|
255
|
+
"wire_count": 0,
|
|
256
|
+
"bus_count": 0,
|
|
257
|
+
"horizontal_count": 0,
|
|
258
|
+
"vertical_count": 0,
|
|
259
|
+
"avg_points_per_wire": 0,
|
|
260
|
+
})
|
|
261
|
+
return base_stats
|
|
262
|
+
|
|
263
|
+
wire_count = sum(1 for w in self._items if w.wire_type == WireType.WIRE)
|
|
264
|
+
bus_count = sum(1 for w in self._items if w.wire_type == WireType.BUS)
|
|
265
|
+
total_segments = sum(len(w.points) - 1 for w in self._items)
|
|
266
|
+
avg_points = sum(len(w.points) for w in self._items) / len(self._items)
|
|
267
|
+
|
|
268
|
+
horizontal = len(self.get_horizontal())
|
|
269
|
+
vertical = len(self.get_vertical())
|
|
270
|
+
|
|
271
|
+
base_stats = super().get_statistics()
|
|
272
|
+
base_stats.update({
|
|
273
|
+
"total_wires": len(self._items),
|
|
274
|
+
"total_segments": total_segments,
|
|
275
|
+
"wire_count": wire_count,
|
|
276
|
+
"bus_count": bus_count,
|
|
277
|
+
"horizontal_count": horizontal,
|
|
278
|
+
"vertical_count": vertical,
|
|
279
|
+
"avg_points_per_wire": avg_points,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return base_stats
|
|
283
|
+
|
|
284
|
+
# Compatibility methods
|
|
285
|
+
@property
|
|
286
|
+
def modified(self) -> bool:
|
|
287
|
+
"""Check if collection has been modified (compatibility)."""
|
|
288
|
+
return self.is_modified
|
|
289
|
+
|
|
290
|
+
def mark_saved(self) -> None:
|
|
291
|
+
"""Mark collection as saved (reset modified flag)."""
|
|
292
|
+
self.mark_clean()
|
kicad_sch_api/core/__init__.py
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
"""Core kicad-sch-api functionality."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from ..collections import Component, ComponentCollection
|
|
4
4
|
from .formatter import ExactFormatter
|
|
5
5
|
from .parser import SExpressionParser
|
|
6
6
|
from .schematic import Schematic, create_schematic, load_schematic
|
|
7
|
-
from .types import Junction, Label, Net, Point, SchematicSymbol, Wire
|
|
7
|
+
from .types import Junction, Label, Net, PinInfo, Point, SchematicSymbol, Wire
|
|
8
|
+
# Exception hierarchy
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
KiCadSchError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
ReferenceError,
|
|
13
|
+
LibraryError,
|
|
14
|
+
GeometryError,
|
|
15
|
+
NetError,
|
|
16
|
+
ParseError,
|
|
17
|
+
FormatError,
|
|
18
|
+
CollectionError,
|
|
19
|
+
ElementNotFoundError,
|
|
20
|
+
DuplicateElementError,
|
|
21
|
+
CollectionOperationError,
|
|
22
|
+
FileOperationError,
|
|
23
|
+
CLIError,
|
|
24
|
+
SchematicStateError,
|
|
25
|
+
)
|
|
8
26
|
|
|
9
27
|
__all__ = [
|
|
10
28
|
"Schematic",
|
|
@@ -16,8 +34,25 @@ __all__ = [
|
|
|
16
34
|
"Junction",
|
|
17
35
|
"Label",
|
|
18
36
|
"Net",
|
|
37
|
+
"PinInfo",
|
|
19
38
|
"SExpressionParser",
|
|
20
39
|
"ExactFormatter",
|
|
21
40
|
"load_schematic",
|
|
22
41
|
"create_schematic",
|
|
42
|
+
# Exceptions
|
|
43
|
+
"KiCadSchError",
|
|
44
|
+
"ValidationError",
|
|
45
|
+
"ReferenceError",
|
|
46
|
+
"LibraryError",
|
|
47
|
+
"GeometryError",
|
|
48
|
+
"NetError",
|
|
49
|
+
"ParseError",
|
|
50
|
+
"FormatError",
|
|
51
|
+
"CollectionError",
|
|
52
|
+
"ElementNotFoundError",
|
|
53
|
+
"DuplicateElementError",
|
|
54
|
+
"CollectionOperationError",
|
|
55
|
+
"FileOperationError",
|
|
56
|
+
"CLIError",
|
|
57
|
+
"SchematicStateError",
|
|
23
58
|
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base collection class for schematic elements.
|
|
3
|
+
|
|
4
|
+
Provides common functionality for all collection types including UUID indexing,
|
|
5
|
+
modification tracking, and standard collection operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Protocol, TypeVar
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HasUUID(Protocol):
|
|
15
|
+
"""Protocol for objects that have a UUID attribute."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def uuid(self) -> str:
|
|
19
|
+
"""UUID of the object."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T", bound=HasUUID)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseCollection(Generic[T]):
|
|
27
|
+
"""
|
|
28
|
+
Generic base class for schematic element collections.
|
|
29
|
+
|
|
30
|
+
Provides common functionality:
|
|
31
|
+
- UUID-based indexing for fast lookup
|
|
32
|
+
- Modification tracking
|
|
33
|
+
- Standard collection operations (__len__, __iter__, __getitem__)
|
|
34
|
+
- Index rebuilding and management
|
|
35
|
+
|
|
36
|
+
Type parameter T must implement the HasUUID protocol.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, items: Optional[List[T]] = None, collection_name: str = "items") -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialize base collection.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
items: Initial list of items
|
|
45
|
+
collection_name: Name for logging (e.g., "wires", "junctions")
|
|
46
|
+
"""
|
|
47
|
+
self._items: List[T] = items or []
|
|
48
|
+
self._uuid_index: Dict[str, int] = {}
|
|
49
|
+
self._modified = False
|
|
50
|
+
self._collection_name = collection_name
|
|
51
|
+
|
|
52
|
+
# Build UUID index
|
|
53
|
+
self._rebuild_index()
|
|
54
|
+
|
|
55
|
+
logger.debug(f"{collection_name} collection initialized with {len(self._items)} items")
|
|
56
|
+
|
|
57
|
+
def _rebuild_index(self) -> None:
|
|
58
|
+
"""Rebuild UUID index for fast lookups."""
|
|
59
|
+
self._uuid_index = {item.uuid: i for i, item in enumerate(self._items)}
|
|
60
|
+
|
|
61
|
+
def _mark_modified(self) -> None:
|
|
62
|
+
"""Mark collection as modified."""
|
|
63
|
+
self._modified = True
|
|
64
|
+
|
|
65
|
+
def is_modified(self) -> bool:
|
|
66
|
+
"""Check if collection has been modified."""
|
|
67
|
+
return self._modified
|
|
68
|
+
|
|
69
|
+
def reset_modified_flag(self) -> None:
|
|
70
|
+
"""Reset modified flag (typically after save)."""
|
|
71
|
+
self._modified = False
|
|
72
|
+
|
|
73
|
+
# Standard collection protocol methods
|
|
74
|
+
def __len__(self) -> int:
|
|
75
|
+
"""Return number of items in collection."""
|
|
76
|
+
return len(self._items)
|
|
77
|
+
|
|
78
|
+
def __iter__(self) -> Iterator[T]:
|
|
79
|
+
"""Iterate over items in collection."""
|
|
80
|
+
return iter(self._items)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key: Any) -> T:
|
|
83
|
+
"""
|
|
84
|
+
Get item by UUID or index.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
key: UUID string or integer index
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Item at the specified location
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
KeyError: If UUID not found
|
|
94
|
+
IndexError: If index out of range
|
|
95
|
+
TypeError: If key is neither string nor int
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(key, str):
|
|
98
|
+
# UUID lookup
|
|
99
|
+
if key not in self._uuid_index:
|
|
100
|
+
raise KeyError(f"Item with UUID '{key}' not found")
|
|
101
|
+
return self._items[self._uuid_index[key]]
|
|
102
|
+
elif isinstance(key, int):
|
|
103
|
+
# Index lookup
|
|
104
|
+
return self._items[key]
|
|
105
|
+
else:
|
|
106
|
+
raise TypeError(f"Key must be string (UUID) or int (index), got {type(key)}")
|
|
107
|
+
|
|
108
|
+
def __contains__(self, key: Any) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check if item exists in collection.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
key: UUID string or item object
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if item exists
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(key, str):
|
|
119
|
+
return key in self._uuid_index
|
|
120
|
+
elif hasattr(key, "uuid"):
|
|
121
|
+
return key.uuid in self._uuid_index
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def get(self, uuid: str) -> Optional[T]:
|
|
125
|
+
"""
|
|
126
|
+
Get item by UUID.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
uuid: Item UUID
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Item if found, None otherwise
|
|
133
|
+
"""
|
|
134
|
+
if uuid not in self._uuid_index:
|
|
135
|
+
return None
|
|
136
|
+
return self._items[self._uuid_index[uuid]]
|
|
137
|
+
|
|
138
|
+
def remove(self, uuid: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Remove item by UUID.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
uuid: UUID of item to remove
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if item was removed, False if not found
|
|
147
|
+
"""
|
|
148
|
+
if uuid not in self._uuid_index:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
index = self._uuid_index[uuid]
|
|
152
|
+
del self._items[index]
|
|
153
|
+
self._rebuild_index()
|
|
154
|
+
self._mark_modified()
|
|
155
|
+
|
|
156
|
+
logger.debug(f"Removed item with UUID {uuid} from {self._collection_name}")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def clear(self) -> None:
|
|
160
|
+
"""Remove all items from collection."""
|
|
161
|
+
self._items.clear()
|
|
162
|
+
self._uuid_index.clear()
|
|
163
|
+
self._mark_modified()
|
|
164
|
+
logger.debug(f"Cleared all items from {self._collection_name}")
|
|
165
|
+
|
|
166
|
+
def find(self, predicate: Callable[[T], bool]) -> List[T]:
|
|
167
|
+
"""
|
|
168
|
+
Find all items matching a predicate.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
predicate: Function that returns True for matching items
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of matching items
|
|
175
|
+
"""
|
|
176
|
+
return [item for item in self._items if predicate(item)]
|
|
177
|
+
|
|
178
|
+
def filter(self, **criteria) -> List[T]:
|
|
179
|
+
"""
|
|
180
|
+
Filter items by attribute values.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
**criteria: Attribute name/value pairs to match
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of matching items
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
collection.filter(wire_type=WireType.BUS, stroke_width=0.5)
|
|
190
|
+
"""
|
|
191
|
+
matches = []
|
|
192
|
+
for item in self._items:
|
|
193
|
+
if all(getattr(item, key, None) == value for key, value in criteria.items()):
|
|
194
|
+
matches.append(item)
|
|
195
|
+
return matches
|
|
196
|
+
|
|
197
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Get collection statistics.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dictionary with statistics
|
|
203
|
+
"""
|
|
204
|
+
return {
|
|
205
|
+
"total_items": len(self._items),
|
|
206
|
+
"modified": self._modified,
|
|
207
|
+
"indexed_items": len(self._uuid_index),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
def _add_item(self, item: T) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Add item to internal storage and index.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
item: Item to add
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If item UUID already exists
|
|
219
|
+
"""
|
|
220
|
+
if item.uuid in self._uuid_index:
|
|
221
|
+
raise ValueError(f"Item with UUID '{item.uuid}' already exists")
|
|
222
|
+
|
|
223
|
+
self._items.append(item)
|
|
224
|
+
self._uuid_index[item.uuid] = len(self._items) - 1
|
|
225
|
+
self._mark_modified()
|
|
226
|
+
|
|
227
|
+
def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
|
|
228
|
+
"""
|
|
229
|
+
Update multiple items matching criteria.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
criteria: Attribute name/value pairs to match
|
|
233
|
+
updates: Attribute name/value pairs to update
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Number of items updated
|
|
237
|
+
"""
|
|
238
|
+
matching_items = self.filter(**criteria)
|
|
239
|
+
for item in matching_items:
|
|
240
|
+
for key, value in updates.items():
|
|
241
|
+
if hasattr(item, key):
|
|
242
|
+
setattr(item, key, value)
|
|
243
|
+
|
|
244
|
+
if matching_items:
|
|
245
|
+
self._mark_modified()
|
|
246
|
+
|
|
247
|
+
logger.debug(f"Bulk updated {len(matching_items)} items in {self._collection_name}")
|
|
248
|
+
return len(matching_items)
|
|
@@ -382,16 +382,43 @@ def get_component_bounding_box(
|
|
|
382
382
|
# Calculate symbol bounding box
|
|
383
383
|
symbol_bbox = SymbolBoundingBoxCalculator.calculate_bounding_box(symbol, include_properties)
|
|
384
384
|
|
|
385
|
-
# Transform to world coordinates
|
|
386
|
-
#
|
|
385
|
+
# Transform to world coordinates with rotation
|
|
386
|
+
# Apply rotation matrix to bounding box corners, then find new min/max
|
|
387
|
+
import math
|
|
388
|
+
|
|
389
|
+
angle_rad = math.radians(component.rotation)
|
|
390
|
+
cos_a = math.cos(angle_rad)
|
|
391
|
+
sin_a = math.sin(angle_rad)
|
|
392
|
+
|
|
393
|
+
# Get all 4 corners of the symbol bounding box
|
|
394
|
+
corners = [
|
|
395
|
+
(symbol_bbox.min_x, symbol_bbox.min_y), # Bottom-left
|
|
396
|
+
(symbol_bbox.max_x, symbol_bbox.min_y), # Bottom-right
|
|
397
|
+
(symbol_bbox.max_x, symbol_bbox.max_y), # Top-right
|
|
398
|
+
(symbol_bbox.min_x, symbol_bbox.max_y), # Top-left
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
# Rotate each corner using standard 2D rotation matrix
|
|
402
|
+
rotated_corners = []
|
|
403
|
+
for x, y in corners:
|
|
404
|
+
rotated_x = x * cos_a - y * sin_a
|
|
405
|
+
rotated_y = x * sin_a + y * cos_a
|
|
406
|
+
rotated_corners.append((rotated_x, rotated_y))
|
|
407
|
+
|
|
408
|
+
# Find min/max of rotated corners
|
|
409
|
+
rotated_xs = [x for x, y in rotated_corners]
|
|
410
|
+
rotated_ys = [y for x, y in rotated_corners]
|
|
411
|
+
|
|
387
412
|
world_bbox = BoundingBox(
|
|
388
|
-
component.position.x +
|
|
389
|
-
component.position.y +
|
|
390
|
-
component.position.x +
|
|
391
|
-
component.position.y +
|
|
413
|
+
component.position.x + min(rotated_xs),
|
|
414
|
+
component.position.y + min(rotated_ys),
|
|
415
|
+
component.position.x + max(rotated_xs),
|
|
416
|
+
component.position.y + max(rotated_ys),
|
|
392
417
|
)
|
|
393
418
|
|
|
394
|
-
logger.debug(
|
|
419
|
+
logger.debug(
|
|
420
|
+
f"Component {component.reference} at {component.rotation}° world bbox: {world_bbox}"
|
|
421
|
+
)
|
|
395
422
|
return world_bbox
|
|
396
423
|
|
|
397
424
|
|