kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.4__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/__init__.py CHANGED
@@ -42,7 +42,7 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.3.2"
45
+ __version__ = "0.3.3"
46
46
  __author__ = "Circuit-Synth"
47
47
  __email__ = "info@circuit-synth.com"
48
48
 
@@ -55,7 +55,7 @@ from .library.cache import SymbolLibraryCache, get_symbol_cache
55
55
  from .utils.validation import ValidationError, ValidationIssue
56
56
 
57
57
  # Version info
58
- VERSION_INFO = (0, 3, 2)
58
+ VERSION_INFO = (0, 3, 3)
59
59
 
60
60
  # Public API
61
61
  __all__ = [
@@ -146,6 +146,9 @@ class ExactFormatter:
146
146
  self.rules["embedded_fonts"] = FormatRule(inline=True)
147
147
  self.rules["page"] = FormatRule(inline=True, quote_indices={1})
148
148
 
149
+ # Image element
150
+ self.rules["image"] = FormatRule(inline=False, custom_handler=self._format_image)
151
+
149
152
  def format(self, data: Any) -> str:
150
153
  """
151
154
  Format S-expression data with exact KiCAD formatting.
@@ -511,6 +514,34 @@ class ExactFormatter:
511
514
  result += f"\n{indent})"
512
515
  return result
513
516
 
517
+ def _format_image(self, lst: List[Any], indent_level: int) -> str:
518
+ """Format image elements with base64 data split across lines."""
519
+ indent = "\t" * indent_level
520
+ next_indent = "\t" * (indent_level + 1)
521
+
522
+ result = f"({lst[0]}"
523
+
524
+ # Process each element
525
+ for element in lst[1:]:
526
+ if isinstance(element, list):
527
+ tag = str(element[0]) if element else ""
528
+ if tag == "data":
529
+ # Special handling for data element
530
+ # First chunk on same line as (data, rest on subsequent lines
531
+ if len(element) > 1:
532
+ result += f'\n{next_indent}({element[0]} "{element[1]}"'
533
+ for chunk in element[2:]:
534
+ result += f'\n{next_indent}\t"{chunk}"'
535
+ result += f"\n{next_indent})"
536
+ else:
537
+ result += f"\n{next_indent}({element[0]})"
538
+ else:
539
+ # Regular element formatting
540
+ result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
541
+
542
+ result += f"\n{indent})"
543
+ return result
544
+
514
545
 
515
546
  class CompactFormatter(ExactFormatter):
516
547
  """Compact formatter for minimal output size."""
@@ -197,6 +197,7 @@ class SExpressionParser:
197
197
  "circles": [],
198
198
  "beziers": [],
199
199
  "rectangles": [],
200
+ "images": [],
200
201
  "nets": [],
201
202
  "lib_symbols": {},
202
203
  "sheet_instances": [],
@@ -282,6 +283,10 @@ class SExpressionParser:
282
283
  rectangle = self._parse_rectangle(item)
283
284
  if rectangle:
284
285
  schematic_data["rectangles"].append(rectangle)
286
+ elif element_type == "image":
287
+ image = self._parse_image(item)
288
+ if image:
289
+ schematic_data["images"].append(image)
285
290
  elif element_type == "lib_symbols":
286
291
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
287
292
  elif element_type == "sheet_instances":
@@ -357,6 +362,10 @@ class SExpressionParser:
357
362
  for graphic in schematic_data.get("graphics", []):
358
363
  sexp_data.append(self._graphic_to_sexp(graphic))
359
364
 
365
+ # Images
366
+ for image in schematic_data.get("images", []):
367
+ sexp_data.append(self._image_to_sexp(image))
368
+
360
369
  # Circles
361
370
  for circle in schematic_data.get("circles", []):
362
371
  sexp_data.append(self._circle_to_sexp(circle))
@@ -1108,6 +1117,37 @@ class SExpressionParser:
1108
1117
 
1109
1118
  return rectangle if rectangle else None
1110
1119
 
1120
+ def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1121
+ """Parse an image element."""
1122
+ # Format: (image (at x y) (uuid "...") (data "base64..."))
1123
+ image = {
1124
+ "position": {"x": 0, "y": 0},
1125
+ "data": "",
1126
+ "scale": 1.0,
1127
+ "uuid": None
1128
+ }
1129
+
1130
+ for elem in item[1:]:
1131
+ if not isinstance(elem, list):
1132
+ continue
1133
+
1134
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1135
+
1136
+ if elem_type == "at" and len(elem) >= 3:
1137
+ image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
1138
+ elif elem_type == "scale" and len(elem) >= 2:
1139
+ image["scale"] = float(elem[1])
1140
+ elif elem_type == "data" and len(elem) >= 2:
1141
+ # The data can be spread across multiple string elements
1142
+ data_parts = []
1143
+ for data_elem in elem[1:]:
1144
+ data_parts.append(str(data_elem).strip('"'))
1145
+ image["data"] = "".join(data_parts)
1146
+ elif elem_type == "uuid" and len(elem) >= 2:
1147
+ image["uuid"] = str(elem[1]).strip('"')
1148
+
1149
+ return image if image.get("uuid") and image.get("data") else None
1150
+
1111
1151
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
1112
1152
  """Parse lib_symbols section."""
1113
1153
  # Implementation for lib_symbols parsing
@@ -1933,6 +1973,38 @@ class SExpressionParser:
1933
1973
 
1934
1974
  return sexp
1935
1975
 
1976
+ def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
1977
+ """Convert image element to S-expression."""
1978
+ sexp = [sexpdata.Symbol("image")]
1979
+
1980
+ # Add position
1981
+ position = image_data.get("position", {"x": 0, "y": 0})
1982
+ pos_x, pos_y = position["x"], position["y"]
1983
+ sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
1984
+
1985
+ # Add UUID
1986
+ if "uuid" in image_data:
1987
+ sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
1988
+
1989
+ # Add scale if not default
1990
+ scale = image_data.get("scale", 1.0)
1991
+ if scale != 1.0:
1992
+ sexp.append([sexpdata.Symbol("scale"), scale])
1993
+
1994
+ # Add image data
1995
+ # KiCad splits base64 data into multiple lines for readability
1996
+ # Each line is roughly 76 characters (standard base64 line length)
1997
+ data = image_data.get("data", "")
1998
+ if data:
1999
+ data_sexp = [sexpdata.Symbol("data")]
2000
+ # Split the data into 76-character chunks
2001
+ chunk_size = 76
2002
+ for i in range(0, len(data), chunk_size):
2003
+ data_sexp.append(data[i:i+chunk_size])
2004
+ sexp.append(data_sexp)
2005
+
2006
+ return sexp
2007
+
1936
2008
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
1937
2009
  """Convert lib_symbols to S-expression."""
1938
2010
  sexp = [sexpdata.Symbol("lib_symbols")]
@@ -1138,6 +1138,55 @@ class Schematic:
1138
1138
  logger.debug(f"Added text box: '{text}' at {position} size {size}")
1139
1139
  return text_box.uuid
1140
1140
 
1141
+ def add_image(
1142
+ self,
1143
+ position: Union[Point, Tuple[float, float]],
1144
+ data: str,
1145
+ scale: float = 1.0,
1146
+ uuid: Optional[str] = None,
1147
+ ) -> str:
1148
+ """
1149
+ Add an image element.
1150
+
1151
+ Args:
1152
+ position: Image position
1153
+ data: Base64-encoded image data
1154
+ scale: Image scale factor (default 1.0)
1155
+ uuid: Optional UUID (auto-generated if None)
1156
+
1157
+ Returns:
1158
+ UUID of created image element
1159
+ """
1160
+ if isinstance(position, tuple):
1161
+ position = Point(position[0], position[1])
1162
+
1163
+ from .types import Image
1164
+
1165
+ import uuid as uuid_module
1166
+
1167
+ image = Image(
1168
+ uuid=uuid if uuid else str(uuid_module.uuid4()),
1169
+ position=position,
1170
+ data=data,
1171
+ scale=scale,
1172
+ )
1173
+
1174
+ if "images" not in self._data:
1175
+ self._data["images"] = []
1176
+
1177
+ self._data["images"].append(
1178
+ {
1179
+ "uuid": image.uuid,
1180
+ "position": {"x": image.position.x, "y": image.position.y},
1181
+ "data": image.data,
1182
+ "scale": image.scale,
1183
+ }
1184
+ )
1185
+ self._modified = True
1186
+
1187
+ logger.debug(f"Added image at {position} with {len(data)} bytes of data")
1188
+ return image.uuid
1189
+
1141
1190
  def add_rectangle(
1142
1191
  self,
1143
1192
  start: Union[Point, Tuple[float, float]],
@@ -1592,9 +1641,7 @@ class Schematic:
1592
1641
  break
1593
1642
 
1594
1643
  # Fix string/symbol conversion issues in pin definitions
1595
- print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
1596
1644
  self._fix_symbol_strings_recursively(modified_data)
1597
- print(f"🔧 DEBUG: After fix - symbol strings fixed")
1598
1645
 
1599
1646
  return modified_data
1600
1647
 
@@ -1607,15 +1654,10 @@ class Schematic:
1607
1654
  if isinstance(item, list):
1608
1655
  # Check for pin definitions that need fixing
1609
1656
  if len(item) >= 3 and item[0] == sexpdata.Symbol("pin"):
1610
- print(
1611
- f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}"
1612
- )
1613
1657
  # Fix pin type and shape - ensure they are symbols not strings
1614
1658
  if isinstance(item[1], str):
1615
- print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
1616
1659
  item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
1617
1660
  if len(item) >= 3 and isinstance(item[2], str):
1618
- print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
1619
1661
  item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
1620
1662
 
1621
1663
  # Recursively process nested lists
@@ -372,6 +372,20 @@ class SchematicRectangle:
372
372
  )
373
373
 
374
374
 
375
+ @dataclass
376
+ class Image:
377
+ """Image element in schematic."""
378
+
379
+ uuid: str
380
+ position: Point
381
+ data: str # Base64-encoded image data
382
+ scale: float = 1.0
383
+
384
+ def __post_init__(self):
385
+ if not self.uuid:
386
+ self.uuid = str(uuid4())
387
+
388
+
375
389
  @dataclass
376
390
  class Net:
377
391
  """Electrical net connecting components."""
@@ -0,0 +1,26 @@
1
+ """
2
+ Geometry module for KiCad schematic symbol bounding box calculations.
3
+
4
+ This module provides accurate bounding box calculations for KiCad symbols,
5
+ including font metrics and symbol geometry analysis.
6
+
7
+ Migrated from circuit-synth to kicad-sch-api for better architectural separation.
8
+ """
9
+
10
+ from .font_metrics import (
11
+ DEFAULT_PIN_LENGTH,
12
+ DEFAULT_PIN_NAME_OFFSET,
13
+ DEFAULT_PIN_NUMBER_SIZE,
14
+ DEFAULT_PIN_TEXT_WIDTH_RATIO,
15
+ DEFAULT_TEXT_HEIGHT,
16
+ )
17
+ from .symbol_bbox import SymbolBoundingBoxCalculator
18
+
19
+ __all__ = [
20
+ "SymbolBoundingBoxCalculator",
21
+ "DEFAULT_TEXT_HEIGHT",
22
+ "DEFAULT_PIN_LENGTH",
23
+ "DEFAULT_PIN_NAME_OFFSET",
24
+ "DEFAULT_PIN_NUMBER_SIZE",
25
+ "DEFAULT_PIN_TEXT_WIDTH_RATIO",
26
+ ]
@@ -0,0 +1,20 @@
1
+ """
2
+ Font metrics and text rendering constants for KiCad schematic text.
3
+
4
+ These constants are used for accurate text bounding box calculations
5
+ and symbol spacing in schematic layouts.
6
+ """
7
+
8
+ # KiCad default text size in mm
9
+ # Increased to better match actual KiCad rendering
10
+ DEFAULT_TEXT_HEIGHT = 2.54 # 100 mils (doubled from 50 mils)
11
+
12
+ # Default pin dimensions
13
+ DEFAULT_PIN_LENGTH = 2.54 # 100 mils
14
+ DEFAULT_PIN_NAME_OFFSET = 0.508 # 20 mils - offset from pin endpoint to label text
15
+ DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
16
+
17
+ # Text width ratio for proportional font rendering
18
+ # KiCad uses proportional fonts where average character width is ~0.65x height
19
+ # This prevents label text from extending beyond calculated bounding boxes
20
+ DEFAULT_PIN_TEXT_WIDTH_RATIO = 0.65 # Width to height ratio for pin text (proportional font average)
@@ -0,0 +1,595 @@
1
+ """
2
+ symbol_bbox.py
3
+
4
+ Calculate accurate bounding boxes for KiCad symbols based on their graphical elements.
5
+ This ensures proper spacing and collision detection in schematic layouts.
6
+
7
+ Migrated from circuit-synth to kicad-sch-api for better architectural separation.
8
+ """
9
+
10
+ import logging
11
+ import math
12
+ import os
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ from .font_metrics import (
16
+ DEFAULT_PIN_LENGTH,
17
+ DEFAULT_PIN_NAME_OFFSET,
18
+ DEFAULT_PIN_NUMBER_SIZE,
19
+ DEFAULT_PIN_TEXT_WIDTH_RATIO,
20
+ DEFAULT_TEXT_HEIGHT,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class SymbolBoundingBoxCalculator:
27
+ """Calculate the actual bounding box of a symbol from its graphical elements."""
28
+
29
+ # Re-export font metrics as class constants for backward compatibility
30
+ DEFAULT_TEXT_HEIGHT = DEFAULT_TEXT_HEIGHT
31
+ DEFAULT_PIN_LENGTH = DEFAULT_PIN_LENGTH
32
+ DEFAULT_PIN_NAME_OFFSET = DEFAULT_PIN_NAME_OFFSET
33
+ DEFAULT_PIN_NUMBER_SIZE = DEFAULT_PIN_NUMBER_SIZE
34
+ DEFAULT_PIN_TEXT_WIDTH_RATIO = DEFAULT_PIN_TEXT_WIDTH_RATIO
35
+
36
+ @classmethod
37
+ def calculate_bounding_box(
38
+ cls,
39
+ symbol_data: Dict[str, Any],
40
+ include_properties: bool = True,
41
+ hierarchical_labels: Optional[List[Dict[str, Any]]] = None,
42
+ pin_net_map: Optional[Dict[str, str]] = None,
43
+ ) -> Tuple[float, float, float, float]:
44
+ """
45
+ Calculate the actual bounding box of a symbol from its graphical elements.
46
+
47
+ Args:
48
+ symbol_data: Dictionary containing symbol definition from KiCad library
49
+ include_properties: Whether to include space for Reference/Value labels
50
+ hierarchical_labels: List of hierarchical labels attached to this symbol
51
+ pin_net_map: Optional mapping of pin numbers to net names (for accurate label sizing)
52
+
53
+ Returns:
54
+ Tuple of (min_x, min_y, max_x, max_y) in mm
55
+
56
+ Raises:
57
+ ValueError: If symbol data is invalid or bounding box cannot be calculated
58
+ """
59
+ if not symbol_data:
60
+ raise ValueError("Symbol data is None or empty")
61
+
62
+ import sys
63
+ # Reduced logging frequency - only log if DEBUG environment variable is set
64
+ debug_enabled = os.getenv("CIRCUIT_SYNTH_DEBUG", "").lower() == "true"
65
+ if debug_enabled:
66
+ print(f"\n=== CALCULATING BOUNDING BOX ===", file=sys.stderr, flush=True)
67
+ print(f"include_properties={include_properties}", file=sys.stderr, flush=True)
68
+
69
+ min_x = float("inf")
70
+ min_y = float("inf")
71
+ max_x = float("-inf")
72
+ max_y = float("-inf")
73
+
74
+ # Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
75
+ shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
76
+ print(f"Processing {len(shapes)} main shapes", file=sys.stderr, flush=True)
77
+ for shape in shapes:
78
+ shape_bounds = cls._get_shape_bounds(shape)
79
+ if shape_bounds:
80
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
81
+ min_x = min(min_x, s_min_x)
82
+ min_y = min(min_y, s_min_y)
83
+ max_x = max(max_x, s_max_x)
84
+ max_y = max(max_y, s_max_y)
85
+
86
+ # Process pins (including their labels)
87
+ pins = symbol_data.get("pins", [])
88
+ print(f"Processing {len(pins)} main pins", file=sys.stderr, flush=True)
89
+ for pin in pins:
90
+ pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
91
+ if pin_bounds:
92
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
93
+ min_x = min(min_x, p_min_x)
94
+ min_y = min(min_y, p_min_y)
95
+ max_x = max(max_x, p_max_x)
96
+ max_y = max(max_y, p_max_y)
97
+
98
+ # Process sub-symbols
99
+ sub_symbols = symbol_data.get("sub_symbols", [])
100
+ for sub in sub_symbols:
101
+ # Sub-symbols can have their own shapes and pins (handle both 'shapes' and 'graphics' keys)
102
+ sub_shapes = sub.get("shapes", []) or sub.get("graphics", [])
103
+ for shape in sub_shapes:
104
+ shape_bounds = cls._get_shape_bounds(shape)
105
+ if shape_bounds:
106
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
107
+ min_x = min(min_x, s_min_x)
108
+ min_y = min(min_y, s_min_y)
109
+ max_x = max(max_x, s_max_x)
110
+ max_y = max(max_y, s_max_y)
111
+
112
+ sub_pins = sub.get("pins", [])
113
+ for pin in sub_pins:
114
+ pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
115
+ if pin_bounds:
116
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
117
+ min_x = min(min_x, p_min_x)
118
+ min_y = min(min_y, p_min_y)
119
+ max_x = max(max_x, p_max_x)
120
+ max_y = max(max_y, p_max_y)
121
+
122
+ # Check if we found any geometry
123
+ if min_x == float("inf") or max_x == float("-inf"):
124
+ raise ValueError(f"No valid geometry found in symbol data")
125
+
126
+ print(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
127
+ print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
128
+
129
+ # Add small margin for text that might extend beyond shapes
130
+ margin = 0.254 # 10 mils
131
+ min_x -= margin
132
+ min_y -= margin
133
+ max_x += margin
134
+ max_y += margin
135
+
136
+ # Include space for component properties (Reference, Value, Footprint)
137
+ if include_properties:
138
+ # Use adaptive spacing based on component dimensions
139
+ component_width = max_x - min_x
140
+ component_height = max_y - min_y
141
+
142
+ # Adaptive property width: minimum 10mm or 80% of component width
143
+ property_width = max(10.0, component_width * 0.8)
144
+ property_height = cls.DEFAULT_TEXT_HEIGHT
145
+
146
+ # Adaptive vertical spacing: minimum 5mm or 10% of component height
147
+ vertical_spacing_above = max(5.0, component_height * 0.1)
148
+ vertical_spacing_below = max(10.0, component_height * 0.15)
149
+
150
+ # Reference label above
151
+ min_y -= vertical_spacing_above + property_height
152
+
153
+ # Value and Footprint labels below
154
+ max_y += vertical_spacing_below + property_height
155
+
156
+ # Extend horizontally for property text
157
+ center_x = (min_x + max_x) / 2
158
+ min_x = min(min_x, center_x - property_width / 2)
159
+ max_x = max(max_x, center_x + property_width / 2)
160
+
161
+ logger.debug(
162
+ f"Calculated bounding box: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
163
+ )
164
+
165
+ print(f"FINAL BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
166
+ print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
167
+ print("=" * 50 + "\n", file=sys.stderr, flush=True)
168
+
169
+ return (min_x, min_y, max_x, max_y)
170
+
171
+ @classmethod
172
+ def calculate_placement_bounding_box(
173
+ cls,
174
+ symbol_data: Dict[str, Any],
175
+ ) -> Tuple[float, float, float, float]:
176
+ """
177
+ Calculate bounding box for PLACEMENT purposes - excludes pin labels.
178
+
179
+ This method calculates a tighter bounding box that only includes:
180
+ - Component body (shapes/graphics)
181
+ - Pin endpoints (without label text)
182
+ - Small margin for component properties
183
+
184
+ Pin label text is excluded because it extends arbitrarily far based on
185
+ text length and would cause incorrect spacing in text-flow placement.
186
+
187
+ Args:
188
+ symbol_data: Dictionary containing symbol definition from KiCad library
189
+
190
+ Returns:
191
+ Tuple of (min_x, min_y, max_x, max_y) in mm
192
+
193
+ Raises:
194
+ ValueError: If symbol data is invalid or bounding box cannot be calculated
195
+ """
196
+ if not symbol_data:
197
+ raise ValueError("Symbol data is None or empty")
198
+
199
+ import sys
200
+ print(f"\n=== CALCULATING PLACEMENT BOUNDING BOX (NO PIN LABELS) ===", file=sys.stderr, flush=True)
201
+
202
+ min_x = float("inf")
203
+ min_y = float("inf")
204
+ max_x = float("-inf")
205
+ max_y = float("-inf")
206
+
207
+ # Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
208
+ shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
209
+ print(f"Processing {len(shapes)} main shapes", file=sys.stderr, flush=True)
210
+ for shape in shapes:
211
+ shape_bounds = cls._get_shape_bounds(shape)
212
+ if shape_bounds:
213
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
214
+ min_x = min(min_x, s_min_x)
215
+ min_y = min(min_y, s_min_y)
216
+ max_x = max(max_x, s_max_x)
217
+ max_y = max(max_y, s_max_y)
218
+
219
+ # Process pins WITHOUT labels (just pin endpoints)
220
+ pins = symbol_data.get("pins", [])
221
+ print(f"Processing {len(pins)} main pins (NO LABELS)", file=sys.stderr, flush=True)
222
+ for pin in pins:
223
+ pin_bounds = cls._get_pin_bounds_no_labels(pin)
224
+ if pin_bounds:
225
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
226
+ min_x = min(min_x, p_min_x)
227
+ min_y = min(min_y, p_min_y)
228
+ max_x = max(max_x, p_max_x)
229
+ max_y = max(max_y, p_max_y)
230
+
231
+ # Process sub-symbols
232
+ sub_symbols = symbol_data.get("sub_symbols", [])
233
+ for sub in sub_symbols:
234
+ # Sub-symbols can have their own shapes and pins
235
+ sub_shapes = sub.get("shapes", []) or sub.get("graphics", [])
236
+ for shape in sub_shapes:
237
+ shape_bounds = cls._get_shape_bounds(shape)
238
+ if shape_bounds:
239
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
240
+ min_x = min(min_x, s_min_x)
241
+ min_y = min(min_y, s_min_y)
242
+ max_x = max(max_x, s_max_x)
243
+ max_y = max(max_y, s_max_y)
244
+
245
+ sub_pins = sub.get("pins", [])
246
+ for pin in sub_pins:
247
+ pin_bounds = cls._get_pin_bounds_no_labels(pin)
248
+ if pin_bounds:
249
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
250
+ min_x = min(min_x, p_min_x)
251
+ min_y = min(min_y, p_min_y)
252
+ max_x = max(max_x, p_max_x)
253
+ max_y = max(max_y, p_max_y)
254
+
255
+ # Check if we found any geometry
256
+ if min_x == float("inf") or max_x == float("-inf"):
257
+ raise ValueError(f"No valid geometry found in symbol data")
258
+
259
+ print(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
260
+ print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
261
+
262
+ # Add small margin for visual spacing
263
+ margin = 0.635 # 25mil margin (reduced from 50mil)
264
+ min_x -= margin
265
+ min_y -= margin
266
+ max_x += margin
267
+ max_y += margin
268
+
269
+ # Add minimal space for component properties (Reference above, Value below)
270
+ # Use adaptive spacing based on component height for better visual hierarchy
271
+ component_height = max_y - min_y
272
+ property_spacing = max(3.0, component_height * 0.15) # Adaptive: minimum 3mm or 15% of height
273
+ property_height = 1.27 # Reduced from 2.54mm
274
+ min_y -= property_spacing + property_height # Reference above
275
+ max_y += property_spacing + property_height # Value below
276
+
277
+ print(f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
278
+ print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
279
+ print("=" * 50 + "\n", file=sys.stderr, flush=True)
280
+
281
+ return (min_x, min_y, max_x, max_y)
282
+
283
+ @classmethod
284
+ def calculate_visual_bounding_box(
285
+ cls, symbol_data: Dict[str, Any], pin_net_map: Optional[Dict[str, str]] = None
286
+ ) -> Tuple[float, float, float, float]:
287
+ """
288
+ Calculate bounding box for visual/debug drawing (includes pin labels, no property spacing).
289
+
290
+ This shows the actual component geometry including pin labels.
291
+ Use this for drawing bounding boxes on schematics.
292
+
293
+ Args:
294
+ symbol_data: Dictionary containing symbol definition
295
+ pin_net_map: Optional mapping of pin numbers to net names (for accurate label sizing)
296
+
297
+ Returns:
298
+ Tuple of (min_x, min_y, max_x, max_y) in mm
299
+ """
300
+ # Initialize bounds
301
+ min_x = float("inf")
302
+ min_y = float("inf")
303
+ max_x = float("-inf")
304
+ max_y = float("-inf")
305
+
306
+ # Process main symbol shapes
307
+ shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
308
+ for shape in shapes:
309
+ shape_bounds = cls._get_shape_bounds(shape)
310
+ if shape_bounds:
311
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
312
+ min_x = min(min_x, s_min_x)
313
+ min_y = min(min_y, s_min_y)
314
+ max_x = max(max_x, s_max_x)
315
+ max_y = max(max_y, s_max_y)
316
+
317
+ # Process pins WITH labels to get accurate visual bounds
318
+ pins = symbol_data.get("pins", [])
319
+ for pin in pins:
320
+ pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
321
+ if pin_bounds:
322
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
323
+ min_x = min(min_x, p_min_x)
324
+ min_y = min(min_y, p_min_y)
325
+ max_x = max(max_x, p_max_x)
326
+ max_y = max(max_y, p_max_y)
327
+
328
+ # Process sub-symbols
329
+ sub_symbols = symbol_data.get("sub_symbols", [])
330
+ for sub in sub_symbols:
331
+ sub_shapes = sub.get("shapes", []) or sub.get("graphics", [])
332
+ for shape in sub_shapes:
333
+ shape_bounds = cls._get_shape_bounds(shape)
334
+ if shape_bounds:
335
+ s_min_x, s_min_y, s_max_x, s_max_y = shape_bounds
336
+ min_x = min(min_x, s_min_x)
337
+ min_y = min(min_y, s_min_y)
338
+ max_x = max(max_x, s_max_x)
339
+ max_y = max(max_y, s_max_y)
340
+
341
+ sub_pins = sub.get("pins", [])
342
+ for pin in sub_pins:
343
+ pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
344
+ if pin_bounds:
345
+ p_min_x, p_min_y, p_max_x, p_max_y = pin_bounds
346
+ min_x = min(min_x, p_min_x)
347
+ min_y = min(min_y, p_min_y)
348
+ max_x = max(max_x, p_max_x)
349
+ max_y = max(max_y, p_max_y)
350
+
351
+ # Check if we found any geometry
352
+ if min_x == float("inf") or max_x == float("-inf"):
353
+ raise ValueError(f"No valid geometry found in symbol data")
354
+
355
+ # Add only a tiny margin for visibility (no property spacing)
356
+ margin = 0.254 # 10mil minimal margin
357
+ min_x -= margin
358
+ min_y -= margin
359
+ max_x += margin
360
+ max_y += margin
361
+
362
+ return (min_x, min_y, max_x, max_y)
363
+
364
+ @classmethod
365
+ def get_symbol_dimensions(
366
+ cls, symbol_data: Dict[str, Any], include_properties: bool = True, pin_net_map: Optional[Dict[str, str]] = None
367
+ ) -> Tuple[float, float]:
368
+ """
369
+ Get the width and height of a symbol.
370
+
371
+ Args:
372
+ symbol_data: Dictionary containing symbol definition
373
+ include_properties: Whether to include space for Reference/Value labels
374
+ pin_net_map: Optional mapping of pin numbers to net names
375
+
376
+ Returns:
377
+ Tuple of (width, height) in mm
378
+ """
379
+ min_x, min_y, max_x, max_y = cls.calculate_bounding_box(
380
+ symbol_data, include_properties, pin_net_map=pin_net_map
381
+ )
382
+ width = max_x - min_x
383
+ height = max_y - min_y
384
+ return (width, height)
385
+
386
+ @classmethod
387
+ def _get_shape_bounds(
388
+ cls, shape: Dict[str, Any]
389
+ ) -> Optional[Tuple[float, float, float, float]]:
390
+ """Get bounding box for a graphical shape."""
391
+ shape_type = shape.get("shape_type", "")
392
+
393
+ if shape_type == "rectangle":
394
+ start = shape.get("start", [0, 0])
395
+ end = shape.get("end", [0, 0])
396
+ return (
397
+ min(start[0], end[0]),
398
+ min(start[1], end[1]),
399
+ max(start[0], end[0]),
400
+ max(start[1], end[1]),
401
+ )
402
+
403
+ elif shape_type == "circle":
404
+ center = shape.get("center", [0, 0])
405
+ radius = shape.get("radius", 0)
406
+ return (
407
+ center[0] - radius,
408
+ center[1] - radius,
409
+ center[0] + radius,
410
+ center[1] + radius,
411
+ )
412
+
413
+ elif shape_type == "arc":
414
+ # For arcs, we need to consider start, mid, and end points
415
+ start = shape.get("start", [0, 0])
416
+ mid = shape.get("mid", [0, 0])
417
+ end = shape.get("end", [0, 0])
418
+
419
+ # Simple approach: use bounding box of all three points
420
+ # More accurate would be to calculate the actual arc bounds
421
+ min_x = min(start[0], mid[0], end[0])
422
+ min_y = min(start[1], mid[1], end[1])
423
+ max_x = max(start[0], mid[0], end[0])
424
+ max_y = max(start[1], mid[1], end[1])
425
+
426
+ return (min_x, min_y, max_x, max_y)
427
+
428
+ elif shape_type == "polyline":
429
+ points = shape.get("points", [])
430
+ if not points:
431
+ return None
432
+
433
+ min_x = min(p[0] for p in points)
434
+ min_y = min(p[1] for p in points)
435
+ max_x = max(p[0] for p in points)
436
+ max_y = max(p[1] for p in points)
437
+
438
+ return (min_x, min_y, max_x, max_y)
439
+
440
+ elif shape_type == "text":
441
+ # Text bounding box estimation
442
+ at = shape.get("at", [0, 0])
443
+ text = shape.get("text", "")
444
+ # Rough estimation: each character is about 1.27mm wide
445
+ text_width = len(text) * cls.DEFAULT_TEXT_HEIGHT * 0.6
446
+ text_height = cls.DEFAULT_TEXT_HEIGHT
447
+
448
+ return (
449
+ at[0] - text_width / 2,
450
+ at[1] - text_height / 2,
451
+ at[0] + text_width / 2,
452
+ at[1] + text_height / 2,
453
+ )
454
+
455
+ return None
456
+
457
+ @classmethod
458
+ def _get_pin_bounds(
459
+ cls, pin: Dict[str, Any], pin_net_map: Optional[Dict[str, str]] = None
460
+ ) -> Optional[Tuple[float, float, float, float]]:
461
+ """Get bounding box for a pin including its labels."""
462
+ import sys
463
+
464
+ # Handle both formats: 'at' array or separate x/y/orientation
465
+ if "at" in pin:
466
+ at = pin.get("at", [0, 0])
467
+ x, y = at[0], at[1]
468
+ angle = at[2] if len(at) > 2 else 0
469
+ else:
470
+ # Handle the format from symbol cache
471
+ x = pin.get("x", 0)
472
+ y = pin.get("y", 0)
473
+ angle = pin.get("orientation", 0)
474
+
475
+ length = pin.get("length", cls.DEFAULT_PIN_LENGTH)
476
+
477
+ # Calculate pin endpoint based on angle
478
+ angle_rad = math.radians(angle)
479
+ end_x = x + length * math.cos(angle_rad)
480
+ end_y = y + length * math.sin(angle_rad)
481
+
482
+ # Start with pin line bounds
483
+ min_x = min(x, end_x)
484
+ min_y = min(y, end_y)
485
+ max_x = max(x, end_x)
486
+ max_y = max(y, end_y)
487
+
488
+ # Add space for pin name and number
489
+ pin_name = pin.get("name", "")
490
+ pin_number = pin.get("number", "")
491
+
492
+ # Use net name for label sizing if available (hierarchical labels show net names, not pin names)
493
+ # If no net name match, use minimal fallback to avoid oversized bounding boxes
494
+ if pin_net_map and pin_number in pin_net_map:
495
+ label_text = pin_net_map[pin_number]
496
+ print(f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}", file=sys.stderr, flush=True)
497
+ else:
498
+ # No net match - use minimal size (3 chars) instead of potentially long pin name
499
+ label_text = "XXX" # 3-character placeholder for unmatched pins
500
+ print(f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})", file=sys.stderr, flush=True)
501
+
502
+ if label_text and label_text != "~": # ~ means no name
503
+ # Calculate text dimensions
504
+ # For horizontal text: width = char_count * char_width
505
+ name_width = (
506
+ len(label_text)
507
+ * cls.DEFAULT_TEXT_HEIGHT
508
+ * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
509
+ )
510
+ # For vertical text: height = char_count * char_height (characters stack vertically)
511
+ name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
512
+
513
+ print(f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})", file=sys.stderr, flush=True)
514
+
515
+ # Adjust bounds based on pin orientation
516
+ # Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
517
+ # Pin angle indicates where the pin points (into component)
518
+ # Apply KiCad's standard pin name offset (0.508mm / 20 mils)
519
+ offset = cls.DEFAULT_PIN_NAME_OFFSET
520
+
521
+ if angle == 0: # Pin points right - label extends LEFT from endpoint
522
+ label_x = end_x - offset - name_width
523
+ print(f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
524
+ min_x = min(min_x, label_x)
525
+ elif angle == 180: # Pin points left - label extends RIGHT from endpoint
526
+ label_x = end_x + offset + name_width
527
+ print(f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
528
+ max_x = max(max_x, label_x)
529
+ elif angle == 90: # Pin points up - label extends DOWN from endpoint
530
+ label_y = end_y - offset - name_height
531
+ print(f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
532
+ min_y = min(min_y, label_y)
533
+ elif angle == 270: # Pin points down - label extends UP from endpoint
534
+ label_y = end_y + offset + name_height
535
+ print(f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
536
+ max_y = max(max_y, label_y)
537
+
538
+ # Pin numbers are typically placed near the component body
539
+ if pin_number:
540
+ num_width = (
541
+ len(pin_number)
542
+ * cls.DEFAULT_PIN_NUMBER_SIZE
543
+ * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
544
+ )
545
+ # Add some space for the pin number
546
+ margin = (
547
+ cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
548
+ ) # Increase margin for better spacing
549
+ min_x -= margin
550
+ min_y -= margin
551
+ max_x += margin
552
+ max_y += margin
553
+
554
+ print(f" Pin bounds: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
555
+ return (min_x, min_y, max_x, max_y)
556
+
557
+ @classmethod
558
+ def _get_pin_bounds_no_labels(
559
+ cls, pin: Dict[str, Any]
560
+ ) -> Optional[Tuple[float, float, float, float]]:
561
+ """Get bounding box for a pin WITHOUT labels - for placement calculations only."""
562
+ import sys
563
+
564
+ # Handle both formats: 'at' array or separate x/y/orientation
565
+ if "at" in pin:
566
+ at = pin.get("at", [0, 0])
567
+ x, y = at[0], at[1]
568
+ angle = at[2] if len(at) > 2 else 0
569
+ else:
570
+ # Handle the format from symbol cache
571
+ x = pin.get("x", 0)
572
+ y = pin.get("y", 0)
573
+ angle = pin.get("orientation", 0)
574
+
575
+ length = pin.get("length", cls.DEFAULT_PIN_LENGTH)
576
+
577
+ # Calculate pin endpoint based on angle
578
+ angle_rad = math.radians(angle)
579
+ end_x = x + length * math.cos(angle_rad)
580
+ end_y = y + length * math.sin(angle_rad)
581
+
582
+ # Only include the pin line itself - NO labels
583
+ min_x = min(x, end_x)
584
+ min_y = min(y, end_y)
585
+ max_x = max(x, end_x)
586
+ max_y = max(y, end_y)
587
+
588
+ # Add small margin for pin graphics (circles, etc)
589
+ margin = 0.5 # Small margin for pin endpoint graphics
590
+ min_x -= margin
591
+ min_y -= margin
592
+ max_x += margin
593
+ max_y += margin
594
+
595
+ return (min_x, min_y, max_x, max_y)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Professional KiCAD schematic manipulation library with exact format preservation
5
5
  Author-email: Circuit-Synth <shane@circuit-synth.com>
6
6
  Maintainer-email: Circuit-Synth <shane@circuit-synth.com>
@@ -1,31 +1,34 @@
1
- kicad_sch_api/__init__.py,sha256=6yDjQeaoOdZatCgDaIULVxwXqT1fjLSNjYU0yKRh9f4,2919
1
+ kicad_sch_api/__init__.py,sha256=87qLx-VMQTBHDVq_CiXW-6wyKtvLFc3AsKQaoLKsIbs,2919
2
2
  kicad_sch_api/cli.py,sha256=ZzmwzfHEvPgGfCiQBU4G2LBAyRtMNiBRoY21pivJSYc,7621
3
3
  kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
4
4
  kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
5
5
  kicad_sch_api/core/component_bounds.py,sha256=BFYJYULyzs5it2hN7bHTimyS9Vet4dxsMklRStob-F4,17509
6
6
  kicad_sch_api/core/components.py,sha256=tXRL18GObl2u94wl5jP-1ID56s_UD9F1gQ_iRIyZ_Kw,25290
7
7
  kicad_sch_api/core/config.py,sha256=itw0j3DeIEHaFVf8p3mfAS1SP6jclBwvMv7NPdkThE4,4309
8
- kicad_sch_api/core/formatter.py,sha256=7kwM7WdbVjT8biirWSSFO44OMjeByvzYYr_-mpGiEA4,20862
8
+ kicad_sch_api/core/formatter.py,sha256=zzZi0f06C1YWUy5l0WFS9G4KRTEzmAY3rFK3XGocvCo,22185
9
9
  kicad_sch_api/core/geometry.py,sha256=27SgN0padLbQuTi8MV6UUCp6Pyaiv8V9gmYDOhfwny8,2947
10
10
  kicad_sch_api/core/ic_manager.py,sha256=Kg0HIOMU-TGXiIkrnwcHFQ1Kfv_3rW2U1cwBKJsKopc,7219
11
11
  kicad_sch_api/core/junctions.py,sha256=Ay6BsWX_DLs-wB0eMA2CytKKq0N8Ja41ZubJWpAqNgM,6122
12
12
  kicad_sch_api/core/manhattan_routing.py,sha256=t_T2u0zsQB-a8dTijFmY-qFq-oDt2qDebYyXzD_pBWI,15989
13
- kicad_sch_api/core/parser.py,sha256=UMTpwrxtyFM5cjAkgLWts6vBJ254gc4Hvx-puZydkVQ,92209
13
+ kicad_sch_api/core/parser.py,sha256=UY_GNX1yHd3xgTVqZ9TZe1u94q4YZBo-NibsSH8Jy44,94983
14
14
  kicad_sch_api/core/pin_utils.py,sha256=XGEow3HzBTyT8a0B_ZC8foMvwzYaENSaqTUwDW1rz24,5417
15
- kicad_sch_api/core/schematic.py,sha256=U9-wrhuGtgRqZJfc76Dj-g1_ZTjrT8R9LmfX-BIBH8w,61201
15
+ kicad_sch_api/core/schematic.py,sha256=B2n0tf3HyyVzTJOPABpzJGVbd5yBAsI9CE5OVZnSCoI,62027
16
16
  kicad_sch_api/core/simple_manhattan.py,sha256=CvIHvwmfABPF-COzhblYxEgRoR_R_eD-lmBFHHjDuMI,7241
17
- kicad_sch_api/core/types.py,sha256=UmqIvEx_Pd3B9jhvtmgZxx4SAjHUeOZBOEc8VtRILZs,13716
17
+ kicad_sch_api/core/types.py,sha256=D8VGvE7N2nj-xqnWSnTl98WaAbWh6JhQsn-pZCiLFfE,13974
18
18
  kicad_sch_api/core/wire_routing.py,sha256=G-C7S-ntQxwuu1z3OaaYlkURXwKE4r4xmhbbi6cvvaI,12830
19
19
  kicad_sch_api/core/wires.py,sha256=608t9oH4UzppdGgNgUd-ABK6T-ahyETZwhO_-CuKFO8,8319
20
20
  kicad_sch_api/discovery/__init__.py,sha256=qSuCsnC-hVtaLYE8fwd-Gea6JKwEVGPQ-hSNDNJYsIU,329
21
21
  kicad_sch_api/discovery/search_index.py,sha256=KgQT8ipT9OU6ktUwhDZ37Mao0Cba0fJOsxUk9m8ZKbY,15856
22
+ kicad_sch_api/geometry/__init__.py,sha256=hTBXkn8mZZCjzDIrtPv67QsnCYB77L67JjthQgEIX7o,716
23
+ kicad_sch_api/geometry/font_metrics.py,sha256=qqnfBuRqiLQDnGkk64rKzdyvuSNU0uBfdp0TKEgzXds,831
24
+ kicad_sch_api/geometry/symbol_bbox.py,sha256=5oMVmmimyqF4ITj8wS9yeU3jXdpqG0XhCIvAHlYr9Rg,24688
22
25
  kicad_sch_api/library/__init__.py,sha256=NG9UTdcpn25Bl9tPsYs9ED7bvpaVPVdtLMbnxkQkOnU,250
23
26
  kicad_sch_api/library/cache.py,sha256=7na88grl465WHwUOGuOzYrrWwjsMBXhXVtxhnaJ9GBY,33208
24
27
  kicad_sch_api/utils/__init__.py,sha256=1V_yGgI7jro6MUc4Pviux_WIeJ1wmiYFID186SZwWLQ,277
25
28
  kicad_sch_api/utils/validation.py,sha256=XlWGRZJb3cOPYpU9sLQQgC_NASwbi6W-LCN7PzUmaPY,15626
26
- kicad_sch_api-0.3.2.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
27
- kicad_sch_api-0.3.2.dist-info/METADATA,sha256=m8NfvoWy9m4AfShTSlHkn2AMUzy1cZkxiQXPnF3e0a0,17183
28
- kicad_sch_api-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- kicad_sch_api-0.3.2.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
30
- kicad_sch_api-0.3.2.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
31
- kicad_sch_api-0.3.2.dist-info/RECORD,,
29
+ kicad_sch_api-0.3.4.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
30
+ kicad_sch_api-0.3.4.dist-info/METADATA,sha256=4RHjg7LWStLaT3wRrAkmDhBTLR5l-TRYoc2eXP5zZHs,17183
31
+ kicad_sch_api-0.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ kicad_sch_api-0.3.4.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
33
+ kicad_sch_api-0.3.4.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
34
+ kicad_sch_api-0.3.4.dist-info/RECORD,,