commit dd31e84d774f91eae2d02a0a14fedcaf536ad751 Author: Morgan 'ARR\!' Allen Date: Mon Sep 16 11:59:40 2024 -0700 pc3b diff --git a/PC3B.FCMacro b/PC3B.FCMacro new file mode 100644 index 0000000..0f28645 --- /dev/null +++ b/PC3B.FCMacro @@ -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 `: 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() diff --git a/PC3B/Boards.py b/PC3B/Boards.py new file mode 100644 index 0000000..d5af5d3 --- /dev/null +++ b/PC3B/Boards.py @@ -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()) + + diff --git a/PC3B/Connector/Asm4.py b/PC3B/Connector/Asm4.py new file mode 100644 index 0000000..f12aacd --- /dev/null +++ b/PC3B/Connector/Asm4.py @@ -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 + diff --git a/PC3B/Connector/Base.py b/PC3B/Connector/Base.py new file mode 100644 index 0000000..82e7e79 --- /dev/null +++ b/PC3B/Connector/Base.py @@ -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}' + diff --git a/PC3B/__init__.py b/PC3B/__init__.py new file mode 100644 index 0000000..e69de29