Skip to content

Commit 61b68bc

Browse files
committed
Bugfix: PCB Records could not be visualized.
svg_utils.py added - Function to generate an arc path get_arc_path is implemented here SchArc and SchEllipticalArc refer to get_arc_path from svg_utils.py PCBArc.py added
1 parent 52787d3 commit 61b68bc

31 files changed

+269
-151
lines changed

docs/source/classes/Components.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Generic Component (Base)
1010
.. autoattribute:: LibFile
1111
.. autoattribute:: Name
1212
.. autoattribute:: Description
13+
.. autoattribute:: Records
1314

1415
.. automethod:: pyaltiumlib.libcomponent.LibComponent.read_meta
1516
.. automethod:: pyaltiumlib.libcomponent.LibComponent.draw_svg
@@ -23,4 +24,5 @@ Schematic Symbol
2324
PCB Footprint
2425
==================
2526

26-
.. autoclass:: pyaltiumlib.pcblib.footprint.PcbLibFootprint
27+
.. autoclass:: pyaltiumlib.pcblib.footprint.PcbLibFootprint
28+

docs/source/classes/LibFile.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ PCB Library File (PCBLib)
3434

3535
.. autoclass:: pyaltiumlib.pcblib.lib.PcbLib
3636

37+
.. autoattribute:: Layers

docs/source/usage/release.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The code is available in `this repository <https://github.com/ChrisHoyer/pyAltiu
1414
File Format Coverage
1515
=======================
1616

17-
Currently, the format is not completely supported, some aspects are not fully implemented.
17+
Currently, the format is not completely supported, some aspects are not fully implemented. Not all containers or records are known yet, so this list and the associated documentation may be incomplete.
1818

1919
The coverage of containers of the Altium library file format is shown below:
2020

@@ -197,7 +197,7 @@ The coverage of records/primitives of the Altium library file format is shown be
197197
- Coverage
198198
* - PCB Primitives
199199
- Arc
200-
- Not Implemented
200+
- Key features implemented
201201
* - PCB Primitives
202202
- Pad
203203
- Key features implemented

src/pyaltiumlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
AUTHOR_EMAIL = '[email protected]'
88
CYEAR = '2024-2025'
99

10-
__version__ = "0.1"
10+
__version__ = "0.2"
1111
__author__ = "Chris Hoyer <[email protected]>"
1212

1313
import os

src/pyaltiumlib/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ def __init__(self, filepath: str):
5353
raise
5454

5555
self.LibType = type(self)
56-
self.FilePath = filepath
56+
self.FilePath = filepath
57+
self.ComponentCount = 0
58+
self.Parts = []
5759

5860
self._olefile = None
5961
self._olefile_open = False

src/pyaltiumlib/datatypes/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@
4444
"PCBHoleShape",
4545
"PCBTextJustification",
4646
"PCBStrokeFont",
47-
"PCBTextKind"
47+
"PCBTextKind",
4848
]

src/pyaltiumlib/datatypes/binaryreader.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from pyaltiumlib.datatypes.coordinate import Coordinate, CoordinatePoint
22

3+
# Configure logging
4+
import logging
5+
logger = logging.getLogger(__name__)
6+
37
class BinaryReader:
48

59
def __init__(self, data):
@@ -12,7 +16,7 @@ def from_stream(cls, stream, size_length=4):
1216
data = stream.read( length )
1317

1418
if len(data) != length:
15-
raise ValueError("Stream does not match the declared block length.")
19+
logger.warning("Stream does not match the declared block length.")
1620

1721
return cls( data )
1822

@@ -25,39 +29,39 @@ def length(self):
2529
def read(self, length):
2630

2731
if self.offset + length > len(self.data):
28-
raise ValueError("Not enough data to read the requested length.")
32+
logger.warning("Not enough data to read the requested length.")
2933

3034
result = self.data[self.offset:self.offset + length]
3135
self.offset += length
3236
return result
3337

3438
def read_byte(self):
3539
if self.offset + 1 > len(self.data):
36-
raise ValueError("Not enough data to read.")
40+
logger.warning("Not enough data to read.")
3741
return self.read(1)
3842

3943
def read_int8(self, signed=False):
4044
return int.from_bytes(self.read_byte(), signed=signed)
4145

4246
def read_int16(self, signed=False):
4347
if self.offset + 2 > len(self.data):
44-
raise ValueError("Not enough data to read an Int16.")
48+
logger.warning("Not enough data to read an Int16.")
4549

4650
value = int.from_bytes(self.data[self.offset:self.offset + 2], byteorder="little", signed=signed)
4751
self.offset += 2
4852
return value
4953

5054
def read_int32(self, signed=False):
5155
if self.offset + 4 > len(self.data):
52-
raise ValueError("Not enough data to read an Int32.")
56+
logger.warning("Not enough data to read an Int32.")
5357

5458
value = int.from_bytes(self.data[self.offset:self.offset + 4], byteorder="little", signed=signed)
5559
self.offset += 4
5660
return value
5761

5862
def read_double(self):
5963
if self.offset + 8 > len(self.data):
60-
raise ValueError("Not enough data to read a Double.")
64+
logger.warning("Not enough data to read a Double.")
6165

6266
raw_bytes = self.data[self.offset:self.offset + 8]
6367
self.offset += 8
@@ -69,7 +73,7 @@ def read_string_block(self, size_string=1):
6973
string_data = self.read( length_string )
7074

7175
if len(string_data) != length_string:
72-
raise ValueError("String does not match the declared string length.")
76+
logger.warning("String does not match the declared string length.")
7377

7478
return string_data.decode('windows-1252')
7579

@@ -85,7 +89,7 @@ def read_unicode_text(self, length=32, encoding='utf-16-le'):
8589

8690
while len(data) < length:
8791
if self.offset + 2 > len(self.data):
88-
raise ValueError("Not enough data to read.")
92+
logger.warning("Not enough data to read.")
8993

9094
# Read 2 bytes (1 Unicode character)
9195
unicode_char = self.read(2)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import math
2+
3+
def get_arc_path(center, radius_x, radius_y, angle_start, angle_end):
4+
"""
5+
Generate the SVG path data for the arc.
6+
7+
Args:
8+
center (tuple): The center coordinates of the arc.
9+
radius_x (int): The x-radius of the arc.
10+
radius_y (int): The y-radius of the arc.
11+
12+
Returns:
13+
str: The SVG path data for the arc.
14+
"""
15+
def degrees_to_radians(degrees):
16+
return (degrees * math.pi / 180) % (2*math.pi)
17+
18+
angle_start = degrees_to_radians(angle_start)
19+
angle_stop = degrees_to_radians(angle_end)
20+
21+
if angle_start == angle_stop:
22+
angle_stop -= 0.001
23+
24+
start_x = center[0] + radius_x * math.cos(-angle_start)
25+
start_y = center[1] + radius_y * math.sin(-angle_start)
26+
end_x = center[0] + radius_x * math.cos(-angle_stop)
27+
end_y = center[1] + radius_y * math.sin(-angle_stop)
28+
29+
# Set large_arc_flag based on the angle difference
30+
large_arc_flag = 1 if (angle_stop - angle_start) % (2 * math.pi) > math.pi else 0
31+
32+
# Set sweep_flag to 0 for counterclockwise
33+
sweep_flag = 0
34+
35+
path_data = (
36+
f"M {start_x},{start_y} "
37+
f"A {radius_x},{radius_y} 0 {large_arc_flag},{sweep_flag} {end_x},{end_y}"
38+
)
39+
return path_data

src/pyaltiumlib/libcomponent.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ class LibComponent:
3131
"""
3232
`string` that contains the description of the component
3333
"""
34+
35+
Records = []
36+
"""
37+
`List[any]` is a collection of records in their specific class
38+
contained in the library.
39+
"""
3440

3541
def __init__(self, parent, name: str, description: str):
3642
"""
@@ -39,12 +45,12 @@ def __init__(self, parent, name: str, description: str):
3945
"""
4046
self.LibFile = parent
4147
self.Name = name
42-
self.Description = description
43-
44-
self.Records = []
45-
self._BoundingBoxes = []
46-
self._drawing_layer = {}
48+
self.Description = description
49+
self.Records = []
4750

51+
self._BoundingBoxes = []
52+
self._graphic_layers = {}
53+
4854

4955
def __repr__(self) -> Dict:
5056
"""
@@ -84,7 +90,6 @@ def draw_svg(self, graphic, size_x: float, size_y: float, draw_bbox: bool = Fals
8490
:param float size_x: The width of the svgwrite drawing object
8591
:param float size_y: The height of the svgwrite drawing object
8692
:param bool [optional] draw_bbox: Draw bounding boxes
87-
:default draw_bbox: False
8893
8994
:raises ImportError: If 'svgwrite' module is not installed.
9095
:raises ValueError: If 'graphic' is not the right instance.
@@ -102,7 +107,8 @@ def draw_svg(self, graphic, size_x: float, size_y: float, draw_bbox: bool = Fals
102107
validObj = []
103108
for obj in self.Records:
104109
if hasattr(obj, 'draw_svg') and callable(getattr(obj, 'draw_svg')):
105-
validObj.append(obj)
110+
if obj.is_initialized:
111+
validObj.append(obj)
106112
else:
107113
logger.warning(f"Object: {obj} has no drawing function")
108114

@@ -115,32 +121,25 @@ def draw_svg(self, graphic, size_x: float, size_y: float, draw_bbox: bool = Fals
115121
fill=self.LibFile._BackgroundColor.to_hex()))
116122

117123
# Add Drawing Layer
124+
self._graphic_layers = {}
118125
if hasattr(self, 'LibFile'):
119-
if hasattr(self.LibFile, '_Layer'):
120-
for x in sorted(self.LibFile._Layer, key=lambda obj: obj.drawing_order, reverse=True):
121-
self._drawing_layer[x.id] = graphic.add(graphic.g(id=x.svg_layer))
122-
126+
if hasattr(self.LibFile, 'Layers'):
127+
for x in sorted(self.LibFile.Layers, key=lambda obj: obj.drawing_order, reverse=True):
128+
self._graphic_layers[x.id] = graphic.add(graphic.g(id=x.svg_layer))
129+
123130
if draw_bbox:
124131
for obj in validObj:
125132
obj.draw_bounding_box( graphic, offset, zoom)
126133

127134
# Draw Primitives
128135
for obj in validObj:
129-
obj.draw_svg( graphic, offset, zoom)
136+
obj.draw_svg( graphic, offset, zoom)
137+
130138

131139

132140
def _autoscale(self, elements: List[Any], target_width: float, target_height: float, margin: float = 10.0) -> tuple:
133141
"""
134142
Adjust the coordinates of elements to fit within the target dimensions using zoom.
135-
136-
Args:
137-
elements (List[Any]): A list of objects with `get_bounding_box` method.
138-
target_width (float): Target image width.
139-
target_height (float): Target image height.
140-
margin (float): Margin around the bounding box.
141-
142-
Returns:
143-
tuple: (offset, zoom)
144143
"""
145144

146145
min_point = CoordinatePoint(Coordinate(float("inf")), Coordinate(float("inf")))

src/pyaltiumlib/pcblib/footprint.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class PcbLibFootprint(LibComponent):
1515
1616
:param class parent: reference to library file :class:`pyaltiumlib.schlib.lib.PCBLib`
1717
:param string name: name of the component
18-
:param string description: description of the component
18+
:param string [optional] description: description of the component
1919
2020
:raises ValueError: If record id is not valid
2121
:raises ValueError: If component data can not be read
@@ -57,6 +57,9 @@ def _ReadFootprintData(self):
5757
if RecordID == 0:
5858
StreamOnGoing = False
5959
break
60+
61+
elif RecordID == 1:
62+
self.Records.append( PcbArc(self, olestream) )
6063

6164
elif RecordID == 2:
6265
self.Records.append( PcbPad(self, olestream) )

0 commit comments

Comments
 (0)