kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.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/core/components.py +63 -0
- kicad_sch_api/core/formatter.py +56 -11
- kicad_sch_api/core/ic_manager.py +187 -0
- kicad_sch_api/core/junctions.py +206 -0
- kicad_sch_api/core/parser.py +606 -26
- kicad_sch_api/core/schematic.py +739 -8
- kicad_sch_api/core/types.py +102 -7
- kicad_sch_api/core/wires.py +248 -0
- kicad_sch_api/library/cache.py +321 -10
- kicad_sch_api/utils/validation.py +1 -1
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/METADATA +13 -17
- kicad_sch_api-0.1.0.dist-info/RECORD +21 -0
- kicad_sch_api-0.0.2.dist-info/RECORD +0 -18
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/types.py
CHANGED
|
@@ -178,10 +178,10 @@ class Wire:
|
|
|
178
178
|
"""Wire connection in schematic."""
|
|
179
179
|
|
|
180
180
|
uuid: str
|
|
181
|
-
|
|
182
|
-
end: Point
|
|
181
|
+
points: List[Point] # Support for multi-point wires
|
|
183
182
|
wire_type: WireType = WireType.WIRE
|
|
184
183
|
stroke_width: float = 0.0
|
|
184
|
+
stroke_type: str = "default"
|
|
185
185
|
|
|
186
186
|
def __post_init__(self):
|
|
187
187
|
if not self.uuid:
|
|
@@ -190,18 +190,48 @@ class Wire:
|
|
|
190
190
|
self.wire_type = (
|
|
191
191
|
WireType(self.wire_type) if isinstance(self.wire_type, str) else self.wire_type
|
|
192
192
|
)
|
|
193
|
+
|
|
194
|
+
# Ensure we have at least 2 points
|
|
195
|
+
if len(self.points) < 2:
|
|
196
|
+
raise ValueError("Wire must have at least 2 points")
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs) -> "Wire":
|
|
200
|
+
"""Create wire from start and end points (convenience method)."""
|
|
201
|
+
return cls(uuid=uuid, points=[start, end], **kwargs)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def start(self) -> Point:
|
|
205
|
+
"""First point of the wire."""
|
|
206
|
+
return self.points[0]
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def end(self) -> Point:
|
|
210
|
+
"""Last point of the wire."""
|
|
211
|
+
return self.points[-1]
|
|
193
212
|
|
|
194
213
|
@property
|
|
195
214
|
def length(self) -> float:
|
|
196
|
-
"""
|
|
197
|
-
|
|
215
|
+
"""Total wire length (sum of all segments)."""
|
|
216
|
+
total = 0.0
|
|
217
|
+
for i in range(len(self.points) - 1):
|
|
218
|
+
total += self.points[i].distance_to(self.points[i + 1])
|
|
219
|
+
return total
|
|
220
|
+
|
|
221
|
+
def is_simple(self) -> bool:
|
|
222
|
+
"""Check if wire is a simple 2-point wire."""
|
|
223
|
+
return len(self.points) == 2
|
|
198
224
|
|
|
199
225
|
def is_horizontal(self) -> bool:
|
|
200
|
-
"""Check if wire is horizontal."""
|
|
226
|
+
"""Check if wire is horizontal (only for simple wires)."""
|
|
227
|
+
if not self.is_simple():
|
|
228
|
+
return False
|
|
201
229
|
return abs(self.start.y - self.end.y) < 0.001
|
|
202
230
|
|
|
203
231
|
def is_vertical(self) -> bool:
|
|
204
|
-
"""Check if wire is vertical."""
|
|
232
|
+
"""Check if wire is vertical (only for simple wires)."""
|
|
233
|
+
if not self.is_simple():
|
|
234
|
+
return False
|
|
205
235
|
return abs(self.start.x - self.end.x) < 0.001
|
|
206
236
|
|
|
207
237
|
|
|
@@ -211,7 +241,8 @@ class Junction:
|
|
|
211
241
|
|
|
212
242
|
uuid: str
|
|
213
243
|
position: Point
|
|
214
|
-
diameter: float =
|
|
244
|
+
diameter: float = 0 # KiCAD default diameter
|
|
245
|
+
color: Tuple[int, int, int, int] = (0, 0, 0, 0) # RGBA color
|
|
215
246
|
|
|
216
247
|
def __post_init__(self):
|
|
217
248
|
if not self.uuid:
|
|
@@ -226,6 +257,17 @@ class LabelType(Enum):
|
|
|
226
257
|
HIERARCHICAL = "hierarchical_label"
|
|
227
258
|
|
|
228
259
|
|
|
260
|
+
class HierarchicalLabelShape(Enum):
|
|
261
|
+
"""Hierarchical label shapes/directions."""
|
|
262
|
+
|
|
263
|
+
INPUT = "input"
|
|
264
|
+
OUTPUT = "output"
|
|
265
|
+
BIDIRECTIONAL = "bidirectional"
|
|
266
|
+
TRISTATE = "tri_state"
|
|
267
|
+
PASSIVE = "passive"
|
|
268
|
+
UNSPECIFIED = "unspecified"
|
|
269
|
+
|
|
270
|
+
|
|
229
271
|
@dataclass
|
|
230
272
|
class Label:
|
|
231
273
|
"""Text label in schematic."""
|
|
@@ -236,6 +278,7 @@ class Label:
|
|
|
236
278
|
label_type: LabelType = LabelType.LOCAL
|
|
237
279
|
rotation: float = 0.0
|
|
238
280
|
size: float = 1.27
|
|
281
|
+
shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
|
|
239
282
|
|
|
240
283
|
def __post_init__(self):
|
|
241
284
|
if not self.uuid:
|
|
@@ -244,6 +287,50 @@ class Label:
|
|
|
244
287
|
self.label_type = (
|
|
245
288
|
LabelType(self.label_type) if isinstance(self.label_type, str) else self.label_type
|
|
246
289
|
)
|
|
290
|
+
|
|
291
|
+
if self.shape:
|
|
292
|
+
self.shape = (
|
|
293
|
+
HierarchicalLabelShape(self.shape) if isinstance(self.shape, str) else self.shape
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class Text:
|
|
299
|
+
"""Free text element in schematic."""
|
|
300
|
+
|
|
301
|
+
uuid: str
|
|
302
|
+
position: Point
|
|
303
|
+
text: str
|
|
304
|
+
rotation: float = 0.0
|
|
305
|
+
size: float = 1.27
|
|
306
|
+
exclude_from_sim: bool = False
|
|
307
|
+
|
|
308
|
+
def __post_init__(self):
|
|
309
|
+
if not self.uuid:
|
|
310
|
+
self.uuid = str(uuid4())
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass
|
|
314
|
+
class TextBox:
|
|
315
|
+
"""Text box element with border in schematic."""
|
|
316
|
+
|
|
317
|
+
uuid: str
|
|
318
|
+
position: Point
|
|
319
|
+
size: Point # Width, height
|
|
320
|
+
text: str
|
|
321
|
+
rotation: float = 0.0
|
|
322
|
+
font_size: float = 1.27
|
|
323
|
+
margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525) # top, right, bottom, left
|
|
324
|
+
stroke_width: float = 0.0
|
|
325
|
+
stroke_type: str = "solid"
|
|
326
|
+
fill_type: str = "none"
|
|
327
|
+
justify_horizontal: str = "left"
|
|
328
|
+
justify_vertical: str = "top"
|
|
329
|
+
exclude_from_sim: bool = False
|
|
330
|
+
|
|
331
|
+
def __post_init__(self):
|
|
332
|
+
if not self.uuid:
|
|
333
|
+
self.uuid = str(uuid4())
|
|
247
334
|
|
|
248
335
|
|
|
249
336
|
@dataclass
|
|
@@ -278,6 +365,14 @@ class Sheet:
|
|
|
278
365
|
name: str
|
|
279
366
|
filename: str
|
|
280
367
|
pins: List["SheetPin"] = field(default_factory=list)
|
|
368
|
+
exclude_from_sim: bool = False
|
|
369
|
+
in_bom: bool = True
|
|
370
|
+
on_board: bool = True
|
|
371
|
+
dnp: bool = False
|
|
372
|
+
fields_autoplaced: bool = True
|
|
373
|
+
stroke_width: float = 0.1524
|
|
374
|
+
stroke_type: str = "solid"
|
|
375
|
+
fill_color: Tuple[float, float, float, float] = (0, 0, 0, 0.0)
|
|
281
376
|
|
|
282
377
|
def __post_init__(self):
|
|
283
378
|
if not self.uuid:
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wire collection and management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides enhanced wire management with performance optimization,
|
|
5
|
+
bulk operations, and professional validation features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid as uuid_module
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from .types import Point, Wire, WireType
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WireCollection:
|
|
18
|
+
"""
|
|
19
|
+
Professional wire collection with enhanced management features.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Fast UUID-based lookup and indexing
|
|
23
|
+
- Bulk operations for performance
|
|
24
|
+
- Multi-point wire support
|
|
25
|
+
- Validation and conflict detection
|
|
26
|
+
- Junction management integration
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, wires: Optional[List[Wire]] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize wire collection.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
wires: Initial list of wires
|
|
35
|
+
"""
|
|
36
|
+
self._wires: List[Wire] = wires or []
|
|
37
|
+
self._uuid_index: Dict[str, int] = {}
|
|
38
|
+
self._modified = False
|
|
39
|
+
|
|
40
|
+
# Build UUID index
|
|
41
|
+
self._rebuild_index()
|
|
42
|
+
|
|
43
|
+
logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
|
|
44
|
+
|
|
45
|
+
def _rebuild_index(self):
|
|
46
|
+
"""Rebuild UUID index for fast lookups."""
|
|
47
|
+
self._uuid_index = {wire.uuid: i for i, wire in enumerate(self._wires)}
|
|
48
|
+
|
|
49
|
+
def __len__(self) -> int:
|
|
50
|
+
"""Number of wires in collection."""
|
|
51
|
+
return len(self._wires)
|
|
52
|
+
|
|
53
|
+
def __iter__(self):
|
|
54
|
+
"""Iterate over wires."""
|
|
55
|
+
return iter(self._wires)
|
|
56
|
+
|
|
57
|
+
def __getitem__(self, uuid: str) -> Wire:
|
|
58
|
+
"""Get wire by UUID."""
|
|
59
|
+
if uuid not in self._uuid_index:
|
|
60
|
+
raise KeyError(f"Wire with UUID '{uuid}' not found")
|
|
61
|
+
return self._wires[self._uuid_index[uuid]]
|
|
62
|
+
|
|
63
|
+
def add(
|
|
64
|
+
self,
|
|
65
|
+
start: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
66
|
+
end: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
67
|
+
points: Optional[List[Union[Point, Tuple[float, float]]]] = None,
|
|
68
|
+
wire_type: WireType = WireType.WIRE,
|
|
69
|
+
stroke_width: float = 0.0,
|
|
70
|
+
uuid: Optional[str] = None
|
|
71
|
+
) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Add a wire to the collection.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
start: Start point (for simple wires)
|
|
77
|
+
end: End point (for simple wires)
|
|
78
|
+
points: List of points (for multi-point wires)
|
|
79
|
+
wire_type: Wire type (wire or bus)
|
|
80
|
+
stroke_width: Line width
|
|
81
|
+
uuid: Optional UUID (auto-generated if not provided)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
UUID of the created wire
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If neither start/end nor points are provided
|
|
88
|
+
"""
|
|
89
|
+
# Generate UUID if not provided
|
|
90
|
+
if uuid is None:
|
|
91
|
+
uuid = str(uuid_module.uuid4())
|
|
92
|
+
elif uuid in self._uuid_index:
|
|
93
|
+
raise ValueError(f"Wire with UUID '{uuid}' already exists")
|
|
94
|
+
|
|
95
|
+
# Convert points
|
|
96
|
+
wire_points = []
|
|
97
|
+
if points:
|
|
98
|
+
# Multi-point wire
|
|
99
|
+
for point in points:
|
|
100
|
+
if isinstance(point, tuple):
|
|
101
|
+
wire_points.append(Point(point[0], point[1]))
|
|
102
|
+
else:
|
|
103
|
+
wire_points.append(point)
|
|
104
|
+
elif start is not None and end is not None:
|
|
105
|
+
# Simple 2-point wire
|
|
106
|
+
if isinstance(start, tuple):
|
|
107
|
+
start = Point(start[0], start[1])
|
|
108
|
+
if isinstance(end, tuple):
|
|
109
|
+
end = Point(end[0], end[1])
|
|
110
|
+
wire_points = [start, end]
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("Must provide either start/end points or points list")
|
|
113
|
+
|
|
114
|
+
# Create wire
|
|
115
|
+
wire = Wire(
|
|
116
|
+
uuid=uuid,
|
|
117
|
+
points=wire_points,
|
|
118
|
+
wire_type=wire_type,
|
|
119
|
+
stroke_width=stroke_width
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Add to collection
|
|
123
|
+
self._wires.append(wire)
|
|
124
|
+
self._uuid_index[uuid] = len(self._wires) - 1
|
|
125
|
+
self._modified = True
|
|
126
|
+
|
|
127
|
+
logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
|
|
128
|
+
return uuid
|
|
129
|
+
|
|
130
|
+
def remove(self, uuid: str) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Remove wire by UUID.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
uuid: Wire UUID to remove
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if wire was removed, False if not found
|
|
139
|
+
"""
|
|
140
|
+
if uuid not in self._uuid_index:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
index = self._uuid_index[uuid]
|
|
144
|
+
del self._wires[index]
|
|
145
|
+
self._rebuild_index()
|
|
146
|
+
self._modified = True
|
|
147
|
+
|
|
148
|
+
logger.debug(f"Removed wire: {uuid}")
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def get_by_point(self, point: Union[Point, Tuple[float, float]], tolerance: float = 0.1) -> List[Wire]:
|
|
152
|
+
"""
|
|
153
|
+
Find wires that pass through or near a point.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
point: Point to search near
|
|
157
|
+
tolerance: Distance tolerance
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of wires near the point
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(point, tuple):
|
|
163
|
+
point = Point(point[0], point[1])
|
|
164
|
+
|
|
165
|
+
matching_wires = []
|
|
166
|
+
for wire in self._wires:
|
|
167
|
+
# Check if any wire point is close
|
|
168
|
+
for wire_point in wire.points:
|
|
169
|
+
if wire_point.distance_to(point) <= tolerance:
|
|
170
|
+
matching_wires.append(wire)
|
|
171
|
+
break
|
|
172
|
+
else:
|
|
173
|
+
# Check if point lies on any wire segment
|
|
174
|
+
for i in range(len(wire.points) - 1):
|
|
175
|
+
if self._point_on_segment(point, wire.points[i], wire.points[i + 1], tolerance):
|
|
176
|
+
matching_wires.append(wire)
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
return matching_wires
|
|
180
|
+
|
|
181
|
+
def _point_on_segment(self, point: Point, seg_start: Point, seg_end: Point, tolerance: float) -> bool:
|
|
182
|
+
"""Check if point lies on line segment within tolerance."""
|
|
183
|
+
# Vector from seg_start to seg_end
|
|
184
|
+
seg_vec = Point(seg_end.x - seg_start.x, seg_end.y - seg_start.y)
|
|
185
|
+
seg_length = seg_start.distance_to(seg_end)
|
|
186
|
+
|
|
187
|
+
if seg_length < 0.001: # Very short segment
|
|
188
|
+
return seg_start.distance_to(point) <= tolerance
|
|
189
|
+
|
|
190
|
+
# Vector from seg_start to point
|
|
191
|
+
point_vec = Point(point.x - seg_start.x, point.y - seg_start.y)
|
|
192
|
+
|
|
193
|
+
# Project point onto segment
|
|
194
|
+
dot_product = (point_vec.x * seg_vec.x + point_vec.y * seg_vec.y)
|
|
195
|
+
projection = dot_product / (seg_length * seg_length)
|
|
196
|
+
|
|
197
|
+
# Check if projection is within segment bounds
|
|
198
|
+
if projection < 0 or projection > 1:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
# Calculate distance from point to line
|
|
202
|
+
proj_point = Point(
|
|
203
|
+
seg_start.x + projection * seg_vec.x,
|
|
204
|
+
seg_start.y + projection * seg_vec.y
|
|
205
|
+
)
|
|
206
|
+
distance = point.distance_to(proj_point)
|
|
207
|
+
|
|
208
|
+
return distance <= tolerance
|
|
209
|
+
|
|
210
|
+
def get_horizontal_wires(self) -> List[Wire]:
|
|
211
|
+
"""Get all horizontal wires."""
|
|
212
|
+
return [wire for wire in self._wires if wire.is_horizontal()]
|
|
213
|
+
|
|
214
|
+
def get_vertical_wires(self) -> List[Wire]:
|
|
215
|
+
"""Get all vertical wires."""
|
|
216
|
+
return [wire for wire in self._wires if wire.is_vertical()]
|
|
217
|
+
|
|
218
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
219
|
+
"""Get wire collection statistics."""
|
|
220
|
+
total_length = sum(wire.length for wire in self._wires)
|
|
221
|
+
simple_wires = sum(1 for wire in self._wires if wire.is_simple())
|
|
222
|
+
multi_point_wires = len(self._wires) - simple_wires
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"total_wires": len(self._wires),
|
|
226
|
+
"simple_wires": simple_wires,
|
|
227
|
+
"multi_point_wires": multi_point_wires,
|
|
228
|
+
"total_length": total_length,
|
|
229
|
+
"avg_length": total_length / len(self._wires) if self._wires else 0,
|
|
230
|
+
"horizontal_wires": len(self.get_horizontal_wires()),
|
|
231
|
+
"vertical_wires": len(self.get_vertical_wires())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def clear(self):
|
|
235
|
+
"""Remove all wires from collection."""
|
|
236
|
+
self._wires.clear()
|
|
237
|
+
self._uuid_index.clear()
|
|
238
|
+
self._modified = True
|
|
239
|
+
logger.debug("Cleared all wires")
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def modified(self) -> bool:
|
|
243
|
+
"""Check if collection has been modified."""
|
|
244
|
+
return self._modified
|
|
245
|
+
|
|
246
|
+
def mark_saved(self):
|
|
247
|
+
"""Mark collection as saved (reset modified flag)."""
|
|
248
|
+
self._modified = False
|