11import importlib
22import inspect
3- from typing import List , Tuple , NamedTuple , Dict
3+ import math
4+ from typing import List , Tuple , NamedTuple , Dict , Union , Set
45
56from .. import edgir
7+ from .KicadFootprintData import FootprintDataTable
68from ..core import *
79from .NetlistGenerator import NetlistTransform , NetBlock , Netlist
810from .SvgPcbTemplateBlock import SvgPcbTemplateBlock
911
1012
13+ class PlacedBlock (NamedTuple ):
14+ """A placement of a hierarchical block, including the coordinates of its immediate elements.
15+ Elements are placed in local space, with (0, 0) as the origin and elements moved as a group.
16+ Elements are indexed by name."""
17+ elts : Dict [str , Tuple [Union ['PlacedBlock' , TransformUtil .Path ], Tuple [float , float ]]] # name -> elt, (x, y)
18+ height : float
19+ width : float
20+
21+
22+ def arrange_netlist (netlist : Netlist ) -> PlacedBlock :
23+ FOOTPRINT_BORDER = 1 # mm
24+ BLOCK_BORDER = 2 # mm
25+
26+ # create list of blocks by path
27+ block_subblocks : Dict [Tuple [str , ...], Set [str ]] = {}
28+ block_footprints : Dict [Tuple [str , ...], List [NetBlock ]] = {}
29+
30+ # for here, we only group one level deep
31+ for block in netlist .blocks :
32+ containing_path = block .full_path .blocks [0 :min (len (block .full_path .blocks ) - 1 , 1 )]
33+ block_footprints .setdefault (containing_path , []).append (block )
34+ for i in range (len (containing_path )):
35+ block_subblocks .setdefault (tuple (containing_path [:i ]), set ()).add (containing_path [i ])
36+
37+ def arrange_hierarchy (root : Tuple [str , ...]) -> PlacedBlock :
38+ """Recursively arranges the immediate components of a hierarchy, treating each element
39+ as a bounding box rectangle, and trying to maintain some aspect ratio."""
40+ # TODO don't count borders as part of a block's width / height
41+ ASPECT_RATIO = 16 / 9
42+
43+ sub_placed : List [Tuple [str , float , float , Union [PlacedBlock , NetBlock ]]] = [] # (name, width, height, PlacedBlock or footprint)
44+ for subblock in block_subblocks .get (root , set ()):
45+ subplaced = arrange_hierarchy (root + (subblock ,))
46+ sub_placed .append ((subblock , subplaced .width + BLOCK_BORDER , subplaced .height + BLOCK_BORDER , subplaced ))
47+
48+ for footprint in block_footprints .get (root , []):
49+ bbox = FootprintDataTable .bbox_of (footprint .footprint ) or (1 , 1 , 1 , 1 )
50+ width = bbox [2 ] - bbox [0 ] + FOOTPRINT_BORDER
51+ height = bbox [3 ] - bbox [1 ] + FOOTPRINT_BORDER
52+ # use refdes as key so it's globally unique, for when this is run with blocks grouped together
53+ sub_placed .append ((footprint .refdes , width , height , footprint ))
54+
55+ total_area = sum (width * height for _ , width , height , _ in sub_placed )
56+ max_width = math .sqrt (total_area * ASPECT_RATIO )
57+
58+ x_max = 0.0
59+ y_max = 0.0
60+ # track the y limits and y position of the prior elements
61+ x_stack : List [Tuple [float , float , float ]] = [] # [(x pos of next, y pos, y limit)]
62+ elts : Dict [str , Tuple [Union [PlacedBlock , TransformUtil .Path ], Tuple [float , float ]]] = {}
63+ for name , width , height , entry in sorted (sub_placed , key = lambda x : - x [2 ]): # by height
64+ if not x_stack : # only on first component
65+ next_y = 0.0
66+ else :
67+ next_y = x_stack [- 1 ][1 ] # y position of the next element
68+
69+ while True : # advance rows as needed
70+ if not x_stack :
71+ break
72+ if x_stack [- 1 ][0 ] + width > max_width : # out of X space, advance a row
73+ _ , _ , next_y = x_stack .pop ()
74+ continue
75+ if next_y + height > x_stack [- 1 ][2 ]: # out of Y space, advance a row
76+ _ , _ , next_y = x_stack .pop ()
77+ continue
78+ break
79+
80+ if not x_stack :
81+ next_x = 0.0
82+ else :
83+ next_x = x_stack [- 1 ][0 ]
84+
85+ if isinstance (entry , PlacedBlock ): # assumed (0, 0) at top left
86+ elts [name ] = (entry , (next_x , next_y ))
87+ elif isinstance (entry , NetBlock ): # account for footprint origin, flipping y-axis
88+ bbox = FootprintDataTable .bbox_of (entry .footprint ) or (0 , 0 , 0 , 0 )
89+ elts [name ] = (entry .full_path , (next_x - bbox [0 ], next_y + bbox [3 ]))
90+ x_stack .append ((next_x + width , next_y , next_y + height ))
91+ x_max = max (x_max , next_x + width )
92+ y_max = max (y_max , next_y + height )
93+ return PlacedBlock (
94+ elts = elts , width = x_max , height = y_max
95+ )
96+ return arrange_hierarchy (())
97+
98+
99+ def flatten_packed_block (block : PlacedBlock ) -> Dict [TransformUtil .Path , Tuple [float , float ]]:
100+ """Flatten a packed_block to a dict of individual components."""
101+ flattened : Dict [TransformUtil .Path , Tuple [float , float ]] = {}
102+ def walk_group (block : PlacedBlock , x_pos : float , y_pos : float ) -> None :
103+ for _ , (elt , (elt_x , elt_y )) in block .elts .items ():
104+ if isinstance (elt , PlacedBlock ):
105+ walk_group (elt , x_pos + elt_x , y_pos + elt_y )
106+ elif isinstance (elt , TransformUtil .Path ):
107+ flattened [elt ] = (x_pos + elt_x , y_pos + elt_y )
108+ else :
109+ raise TypeError
110+ walk_group (block , 0 , 0 )
111+ return flattened
112+
113+
11114class SvgPcbGeneratedBlock (NamedTuple ):
12115 path : TransformUtil .Path
13116 fn_name : str
@@ -45,26 +148,15 @@ def run(self) -> List[SvgPcbGeneratedBlock]:
45148 return self ._svgpcb_blocks
46149
47150
48- class SvgPcbCompilerResult (NamedTuple ):
49- functions : list [str ]
50- instantiations : list [str ]
51-
52-
53151class SvgPcbBackend (BaseBackend ):
54152 def run (self , design : CompiledDesign , args : Dict [str , str ] = {}) -> List [Tuple [edgir .LocalPath , str ]]:
55153 netlist = NetlistTransform (design ).run ()
56154 result = self ._generate (design , netlist )
57- if result .functions : # pack layout templates into a file
58- svgpcb_str = ""
59- svgpcb_str += "\n " .join (result .functions )
60- svgpcb_str += "\n " + "\n " .join (result .instantiations )
61- return [
62- (edgir .LocalPath (), svgpcb_str )
63- ]
64- else :
65- return [] # no layout templates, ignore
155+ return [
156+ (edgir .LocalPath (), result )
157+ ]
66158
67- def _generate (self , design : CompiledDesign , netlist : Netlist ) -> SvgPcbCompilerResult :
159+ def _generate (self , design : CompiledDesign , netlist : Netlist ) -> str :
68160 """Generates SVBPCB fragments as a structured result"""
69161 def block_matches_prefixes (block : NetBlock , prefixes : List [Tuple [str , ...]]):
70162 for prefix in prefixes :
@@ -80,21 +172,70 @@ def filter_blocks_by_pathname(blocks: List[NetBlock], exclude_prefixes: List[Tup
80172 svgpcb_block_prefixes = [block .path .to_tuple () for block in svgpcb_blocks ]
81173 netlist = NetlistTransform (design ).run ()
82174 other_blocks = filter_blocks_by_pathname (netlist .blocks , svgpcb_block_prefixes )
175+ arranged_blocks = arrange_netlist (netlist )
176+ pos_dict = flatten_packed_block (arranged_blocks )
83177
84178 svgpcb_block_instantiations = [
85179 f"const { SvgPcbTemplateBlock ._svgpcb_pathname_to_svgpcb (block .path )} = { block .fn_name } (pt(0, 0))"
86180 for block in svgpcb_blocks
87181 ]
88- other_block_instantiations = [
89- f"""\
182+
183+ # note, dimensions in inches
184+ other_block_instantiations = []
185+ for block in other_blocks :
186+ x_pos , y_pos = pos_dict .get (block .full_path , (0 , 0 )) # in mm, need to convert to in below
187+ block_code = f"""\
90188 const { SvgPcbTemplateBlock ._svgpcb_pathname_to_svgpcb (block .full_path )} = board.add({ SvgPcbTemplateBlock ._svgpcb_footprint_to_svgpcb (block .footprint )} , {{
91- translate: pt(0, 0 ), rotate: 0,
189+ translate: pt({ x_pos / 25.4 :.3f } , { y_pos / 25.4 :.3f } ), rotate: 0,
92190 id: '{ SvgPcbTemplateBlock ._svgpcb_pathname_to_svgpcb (block .full_path )} '
93191}})"""
94- for block in other_blocks
95- ]
96-
97- return SvgPcbCompilerResult (
98- [block .svgpcb_code for block in svgpcb_blocks ],
99- svgpcb_block_instantiations + other_block_instantiations
100- )
192+ other_block_instantiations .append (block_code )
193+
194+ NEWLINE = '\n '
195+ full_code = f"""\
196+ const board = new PCB();
197+
198+ { NEWLINE .join (svgpcb_block_instantiations + other_block_instantiations )}
199+
200+ const limit0 = pt(-{ 2 / 25.4 } , -{ 2 / 25.4 } );
201+ const limit1 = pt({ arranged_blocks .width / 25.4 } , { arranged_blocks .height / 25.4 } );
202+ const xMin = Math.min(limit0[0], limit1[0]);
203+ const xMax = Math.max(limit0[0], limit1[0]);
204+ const yMin = Math.min(limit0[1], limit1[1]);
205+ const yMax = Math.max(limit0[1], limit1[1]);
206+
207+ const filletRadius = 0.1;
208+ const outline = path(
209+ [(xMin+xMax/2), yMax],
210+ ["fillet", filletRadius, [xMax, yMax]],
211+ ["fillet", filletRadius, [xMax, yMin]],
212+ ["fillet", filletRadius, [xMin, yMin]],
213+ ["fillet", filletRadius, [xMin, yMax]],
214+ [(xMin+xMax/2), yMax],
215+ );
216+ board.addShape("outline", outline);
217+
218+ renderPCB({{
219+ pcb: board,
220+ layerColors: {{
221+ "F.Paste": "#000000ff",
222+ "F.Mask": "#000000ff",
223+ "B.Mask": "#000000ff",
224+ "componentLabels": "#00e5e5e5",
225+ "outline": "#002d00ff",
226+ "padLabels": "#ffff99e5",
227+ "B.Cu": "#ef4e4eff",
228+ "F.Cu": "#ff8c00cc",
229+ }},
230+ limits: {{
231+ x: [xMin, xMax],
232+ y: [yMin, yMax]
233+ }},
234+ background: "#00000000",
235+ mmPerUnit: 25.4
236+ }})
237+
238+ { NEWLINE .join ([block .svgpcb_code for block in svgpcb_blocks ])}
239+ """
240+
241+ return full_code
0 commit comments