Compare commits

...

15 commits

Author SHA1 Message Date
Morgan 'ARR\!' Allen
c1194ecee0 remove Activate() from Syncable subclasses 2025-04-30 20:27:04 -07:00
Morgan 'ARR\!' Allen
cfc3370ad8 make Syncables base class handle selection filtering and dispatching sync to sub Features 2025-04-30 20:25:38 -07:00
Morgan 'ARR\!' Allen
613f959781 move to specific sync_to and sync_from methods 2025-04-30 20:23:20 -07:00
Morgan 'ARR\!' Allen
e81979ad84 add KiConnect Type to Substrate (PartDesign::Body) 2025-04-30 20:23:00 -07:00
Morgan 'ARR\!' Allen
ad679b9ae6 add stub sync methods and isChildOf helper method 2025-04-30 20:21:01 -07:00
Morgan 'ARR\!' Allen
f5d3e26a0b add selection checking to sync buttons 2025-04-30 17:03:18 -07:00
Morgan 'ARR\!' Allen
d17d97f678 Board needs to call super on execute 2025-04-30 16:21:20 -07:00
Morgan 'ARR\!' Allen
c66b554b46 dont try to create Boards if there on no documents on the API 2025-04-30 13:51:56 -07:00
Morgan 'ARR\!' Allen
f1d2800354 add EXTENSIONS to BaseObject class 2025-04-30 13:51:19 -07:00
Morgan 'ARR\!' Allen
09a2daf427 also track out feature on the ViewProviders Proxy object 2025-04-30 13:50:42 -07:00
Morgan 'ARR\!' Allen
527d493c1b rename VIEWPROVIER_EXTENSIONS to just EXTENSIONS, its clear enough which class we're on 2025-04-30 13:50:03 -07:00
Morgan 'ARR\!' Allen
b5a1e8c71d better feedback on API connection status and Documnet availability 2025-04-30 13:29:23 -07:00
Morgan 'ARR\!' Allen
caa3225ef8 notes and minor changes to ping 2025-04-30 13:13:40 -07:00
Morgan 'ARR\!' Allen
e05f3f6a3c major rework of Board making bidirectional syncing more reliable 2025-04-29 10:25:16 -07:00
Morgan 'ARR\!' Allen
8c41e66a1a fix workbench switching for reload command 2025-04-23 14:33:19 -07:00
9 changed files with 175 additions and 48 deletions

View file

@ -2,6 +2,7 @@ import FreeCAD as App
import FreeCADGui as Gui import FreeCADGui as Gui
from kipy import KiCad from kipy import KiCad
from kipy.proto.common.types import DocumentType
from . import settings from . import settings
from .bases import BaseObject, BaseViewProvider from .bases import BaseObject, BaseViewProvider
@ -14,6 +15,7 @@ class APIObject(BaseObject):
feature.addProperty('App::PropertyFile', 'Socket', 'KiConnect', 'Path to the KiCAD Socket File').Socket = '/tmp/kicad/api.lock' feature.addProperty('App::PropertyFile', 'Socket', 'KiConnect', 'Path to the KiCAD Socket File').Socket = '/tmp/kicad/api.lock'
feature.addProperty('App::PropertyBool', 'Connected', 'KiConnect', 'Is socket connected') feature.addProperty('App::PropertyBool', 'Connected', 'KiConnect', 'Is socket connected')
feature.addProperty('App::PropertyInteger', 'DocumentCount', 'KiConnect', 'Count of open Documnets')
self.onDocumentRestored(feature) self.onDocumentRestored(feature)
@ -26,23 +28,49 @@ class APIObject(BaseObject):
parent = feature.getParent() parent = feature.getParent()
if not parent: return if not parent: return
# XXX This gets all of the KiConnect::Board features but then does nothing with them
# future multi-board support?
boards = [ board for board in parent.Group if hasattr(board, 'Type') and board.Type == 'KiConnect::Board' ] boards = [ board for board in parent.Group if hasattr(board, 'Type') and board.Type == 'KiConnect::Board' ]
@property @property
def is_connected(self): def is_connected(self):
'''
Returns connection status
'''
return self.feature.Connected return self.feature.Connected
def ping_connection(self, feature): def ping_connection(self, feature):
'''
Ping the KiCAD API to determine if it's connected
'''
connection_status = 'Disconnected'
document_status = 'No Documents'
try: try:
self.kicad.ping() self.kicad.ping()
feature.Connected = True feature.Connected = True
feature.Label2 = 'Connected' connection_status = 'Connected'
except Exception as e:
feature.Connected = False
connection_status = 'Disconnected'
if feature.Connected:
try:
docs = self.kicad.get_open_documents(DocumentType.DOCTYPE_PCB)
document_status = f'{len(docs)} Documents'
feature.DocumentCount = len(docs)
except Exception as e: except Exception as e:
print(e) print(e)
feature.Connected = False feature.DocumentCount = 0
feature.Label2 = 'Disconnected'
pass feature.Label2 = f'{connection_status} ({document_status})'
return feature.Connected
class APIViewProvider(BaseViewProvider): class APIViewProvider(BaseViewProvider):

View file

@ -1,4 +1,5 @@
class BaseObject: class BaseObject:
EXTENSIONS = []
TYPE = None TYPE = None
def __init__(self, feature): def __init__(self, feature):
@ -13,7 +14,8 @@ class BaseObject:
def execute(self, feature): def execute(self, feature):
# TODO this might not be the right move # TODO this might not be the right move
self.onDocumentRestored(feature) print(self, 'BaseObject.execute')
#self.onDocumentRestored(feature)
def get_api(self): def get_api(self):
p = self.feature p = self.feature
@ -25,6 +27,16 @@ class BaseObject:
return None return None
def isChildOf(self, parent):
p = self.feature
while p:
if p == parent:
return True
p = p.getParent()
return False
def onBeforeChange(self, feature, prop): def onBeforeChange(self, feature, prop):
pass pass
@ -42,5 +54,11 @@ class BaseObject:
def setup_properties(self, feature): def setup_properties(self, feature):
feature.addProperty('App::PropertyString', 'Type', 'KiConnect', 'Internatl KiConnect Type', read_only=True, hidden=True) feature.addProperty('App::PropertyString', 'Type', 'KiConnect', 'Internatl KiConnect Type', read_only=True, hidden=True)
def sync_from(self):
pass
def sync_to(self):
pass
def __getstate__(self): def __getstate__(self):
return None return None

View file

@ -7,10 +7,11 @@ from .. import settings
class BaseViewProvider: class BaseViewProvider:
ICON = None ICON = None
TYPE = None TYPE = None
VIEWPROVIDER_EXTENSIONS = [] EXTENSIONS = []
def __init__(self, viewprovider): def __init__(self, viewprovider):
self.viewprovider = viewprovider self.viewprovider = viewprovider
self.feature = viewprovider.Object.Proxy.feature
self.icon = '' self.icon = ''
@ -22,8 +23,8 @@ class BaseViewProvider:
self.setup_extensions() self.setup_extensions()
def setup_extensions(self): def setup_extensions(self):
if hasattr(self, 'VIEWPROVIDER_EXTENSIONS'): if hasattr(self, 'EXTENSIONS'):
for ext in self.VIEWPROVIDER_EXTENSIONS: for ext in self.EXTENSIONS:
self.feature.addExtension(ext) self.feature.addExtension(ext)
def attach(self, vobj): def attach(self, vobj):

View file

@ -19,23 +19,57 @@ class BoardObject(BaseObject):
super(BoardObject, self).__init__(feature) super(BoardObject, self).__init__(feature)
self.kicad_board = None self.kicad_board = None
self.substrate_body = None
self.substrate_sketch = None
self.via_sketch = None self.via_sketch = None
feature.addProperty('App::PropertyPlacement', 'BoardOffset', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True, read_only=True) feature.addProperty('App::PropertyPlacement', 'BoardOffset', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True, read_only=True)
def onDocumentRestored(self, feature):
def execute(self, feature):
super(BoardObject, self).execute(feature)
if self.kicad_board is None: if self.kicad_board is None:
self.kicad_board = self.get_api().kicad.get_board() self.kicad_board = self.get_api().kicad.get_board()
if self.substrate_body is None: if not self.substrate_body:
self.extrude_substrate(feature) self.create_substrate_body()
self.sketch_outline(feature)
if not self.substrate_sketch:
self.create_substrate_sketch()
self.sketch_outline()
def onDocumentRestored(self, feature):
super(BoardObject, self).onDocumentRestored(feature)
self.kicad_board = self.get_api().kicad.get_board()
@property
def substrate_body(self):
return self.feature.getObject('Substrate')
@property
def substrate_sketch(self):
return self.substrate_body.getObject('Sketch')
def create_substrate_body(self):
substrate_body = App.ActiveDocument.addObject('PartDesign::Body', 'Substrate')
substrate_body.addProperty('App::PropertyString', 'Type', 'KiConnect', 'KiConnect specific Type', read_only=True, hidden=True)
substrate_body.Type = 'KiConnect::BoardBody'
self.feature.addObject(substrate_body)
def create_substrate_sketch(self):
substrate_sketch = App.ActiveDocument.addObject('Sketcher::SketchObject', 'Sketch')
substrate_sketch.Visibility = False
self.substrate_body.addObject(substrate_sketch)
pad = self.substrate_body.newObject('PartDesign::Pad', 'Outline')
pad.Profile = substrate_sketch
pad.Length = 1.6
pad.Midplane = True
if len(self.kicad_board.get_vias()) > 0 and self.via_sketch is None:
self.setup_vias()
self.pocket_vias()
def extrude_substrate(self, feature): def extrude_substrate(self, feature):
self.substrate_sketch = App.ActiveDocument.addObject('Sketcher::SketchObject', 'Sketch') self.substrate_sketch = App.ActiveDocument.addObject('Sketcher::SketchObject', 'Sketch')
@ -95,7 +129,14 @@ class BoardObject(BaseObject):
via_pocket.Midplane = True via_pocket.Midplane = True
via_pocket.Type = 1 via_pocket.Type = 1
def sketch_outline(self, feature): def sketch_outline(self, do_offset=True):
'''
Draws the Board outline fetched from the API
Parameters:
do_offset (bool): If offset should be recalcualted, typically this is undesired after calculated the first time. (Default: True)
'''
boardpoly = self.get_boardpoly() boardpoly = self.get_boardpoly()
poly = boardpoly.polygons[0] poly = boardpoly.polygons[0]
@ -104,13 +145,15 @@ class BoardObject(BaseObject):
self.feature.PolygonId = boardpoly.id.value self.feature.PolygonId = boardpoly.id.value
# this offset centers the board to 0,0 # this offset centers the board to 0,0
if do_offset:
bb = boardpoly.bounding_box() bb = boardpoly.bounding_box()
self.feature.BoardOffset.Base = (App.Vector(bb.pos.x, -bb.pos.y) + App.Vector(bb.size.x, -bb.size.y) / 2) / 1000000.0 self.feature.BoardOffset.Base = (App.Vector(bb.pos.x, -bb.pos.y) + App.Vector(bb.size.x, -bb.size.y) / 2) / 1000000.0
begin = None begin = None
start = None start = None
# reset Sketch Geometry # reset Sketch Constraints and Geometry
self.substrate_sketch.Constraints = []
self.substrate_sketch.Geometry = [] self.substrate_sketch.Geometry = []
# sketch outline # sketch outline
@ -136,7 +179,11 @@ class BoardObject(BaseObject):
Part.LineSegment(start, begin) Part.LineSegment(start, begin)
) )
def sync(self): def sync_from(self):
self.sketch_outline()
self.substrate_body.recompute(True)
def sync_to(self):
board = self.kicad_board board = self.kicad_board
commit = board.begin_commit() commit = board.begin_commit()

View file

@ -0,0 +1,43 @@
import FreeCADGui as Gui
class Syncable:
SYNCABLES = [ 'KiConnect::Project', 'KiConnect::Board', 'KiConnect::Parts', 'KiConnect::BoardBody', ]
def IsActive(self):
sel = Gui.Selection.getSelection()
if len(sel) == 0: return False
for obj in sel:
if obj.Type not in self.SYNCABLES:
return False
return True
def Activated(self):
selection = [ sel for sel in Gui.Selection.getSelection() if sel.Type in self.SYNCABLES ]
syncables = []
if len(selection) == 1:
syncables = selection
else:
syncables = []
for i in selection:
for j in selection:
print(selection)
if i == j: continue
if i in j.OutList:
break
else:
syncables.append(i)
for s in syncables:
if not hasattr(s, 'Proxy'): continue
feature = s.Proxy
if hasattr(feature, self.method):
getattr(feature, self.method)()
s.recompute()

View file

@ -8,7 +8,7 @@ from ..project import Project
class Reload: class Reload:
def GetResources(self): def GetResources(self):
tooltip = '<p>Reload KiConnect Workbench for development.</p>' tooltip = '<p>Reload KiConnect Workbench for development.\nNOTE: Does not reload toolbars.</p>'
iconFile = os.path.join(settings.ICONPATH, 'kiconnect.svg') iconFile = os.path.join(settings.ICONPATH, 'kiconnect.svg')
return { return {
@ -19,15 +19,14 @@ class Reload:
def Activated(self): def Activated(self):
try: try:
Gui.activateWorkbench('Part') Gui.activateWorkbench('PartWorkbench')
except:
print('failed to switch to Part WB') print('failed to switch to Part WB')
except: pass
try: try:
Gui.removeWorkbench('KiConnect') Gui.removeWorkbench('KiConnect')
except: except:
print('failed to remove KiConnect') print('failed to remove KiConnect')
pass
for mod in [mod for mod in sys.modules if 'kicon' in mod]: for mod in [mod for mod in sys.modules if 'kicon' in mod]:
print(f'Reloading {mod}') print(f'Reloading {mod}')

View file

@ -7,7 +7,11 @@ import sys
from .. import settings from .. import settings
from ..project import Project from ..project import Project
class Sync: from .Syncable import Syncable
class SyncFrom(Syncable):
method = 'sync_from'
def GetResources(self): def GetResources(self):
tooltip = '<p>Reload Board from KiCAD.</p>' tooltip = '<p>Reload Board from KiCAD.</p>'
iconFile = os.path.join(settings.ICONPATH, 'import_brd_file.svg') iconFile = os.path.join(settings.ICONPATH, 'import_brd_file.svg')
@ -18,12 +22,4 @@ class Sync:
'Pixmap' : iconFile 'Pixmap' : iconFile
} }
def Activated(self): Gui.addCommand('kiconn_sync_from', SyncFrom())
boards = [ sel for sel in Gui.Selection.getSelection() if sel.Type == 'KiConnect::Board' ]
for board in boards:
board.KiConnBoard.sketch_outline()
App.ActiveDocument.recompute()
Gui.addCommand('kiconn_sync_from', Sync())

View file

@ -6,7 +6,11 @@ import sys
from .. import settings from .. import settings
from ..project import Project from ..project import Project
class Sync: from .Syncable import Syncable
class SyncTo(Syncable):
method = 'sync_to'
def GetResources(self): def GetResources(self):
tooltip = '<p>Update Board in KiCAD.</p>' tooltip = '<p>Update Board in KiCAD.</p>'
iconFile = os.path.join(settings.ICONPATH, 'export_to_pcbnew.svg') iconFile = os.path.join(settings.ICONPATH, 'export_to_pcbnew.svg')
@ -17,13 +21,4 @@ class Sync:
'Pixmap' : iconFile 'Pixmap' : iconFile
} }
def Activated(self): Gui.addCommand('kiconn_sync_to', SyncTo())
boards = [ sel for sel in Gui.Selection.getSelection() if sel.Type == 'KiConnect::Board' ]
for board in boards:
board.KiConnBoard.sync()
Gui.addCommand('kiconn_sync_to', Sync())

View file

@ -28,7 +28,7 @@ class Project:
self.API = api.makeAPI(self.feature) self.API = api.makeAPI(self.feature)
if self.API.Proxy.is_connected: if self.API.Proxy.is_connected and self.API.DocumentCount > 0:
kicad_board = self.API.Proxy.kicad.get_board() kicad_board = self.API.Proxy.kicad.get_board()
self.board = Board(kicad_board, self.feature) self.board = Board(kicad_board, self.feature)