Skip to content

Instantly share code, notes, and snippets.

@palaniraja
Last active June 20, 2026 03:43
Show Gist options
  • Select an option

  • Save palaniraja/fc53d1b2caa8646f86015aebc27f29e9 to your computer and use it in GitHub Desktop.

Select an option

Save palaniraja/fc53d1b2caa8646f86015aebc27f29e9 to your computer and use it in GitHub Desktop.
Freecad macro to generate Side flexing hook and mate from two selected parallel edges
import FreeCAD as App
import FreeCADGui as Gui
import Part
from FreeCAD import Vector
# Cross-version PySide wrapper to handle old and new FreeCAD environments seamlessly
try:
from PySide6 import QtWidgets, QtCore
except ImportError:
try:
from PySide2 import QtWidgets, QtCore
except ImportError:
from PySide import QtWidgets, QtCore
# =========================================================================
# GEOMETRY ENGINE PROXIES (Data Tab Properties & Math)
# =========================================================================
class SnapFitPositiveProxy:
def __init__(self, obj):
obj.Proxy = self
def execute(self, obj):
w = obj.ClipWidth.Value
l = obj.HookLength.Value
ov = obj.Overlap.Value
b = obj.Bite.Value
sw = obj.SlotWidth.Value
t = obj.WallThickness.Value
off = obj.PositionOffset.Value
inv = obj.InvertVertical
ct = t * 0.5 # Hook thickness is 50% of total wall thickness
if w <= 0 or l <= 0:
return
ym = -1.0 if inv else 1.0
# Build Prong Profile
pts = [
Vector(-w/2 + off, ov * ym, 0),
Vector(w/2 + off, ov * ym, 0),
Vector(w/2 + off, (-l + 2) * ym, 0),
Vector(w/2 + b + off, (-l + 1) * ym, 0),
Vector(w/2 - 0.4 + off, -l * ym, 0),
Vector(-w/2 + 0.4 + off, -l * ym, 0),
Vector(-w/2 - b + off, (-l + 1) * ym, 0),
Vector(-w/2 + off, (-l + 2) * ym, 0),
Vector(-w/2 + off, ov * ym, 0)
]
f_h = Part.Face(Part.makePolygon(pts))
pts_s = [
Vector(-sw/2 + off, 0.1 * ym, 0),
Vector(sw/2 + off, 0.1 * ym, 0),
Vector(sw/2 + off, (-l - 0.1) * ym, 0),
Vector(-sw/2 + off, (-l - 0.1) * ym, 0),
Vector(-sw/2 + off, 0.1 * ym, 0)
]
f_s = Part.Face(Part.makePolygon(pts_s))
prof = f_h.cut(f_s)
# Foolproof centering: Shift the face geometry back by half its thickness before extruding
shift_mat = App.Matrix()
shift_mat.move(Vector(0, 0, -ct / 2))
prof = prof.transformGeometry(shift_mat)
# Extrude from -ct/2 to +ct/2, centering it perfectly on the origin
sol = prof.extrude(Vector(0, 0, ct))
obj.Shape = sol
class SnapFitNegativeProxy:
def __init__(self, obj):
obj.Proxy = self
def execute(self, obj):
w = obj.ClipWidth.Value
l = obj.HookLength.Value
ov = obj.Overlap.Value
b = obj.Bite.Value
cl = obj.Clearance.Value
t = obj.WallThickness.Value
off = obj.PositionOffset.Value
inv = obj.InvertVertical
ct = t * 0.5
total_thick = ct + cl * 2
if w <= 0 or l <= 0:
return
ym = -1.0 if inv else 1.0
# Build Clearance Cutout Tool Profile
pts = [
Vector(-w/2 - cl + off, ov * ym, 0),
Vector(w/2 + cl + off, ov * ym, 0),
Vector(w/2 + cl + off, (-l + 2.1) * ym, 0),
Vector(w/2 + b + cl + off, (-l + 1.1) * ym, 0),
Vector(w/2 + b + cl + off, (-l - cl) * ym, 0),
Vector(-w/2 - b - cl + off, (-l - cl) * ym, 0),
Vector(-w/2 - b - cl + off, (-l + 1.1) * ym, 0),
Vector(-w/2 - cl + off, (-l + 2.1) * ym, 0),
Vector(-w/2 - cl + off, ov * ym, 0)
]
f_c = Part.Face(Part.makePolygon(pts))
# Foolproof centering for cutout: Shift back by half of the total cleared thickness
shift_mat = App.Matrix()
shift_mat.move(Vector(0, 0, -total_thick / 2))
f_c = f_c.transformGeometry(shift_mat)
sol = f_c.extrude(Vector(0, 0, total_thick))
obj.Shape = sol
# =========================================================================
# VIEWPORT RENDER PROXIES
# =========================================================================
class SnapFitViewProxy:
def __init__(self, vobj):
vobj.Proxy = self
def getDisplayModes(self, obj):
return ["Flat Lines", "Shaded", "Wireframe"]
def getDefaultDisplayMode(self):
return "Flat Lines"
def updateData(self, obj, prop):
return
# =========================================================================
# CONFIGURATION INTERFACE (UI)
# =========================================================================
class SnapFitGeneratorDialog(QtWidgets.QDialog):
def __init__(self, e1, e2):
super(SnapFitGeneratorDialog, self).__init__()
self.e1 = e1
self.e2 = e2
self.init_ui()
def init_ui(self):
self.setWindowTitle("Parametric Snap Creator")
form = QtWidgets.QFormLayout()
self.sb_w = QtWidgets.QDoubleSpinBox()
self.sb_w.setValue(8.0)
form.addRow("Width:", self.sb_w)
self.sb_l = QtWidgets.QDoubleSpinBox()
self.sb_l.setValue(5.0)
form.addRow("Length (Depth):", self.sb_l)
self.sb_ov = QtWidgets.QDoubleSpinBox()
self.sb_ov.setValue(2.0)
form.addRow("Mating Overlap:", self.sb_ov)
self.sb_b = QtWidgets.QDoubleSpinBox()
self.sb_b.setValue(0.5)
form.addRow("Locking Bite:", self.sb_b)
self.sb_sw = QtWidgets.QDoubleSpinBox()
self.sb_sw.setValue(1.5)
form.addRow("Flex Slot Width:", self.sb_sw)
self.sb_cl = QtWidgets.QDoubleSpinBox()
self.sb_cl.setValue(0.15)
form.addRow("Clearance Tolerance:", self.sb_cl)
self.sb_off = QtWidgets.QDoubleSpinBox()
self.sb_off.setRange(-999999.0, 999999.0) # Unbounded movement limit
self.sb_off.setValue(0.0)
form.addRow("Position Offset:", self.sb_off)
self.cb_inv = QtWidgets.QCheckBox()
self.cb_inv.setChecked(False)
form.addRow("Invert Vertical Direction", self.cb_inv)
QBB = QtWidgets.QDialogButtonBox
btn = QBB(QBB.Ok | QBB.Cancel)
btn.accepted.connect(self.generate)
btn.rejected.connect(self.reject)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(form)
layout.addWidget(btn)
self.setLayout(layout)
def generate(self):
v1_s = self.e1.Vertexes[0].Point
v1_e = self.e1.Vertexes[-1].Point
v2_s = self.e2.Vertexes[0].Point
v2_e = self.e2.Vertexes[-1].Point
m1 = (v1_s + v1_e) * 0.5
m2 = (v2_s + v2_e) * 0.5
u_len = (v1_e - v1_s).normalize()
v_across = m2 - m1
u_height = u_len.cross(v_across).normalize()
u_thick = u_height.cross(u_len).normalize()
pos1_s = 0.0
pos1_e = (v1_e - v1_s).dot(u_len)
pos2_s = (v2_s - v1_s).dot(u_len)
pos2_e = (v2_e - v1_s).dot(u_len)
min1, max1 = min(pos1_s, pos1_e), max(pos1_s, pos1_e)
min2, max2 = min(pos2_s, pos2_e), max(pos2_s, pos2_e)
overlap_start = max(min1, min2)
overlap_end = min(max1, max2)
if overlap_end > overlap_start:
center_x = (overlap_start + overlap_end) * 0.5
else:
center_x = (min1 + max1 + min2 + max2) * 0.25
wall_t = abs(v_across.dot(u_thick))
center_z = v_across.dot(u_thick) * 0.5
center = v1_s + (u_len * center_x) + (u_thick * center_z)
mat = App.Matrix()
mat.A11 = u_len.x; mat.A12 = u_height.x; mat.A13 = u_thick.x; mat.A14 = center.x
mat.A21 = u_len.y; mat.A22 = u_height.y; mat.A23 = u_thick.y; mat.A24 = center.y
mat.A31 = u_len.z; mat.A32 = u_height.z; mat.A33 = u_thick.z; mat.A34 = center.z
tp = App.Placement(mat)
doc = App.activeDocument()
o_h = doc.addObject("Part::FeaturePython", "Parametric_Snap_Hook")
o_c = doc.addObject("Part::FeaturePython", "Parametric_Snap_Cutout")
import SideFlexSnapFit
o_h.Proxy = SideFlexSnapFit.SnapFitPositiveProxy(o_h)
o_c.Proxy = SideFlexSnapFit.SnapFitNegativeProxy(o_c)
if App.GuiUp:
o_h.ViewObject.Proxy = SideFlexSnapFit.SnapFitViewProxy(o_h.ViewObject)
o_c.ViewObject.Proxy = SideFlexSnapFit.SnapFitViewProxy(o_c.ViewObject)
for o in [o_h, o_c]:
o.addProperty("App::PropertyLength", "ClipWidth", "SnapFit")
o.ClipWidth = self.sb_w.value()
o.addProperty("App::PropertyLength", "HookLength", "SnapFit")
o.HookLength = self.sb_l.value()
o.addProperty("App::PropertyLength", "Overlap", "SnapFit")
o.Overlap = self.sb_ov.value()
o.addProperty("App::PropertyLength", "Bite", "SnapFit")
o.Bite = self.sb_b.value()
o.addProperty("App::PropertyLength", "SlotWidth", "SnapFit")
o.SlotWidth = self.sb_sw.value()
o.addProperty("App::PropertyLength", "Clearance", "SnapFit")
o.Clearance = self.sb_cl.value()
o.addProperty("App::PropertyLength", "WallThickness", "SnapFit")
o.WallThickness = wall_t
o.addProperty("App::PropertyDistance", "PositionOffset", "SnapFit")
o.PositionOffset = self.sb_off.value()
o.addProperty("App::PropertyBool", "InvertVertical", "SnapFit")
o.InvertVertical = self.cb_inv.isChecked()
o.Placement = tp
o.touch()
if App.GuiUp:
o_h.ViewObject.ShapeColor = (1.0, 0.8, 0.2) # yellow
o_c.ViewObject.ShapeColor = (1.0, 0.3, 0.2) # red
o_h.ViewObject.Transparency = 50
o_c.ViewObject.Transparency = 70
o_h.ViewObject.Visibility = True
o_c.ViewObject.Visibility = True
doc.recompute()
self.accept()
def run_macro():
edges = []
for sel in Gui.Selection.getSelectionEx():
for sub in sel.SubObjects:
if sub.ShapeType == "Edge":
edges.append(sub)
if len(edges) != 2:
QtWidgets.QMessageBox.critical(None, "Error", "Select exactly TWO parallel wall edges.")
return
dialog = SnapFitGeneratorDialog(edges[0], edges[1])
dialog.exec()
# FIXED: Execution Guard protects module imports from re-running the macro window loop
if __name__ == "__main__":
run_macro()
@palaniraja

Copy link
Copy Markdown
Author
# Bundle objects in a single asset container for clean copy/paste/link tracking
instance_part = doc.addObject("App::Part", "SnapFit_Instance")

o_h = doc.addObject("Part::FeaturePython", "Parametric_Snap_Hook")
SnapFitPositiveProxy(o_h)
o_c = doc.addObject("Part::FeaturePython", "Parametric_Snap_Cutout")
SnapFitNegativeProxy(o_c)

# Link children under the structural container node
instance_part.addObject(o_h)
instance_part.addObject(o_c)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment