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 +2 -2
- kicad_sch_api/core/formatter.py +31 -0
- kicad_sch_api/core/parser.py +72 -0
- kicad_sch_api/core/schematic.py +49 -7
- kicad_sch_api/core/types.py +14 -0
- kicad_sch_api/geometry/__init__.py +26 -0
- kicad_sch_api/geometry/font_metrics.py +20 -0
- kicad_sch_api/geometry/symbol_bbox.py +595 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/METADATA +1 -1
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/RECORD +14 -11
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.4.dist-info}/top_level.txt +0 -0
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.
|
|
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,
|
|
58
|
+
VERSION_INFO = (0, 3, 3)
|
|
59
59
|
|
|
60
60
|
# Public API
|
|
61
61
|
__all__ = [
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -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."""
|
kicad_sch_api/core/parser.py
CHANGED
|
@@ -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")]
|
kicad_sch_api/core/schematic.py
CHANGED
|
@@ -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
|
kicad_sch_api/core/types.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
27
|
-
kicad_sch_api-0.3.
|
|
28
|
-
kicad_sch_api-0.3.
|
|
29
|
-
kicad_sch_api-0.3.
|
|
30
|
-
kicad_sch_api-0.3.
|
|
31
|
-
kicad_sch_api-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|