pc3b
This commit is contained in:
		
						commit
						dd31e84d77
					
				
					 5 changed files with 420 additions and 0 deletions
				
			
		
							
								
								
									
										46
									
								
								PC3B.FCMacro
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								PC3B.FCMacro
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										217
									
								
								PC3B/Boards.py
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										107
									
								
								PC3B/Connector/Asm4.py
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										50
									
								
								PC3B/Connector/Base.py
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										0
									
								
								PC3B/__init__.py
									
										
									
									
									
										Normal file
									
								
							
		Loading…
	
	Add table
		
		Reference in a new issue