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…
Reference in a new issue