Compare commits

...

11 commits

Author SHA1 Message Date
Morgan 'ARR\!' Allen
984cb42b1c wip towards SketchObjectPython feature for board outline
This is helpful for preventing "sketch has been touhed after recompute type errors, which seem to occur because the sketch has been computed before the Board, which was doing the Sketch updates.
2025-05-16 22:43:39 -07:00
Morgan 'ARR\!' Allen
e1a0301e35 track board thickness on Board Feature 2025-05-16 18:05:46 -07:00
Morgan 'ARR\!' Allen
0e5f275b97 quickish fix to prevent board from jumping around when overall size changes 2025-05-16 18:05:34 -07:00
Morgan 'ARR\!' Allen
3a40276260 remove prints 2025-05-16 18:05:10 -07:00
Morgan 'ARR\!' Allen
da98f7ba01 part of eventual multiboard support 2025-05-16 17:52:59 -07:00
Morgan 'ARR\!' Allen
961b0b44f3 major flow rearrangement, better separation of syncing from KiCAD and FreeCAD executing/drawing. still more to go 2025-05-16 17:52:17 -07:00
Morgan 'ARR\!' Allen
6d6ccadccf now sync_from on init of all BaseObject classes 2025-05-16 17:51:18 -07:00
Morgan 'ARR\!' Allen
ac84562e1a use setup_properties method on Board 2025-05-16 17:45:24 -07:00
Morgan 'ARR\!' Allen
a5dbdee3c8 fixed type 2025-05-16 17:43:19 -07:00
Morgan 'ARR\!' Allen
5b73f2837a create the kicad api object earlier on 2025-05-16 17:43:02 -07:00
Morgan 'ARR\!' Allen
c4e8e79395 .gitignore 2025-05-01 18:47:30 -07:00
6 changed files with 168 additions and 105 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__

View file

@ -11,6 +11,8 @@ class APIObject(BaseObject):
TYPE = 'KiConnect::API'
def __init__(self, feature):
self.kicad = KiCad()
super(APIObject, self).__init__(feature)
feature.addProperty('App::PropertyFile', 'Socket', 'KiConnect', 'Path to the KiCAD Socket File').Socket = '/tmp/kicad/api.lock'
@ -19,6 +21,8 @@ class APIObject(BaseObject):
self.onDocumentRestored(feature)
self.ping_connection(feature)
def onDocumentRestored(self, feature):
super(APIObject, self).onDocumentRestored(feature)

View file

@ -12,6 +12,8 @@ class BaseObject:
feature.Type = self.TYPE
self.sync_from()
def execute(self, feature):
# TODO this might not be the right move
print(self, 'BaseObject.execute')
@ -52,7 +54,7 @@ class BaseObject:
self.feature.addExtension(ext)
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', 'Internal KiConnect Type', read_only=True, hidden=True)
def sync_from(self):
pass

View file

@ -10,45 +10,48 @@ from kipy.proto.common.types import KiCadObjectType
from kipy.util.board_layer import BoardLayer
from .bases import BaseObject, BaseViewProvider
from . import parts
from . import parts as Parts
from . import board_sketch as BoardSketch
class BoardObject(BaseObject):
TYPE = 'KiConnect::Board'
def __init__(self, feature):
super(BoardObject, self).__init__(feature)
def __init__(self, feature, kicad_board, board_polygon):
self.feature = feature
self.kicad_board = kicad_board
# TODO add this to FreeCAD Preferences and Property?
self.do_offset = True
# TODO needs to be resotred in onDocumentRestored
#self.board_polygon = board_polygon
self.polygon_id = board_polygon.id.value
self.kicad_board = None
self.via_sketch = None
feature.addProperty('App::PropertyPlacement', 'BoardOffset', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True, read_only=True)
super(BoardObject, self).__init__(feature)
self.feature.PolygonId = board_polygon.id.value
def execute(self, feature):
super(BoardObject, self).execute(feature)
self.board_sketch = BoardSketch.makeBoardSketch(self.substrate_body, kicad_board, board_polygon)
self.create_substrate_pad()
if self.kicad_board is None:
self.kicad_board = self.get_api().kicad.get_board()
if not self.substrate_body:
self.create_substrate_body()
if not self.substrate_sketch:
self.create_substrate_sketch()
self.sketch_outline()
@property
def board_polygon(self):
return [ poly for poly in extract_polygons(self.kicad_board) if poly.id.value == self.polygon_id ][0]
def onDocumentRestored(self, feature):
super(BoardObject, self).onDocumentRestored(feature)
self.board_sketch = self.feature.get('BoardSketch')
self.kicad_board = self.get_api().kicad.get_board()
@property
def substrate_body(self):
return self.feature.getObject('Substrate')
body = self.feature.getObject('Substrate')
@property
def substrate_sketch(self):
return self.substrate_body.getObject('Sketch')
if not body:
body = self.create_substrate_body()
return body
def create_substrate_body(self):
substrate_body = App.ActiveDocument.addObject('PartDesign::Body', 'Substrate')
@ -58,35 +61,19 @@ class BoardObject(BaseObject):
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)
return substrate_body
def create_substrate_pad(self):
pad = self.substrate_body.newObject('PartDesign::Pad', 'Outline')
pad.Profile = substrate_sketch
pad.Profile = self.board_sketch
pad.Length = 1.6
pad.Midplane = True
def extrude_substrate(self, feature):
self.substrate_sketch = App.ActiveDocument.addObject('Sketcher::SketchObject', 'Sketch')
self.substrate_sketch.Visibility = False
self.substrate_body = App.ActiveDocument.addObject('PartDesign::Body', 'Substrate')
self.feature.addObject(self.substrate_body)
pad = self.substrate_body.newObject('PartDesign::Pad', 'Outline')
pad.Profile = self.substrate_sketch
pad.Length = 1.6
pad.Midplane = True
self.substrate_body.addObject(self.substrate_sketch)
return self.board_sketch
def get_boardpoly(self):
# TODO remove in favor of extract_polygons class method
board = self.kicad_board
# find polygons of Edge Cuts
@ -129,59 +116,29 @@ class BoardObject(BaseObject):
via_pocket.Midplane = True
via_pocket.Type = 1
def sketch_outline(self, do_offset=True):
'''
Draws the Board outline fetched from the API
def point_to_vector(self, point, offset=True):
return (
App.Vector(point.x,
-point.y
)) / 1000000.0 - (self.feature.BoardOffset.Base if offset else 0)
Parameters:
do_offset (bool): If offset should be recalcualted, typically this is undesired after calculated the first time. (Default: True)
'''
boardpoly = self.get_boardpoly()
poly = boardpoly.polygons[0]
# track KIID
# TODO FeaturePython objects interacting with kipy objects should inherit common parent class
self.feature.PolygonId = boardpoly.id.value
# this offset centers the board to 0,0
if do_offset:
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
begin = None
start = None
# reset Sketch Constraints and Geometry
self.substrate_sketch.Constraints = []
self.substrate_sketch.Geometry = []
# sketch outline
for segment in poly.outline:
if not start:
start = (App.Vector(segment.point.x, -segment.point.y)) / 1000000.0 - self.feature.BoardOffset.Base
# needs to remain to connect the last segment
begin = start
continue
end = (App.Vector(segment.point.x, -segment.point.y)) / 1000000.0 - self.feature.BoardOffset.Base
self.substrate_sketch.addGeometry(
Part.LineSegment(start, end)
)
start = end
# make final connection
self.substrate_sketch.addGeometry(
Part.LineSegment(start, begin)
)
def sync_from(self):
self.sketch_outline()
self.substrate_body.recompute(True)
'''
Pulls outline from KiCAD PolygonBoard and saves points as Vectors
'''
# bit of a quick hack to keep the board in one place, needs more testing
if self.do_offset and not self.feature.BoardOffset:
bb = self.board_polygon.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
vectors = []
for node in self.board_polygon.polygons[0].outline:
vectors.append(self.point_to_vector(node.point))
self.feature.Vectors = vectors
def sync_to(self):
board = self.kicad_board
@ -212,6 +169,22 @@ class BoardObject(BaseObject):
# maybe save the selection and restore if not empty, else select the poly as below
#board.add_to_selection(boardpoly)
def setup_properties(self, feature):
super(BoardObject, self).setup_properties(feature)
kicad_board = self.kicad_board
feature.addProperty('App::PropertyPlacement', 'BoardOffset', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True, read_only=True)
feature.addProperty('App::PropertyString', 'Doc', 'KiConnect', 'Doc in project to sync with', read_only=True)
feature.addProperty('App::PropertyString', 'PolygonId', 'KiConnect', 'Polygon ID for the original outline', hidden=True, read_only=True)
feature.addProperty('App::PropertyLength', 'Thickness', 'KiConnect', 'Thickness of PCB', hidden=True, read_only=True)
feature.addProperty('App::PropertyVectorList', 'Vectors', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True)
feature.Doc = kicad_board.name
if not feature.Thickness:
feature.Thickness = 1.6
def __getstate__(self):
return None
@ -219,18 +192,22 @@ class BoardViewProvider(BaseViewProvider):
TYPE = 'KiConnect::Board'
ICON = 'board.svg'
class Board:
def __init__(self, kicad_board, parent):
def makeBoard(parent, kicad_board, polygon):
feature = App.ActiveDocument.addObject('App::DocumentObjectGroupPython', 'Board')
parent.addObject(feature)
self.feature = feature
BoardObject(feature)
BoardObject(feature, kicad_board, polygon)
BoardViewProvider(feature.ViewObject)
# TODO move into BoardObject
feature.addProperty('App::PropertyString', 'Doc', 'KiConnect', 'Doc in project to sync with', read_only=True).Doc = kicad_board.name
feature.addProperty('App::PropertyString', 'PolygonId', 'KiConnect', 'Polygon ID for the original outline', hidden=True, read_only=True)
Parts.makeParts(feature)
parts.makeParts(feature)
return feature
def extract_polygons(board):
# find polygons of Edge Cuts
edge_cuts = [ edge for edge in board.get_shapes() if edge.layer == BoardLayer.BL_Edge_Cuts ]
polygons = [ edge for edge in edge_cuts if isinstance(edge, BoardPolygon) ]
# XXX only single board supported at the moment
return polygons

View file

@ -0,0 +1,77 @@
import os
import FreeCAD as App
from . import settings
import Part
from kipy.board_types import Footprint3DModel, BoardPolygon, BoardSegment, PadStackShape
from kipy.geometry import PolygonWithHoles, PolyLine, PolyLineNode, Vector2
from kipy.proto.common.types import KiCadObjectType
from kipy.util.board_layer import BoardLayer
from .bases import BaseObject, BaseViewProvider
class BoardSketchObject(BaseObject):
TYPE = 'KiConnect::BoardSketch'
def __init__(self, feature, kicad_board, board_polygon):
super(BoardSketchObject, self).__init__(feature)
#feature.Visibility = False
vectors = []
for node in board_polygon.polygons[0].outline:
vectors.append(self.point_to_vector(node.point))
self.feature.Vectors = vectors
def execute(self, feature):
begin = None
start = None
# this probably needs a bit more control..
feature.Constraints = []
feature.Geometry = []
for vector in self.feature.Vectors:
if not start:
start = vector
begin = vector
continue
feature.addGeometry(Part.LineSegment(start, vector))
start = vector
feature.addGeometry(Part.LineSegment(start, begin))
feature.recompute()
def point_to_vector(self, point, offset=True):
return (
App.Vector(point.x,
-point.y
)) / 1000000.0 - (self.feature.BoardOffset.Base if offset else 0)
def setup_properties(self, feature):
super(BoardSketchObject, self).setup_properties(feature)
feature.addProperty('App::PropertyPlacement', 'BoardOffset', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True, read_only=True)
feature.addProperty('App::PropertyVectorList', 'Vectors', 'KiConnect', 'Internal offset for zeroing out Footprint offset', hidden=True)
def __getstate__(self):
return None
class BoardSketchViewProvider(BaseViewProvider):
TYPE = 'KiConnect::BoardSketch'
#ICON = 'board.svg'
def makeBoardSketch(parent, kicad_board, polygon):
feature = App.ActiveDocument.addObject('Sketcher::SketchObjectPython', 'BoardSketch')
parent.addObject(feature)
BoardSketchObject(feature, kicad_board, polygon)
BoardSketchViewProvider(feature.ViewObject)
return feature

View file

@ -9,7 +9,7 @@ from . import settings
from . import api
from .copper import Copper
from .board import Board
from . import board as Board
#from .parts import Parts
class Project:
@ -31,7 +31,9 @@ class Project:
if self.API.Proxy.is_connected and self.API.DocumentCount > 0:
kicad_board = self.API.Proxy.kicad.get_board()
self.board = Board(kicad_board, self.feature)
polygons = Board.extract_polygons(kicad_board)
for polygon in polygons:
self.board = Board.makeBoard(self.feature, kicad_board, polygon)
#self.copper = Copper(kicad_board, self.board)
#self.board.feature.addObject(self.copper.feature)