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