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.

@@ -178,10 +178,10 @@ class Wire:
178
178
  """Wire connection in schematic."""
179
179
 
180
180
  uuid: str
181
- start: Point
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
- """Wire length."""
197
- return self.start.distance_to(self.end)
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 = 1.27 # Standard junction diameter
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