This commit is contained in:
Morgan 'ARR\!' Allen 2024-09-16 11:59:40 -07:00
commit dd31e84d77
5 changed files with 420 additions and 0 deletions

46
PC3B.FCMacro Normal file
View file

@ -0,0 +1,46 @@
import Asm4_libs as Asm4
import importlib
import math
import pcbnew
from pcbnew import VECTOR2I as Vec
import PC3B
import PC3B.Boards
try:
importlib.reload(Asm4)
importlib.reload(PC3B)
importlib.reload(PC3B.Connector)
importlib.reload(PC3B.Connector.Asm4)
importlib.reload(PC3B.Connector.Base)
importlib.reload(PC3B.Boards)
pass
except Exception as e:
import traceback
print(e)
traceback.print_tb(e.__traceback__)
pass
from PC3B.Connector.Asm4 import Asm4Connector, Asm4ConnectionManager
from PC3B.Connector.Base import Connector
from PC3B.Boards import SubBoard, BoardManager
ad = App.ActiveDocument
# TODO bootstrap Asm4 by externally loading the WB
# this prevents `<class 'Base.FreeCADError'>: No such command 'Asm4_makeAssembly'`
# from being raised if this is ran before Asm4 has been loaded
print('starting....')
board = pcbnew.LoadBoard('/home/morgan/mnt/Documents/PCBs/pcbnew_assembly_demo/pcbnew_assembly_demo.kicad_pcb')
groups = list(board.Groups())
ad.openTransaction('pcb')
bm = BoardManager(board, ad, {
'conn_cls': Asm4Connector,
'conn_man_cls': Asm4ConnectionManager,
})
ad.commitTransaction()

217
PC3B/Boards.py Normal file
View file

@ -0,0 +1,217 @@
import importlib
import FreeCAD as App
import Part
from PC3B.Connector.Base import Connector
class SubBoard:
'''
SubBoard is a single outline by Group, distinct from PCBNew Board,
which is tracked by the BoardManager
'''
body = None
part = None
sketch = None
def __init__(self, outline, group, doc, fps, connector_cls=Connector, conn_man=None):
self.conn_man = conn_man
self.connections_by_ref = {}
self.doc = doc
self.group = group
self.group_name = group.GetName()
self.outline = outline
self.create_part()
self.create_body(self.part)
self.doc.recompute()
if self.conn_man and self.conn_man.parts:
self.conn_man.parts.addObject(self.part)
# TODO determine which connection is this boards connected_to
# throw a warning if multiple are found, that may or may not work
for fp in fps:
ref = fp.GetFieldText('Reference')
self.connections_by_ref[ref] = connector_cls(fp, self.doc, parent=self.part, conn_man=self.conn_man, board=self)
def create_body(self, parent=None):
body = App.ActiveDocument.addObject('PartDesign::Body')
self.body = body
if parent:
parent.addObject(body)
sketch = App.ActiveDocument.addObject('Sketcher::SketchObject')
self.sketch = sketch
sketch.Visibility = False
pad = body.newObject('PartDesign::Pad', 'Pad')
pad.Profile = sketch
pad.Length = 1.6
pad.Midplane = True
body.addObject(sketch)
for edge in self.outline:
start = edge.GetStart()
start = (App.Vector(start[0], -start[1])) / 1000000.0
end = edge.GetEnd()
end = (App.Vector(end[0], -end[1])) / 1000000.0
sketch.addGeometry(
Part.LineSegment(
start, end
),
False
)
return body
def create_part(self, parent=None):
print(self.group_name)
self.part = App.ActiveDocument.addObject('App::Part', self.group_name)
self.part.Visibility = False
try:
importlib.reload(SubBoard)
except Exception as e:
print(e)
class BoardManager:
'''
BoardManager takes in a PCBNew Board object and extracts board outlines,
supporting multiple boards by Group.
:param [pcb]: PCBNew Board object
:param [doc]: FreeCAD Document
:param [config]: dict config object
.conn_cls: Class derived from Connector
'''
def __init__(self, pcb, doc, config=None):
# FreeCAD Document
self.doc = doc
# pcbnew.Board
self.pcb = pcb
# SubBoard
self.boards = {}
self.boards_by_connector_ref = {}
self.config = config
self.conn_man = {}
self.connections = {}
self.connector_cls = None
# track all edges by group, these will be handed off to SubBoards after processing
self.board_edges = {}
# all subboards are tracked by their Group UUID (or 0 for ungrouped edges)
self.groups = { g.m_Uuid.AsString(): g for g in pcb.Groups() }
# footprints tracked by pcbnew.FOOTPRINT.Reference (ie; J3, J4, J666)
self.footprints_by_ref = {}
self.footprints_by_group = {}
if self.config:
self.process_config()
# gather all of the edges from the pcbnew.Board and edges contained within footprints
self.process_board_edges()
self.process_footprint_edges()
if self.conn_man:
self.conn_man.create_assembly()
# create SubBoards for each Group
for gid in self.board_edges:
self.boards[gid] = SubBoard(self.board_edges[gid], self.groups[gid], self.doc, self.footprints_by_group[gid], self.connector_cls, self.conn_man)
connections = len(self.boards[gid].connections_by_ref.keys())
print(f'{gid} /w {connections}')
if self.conn_man:
self.conn_man.assemble()
def get_board_by_connection_ref(self, ref):
for bid in self.boards:
board = self.boards[bid]
if ref in board.connections_by_ref:
return board
def get_connection_by_ref(self, ref):
for bid in self.boards:
board = self.boards[bid]
if ref in board.connections_by_ref:
return board.connections_by_ref[ref]
def add_edge(self, edge):
BoardManager.add_item_to_group_dict(edge, self.board_edges)
def process_board_edges(self):
for d in self.pcb.GetDrawings():
if d.GetLayerName() == 'Edge.Cuts':
self.add_edge(d)
def process_config(self):
if 'conn_cls' in self.config:
self.connector_cls = self.config['conn_cls']
else:
self.connector_cls = Connector
if 'conn_man_cls' in self.config:
self.conn_man = self.config['conn_man_cls'](self.doc, board_man=self)
def process_footprint_edges(self):
for f in self.pcb.GetFootprints():
ref = f.GetFieldText('Reference')
group = BoardManager.get_group(f)
gid = group.m_Uuid.AsString() if group else '0'
# also get edges from footprints
drawings = f.GraphicalItems()
for d in drawings:
if d.GetLayerName() == 'Edge.Cuts':
self.add_edge(d)
# track all footprints that have a Connection field
# weather it contains a value or otherwise, it could
# be connect_to only
if f.HasFieldByName('Connection'):
BoardManager.add_item_to_group_dict(f, self.footprints_by_group)
#self.footprints_by_ref[ref] = f
@staticmethod
def add_item_to_group_dict(item, group_dict):
# get the group the edge belongs to
group = BoardManager.get_group(item)
# use the group uuid, or 0 if none
# WARN multiple separate boards cannot exist ungroup, it will throw a
# multiple solids error
gid = group.m_Uuid.AsString() if group else '0'
# create per-board array to track edges
if gid not in group_dict:
group_dict[gid] = []
group_dict[gid].append(item)
'''
Upward traversal to determine what, if any group an object is in
:param item: Any valid PCBNew object
:return: The Group object or None if not a member of a group
'''
@staticmethod
def get_group(item):
if item is None: return
group = item.GetParentGroup()
if group is not None:
return group
else:
return BoardManager.get_group(item.GetParent())

107
PC3B/Connector/Asm4.py Normal file
View file

@ -0,0 +1,107 @@
import Asm4_libs as Asm4
import FreeCAD
import FreeCADGui as Gui
import math
from .Base import Connector, ConnectionManager
TO_RAD = 180 / math.pi
class Asm4ConnectionManager(ConnectionManager):
root_assembly = None
parts = None
lcs_origin = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def assemble(self):
for ref in self.connections_by_ref:
connection = self.connections_by_ref[ref]
connection.attach()
def create_assembly(self):
Gui.runCommand('Asm4_newAssembly', 0)
self.root_assembly = self.doc.getObject('Assembly')
self.lcs_origin = self.root_assembly.getObject('LCS_Origin')
self.parts = self.doc.getObject('Parts')
class Asm4Connector(Connector):
def __init__(self, *args, **kargs):
self.lcs = None
super().__init__(*args, **kargs)
self.create_lcs()
self.create_link()
def attach(self):
if not self.connected_to: return
print(self.reference, self.connected_to)
connected_to = self.conn_man.get_connection_by_ref(self.connected_to)
connected_to_board = self.conn_man.board_man.get_board_by_connection_ref(self.connected_to)
if not connected_to or not connected_to_board: return
print(connected_to, connected_to_board)
Asm4.makeAsmProperties(self.parent)
if self.connected_to == 'root':
a_lcs = self.conn_man.lcs_origin.Name
a_link = 'Parent Assembly'
a_part = None
l_part = self.doc.Name
else:
a_lcs = connected_to.lcs.Name
print(a_lcs)
a_link = connected_to_board.part.Name
a_part = self.doc.Name
l_part = self.doc.Name
self.link.AttachedBy = f'#{self.lcs.Name}'
self.link.AttachedTo = f'{a_link}#{a_lcs}'
self.link.SolverId = 'Asm4EE'
expr = f'{connected_to_board.part.Name}.Placement * {connected_to.lcs.Name}.Placement * AttachmentOffset * {self.lcs.Name}.Placement ^ -1'
print(expr)
self.link.setExpression('Placement', expr)
def create_lcs(self):
pos = self.footprint.GetPosition()
ref = self.footprint.GetFieldText('Reference')
rot = self.footprint.GetOrientationDegrees()
lcs = self.doc.addObject('PartDesign::CoordinateSystem', f'LCS_{ref}')
lcs.Visibility = False
lcs.Placement.Base = FreeCAD.Vector(pos[0], -pos[1]) / 1000000.0
self.lcs = lcs
if self.parent:
self.parent.addObject(lcs)
if rot != 0:
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', rot)
rotation = FreeCAD.Rotation()
rotation.Axis = (0, 0, 1)
rotation.Angle = float(math.pi / (180 / rot))
print(rotation)
print(rotation.Angle)
lcs.Placement.Rotation *= rotation
if not self.board.body.Shape.isInside(lcs.Placement.Base, 0.001, True):
rotation = FreeCAD.Rotation()
rotation.Axis = (0, 1, 0)
rotation.Angle = math.pi / 2
lcs.Placement.Rotation *= rotation
def create_link(self):
if self.connected_to:
connected_to = self.conn_man.get_connection_by_ref(self.connected_to)
self.link = self.conn_man.root_assembly.newObject('App::Link', f'{self.board.group_name}_{self.reference}_{self.connected_to}')
self.link.LinkedObject = self.parent

50
PC3B/Connector/Base.py Normal file
View file

@ -0,0 +1,50 @@
import FreeCAD
class ConnectionManager:
def __init__(self, doc, board_man=None):
self.doc = doc
self.board_man = board_man
self.connections_by_ref = {}
def add_connection_by_ref(self, ref, connection):
self.connections_by_ref[ref] = connection
print(f'adding {ref}', connection)
def get_connection_by_ref(self, ref):
return self.connections_by_ref[ref] if ref in self.connections_by_ref else None
def assemble(self):
raise NotImplementedError()
def create_assembly(self):
raise NotImplementedError()
class Connector:
board = None
def __init__(self, footprint, doc, parent=None, conn_man=None, board=None):
self.board = board
self.conn_man = conn_man
self.connected_to = footprint.GetFieldText('Connection')
self.connection_from = None
self.doc = doc
# footprint is pcbnew.FOOTPRINT
self.footprint = footprint
self.group = None
self.link = None
self.parent = parent
self.reference = footprint.GetFieldText('Reference')
# connections can be to, from or both
if self.conn_man:
self.conn_man.add_connection_by_ref(self.reference, self)
def assemble(self):
raise NotImplementedError()
def __str__(self):
if self.connection_from:
return f'Connector: {self.reference} <- {self.connection_from}'
else:
return f'Connector: {self.reference} -> {self.connected_to}'

0
PC3B/__init__.py Normal file
View file