3 Commits

Author SHA1 Message Date
forbes
04f9df75cb fix: update AttachmentOffset during angled datum editing (#66)
on_param_changed() now recomputes AttachmentOffset.Rotation for angled
datums and recalculates Placement for tangent_cylinder datums when the
angle spinner changes. Previously only ZTools_Params was updated,
leaving the visual representation unchanged until a manual recompute.

Add _resolve_source_refs() helper to parse ZTools_SourceRefs and
resolve stored object/subname pairs to actual shapes for the rotation
and placement math.
2026-02-08 18:42:28 -06:00
forbes
2132c4e64c fix: use append instead of fragile insert chain in PartDesign menu (#57)
Replace the chained insert operations in modifyMenuBar() with independent
append operations. The old approach anchored on PartDesign_Boolean and
chained each subsequent command off the previous insertion — a single
missing anchor caused a cascade failure.

The new approach uses append with PartDesign_Body as the parent locator.
Each operation is independent, so a failure in one does not affect the
others.
2026-02-08 18:07:18 -06:00
forbes
12e332240a fix: register commands and manipulator at module scope (#52)
Move command imports and PartDesign manipulator installation from
ZToolsWorkbench.Initialize() to module scope. This ensures commands
are registered and the manipulator is available before any workbench
activates, fixing the case where PartDesign activates before ZTools
and ztools buttons never appear.
2026-02-08 17:57:54 -06:00
2 changed files with 81 additions and 37 deletions

View File

@@ -66,13 +66,8 @@ class ZToolsWorkbench(Gui.Workbench):
except Exception:
pass
from ztools.commands import (
assembly_pattern_commands,
datum_commands,
pattern_commands,
pocket_commands,
spreadsheet_commands,
)
# Command imports moved to module scope (after Gui.addWorkbench) so they
# are available before Initialize() runs. See end of file.
# =====================================================================
# PartDesign Structure Tools
@@ -306,12 +301,6 @@ class ZToolsWorkbench(Gui.Workbench):
+ self.ztools_spreadsheet_tools,
)
# Register the PartDesign manipulator now that commands exist.
# Guard so it only registers once even if Initialize is called again.
if not getattr(ZToolsWorkbench, "_manipulator_installed", False):
ZToolsWorkbench._manipulator_installed = True
Gui.addWorkbenchManipulator(_ZToolsPartDesignManipulator())
App.Console.PrintMessage("ztools workbench initialized\n")
def Activated(self):
@@ -337,11 +326,24 @@ class ZToolsWorkbench(Gui.Workbench):
Gui.addWorkbench(ZToolsWorkbench())
# ---------------------------------------------------------------------------
# Eager command registration
# ---------------------------------------------------------------------------
# Import command modules at module scope so Gui.addCommand() calls run before
# any workbench activates. This ensures the PartDesign manipulator can
# reference them regardless of workbench activation order (#52).
from ztools.commands import (
assembly_pattern_commands,
datum_commands,
pattern_commands,
pocket_commands,
spreadsheet_commands,
)
# ---------------------------------------------------------------------------
# WorkbenchManipulator: inject ZTools commands into PartDesign workbench
# ---------------------------------------------------------------------------
# Registered in ZToolsWorkbench.Initialize() after commands are imported,
# so the commands exist before the manipulator references them.
class _ZToolsPartDesignManipulator:
@@ -363,24 +365,11 @@ class _ZToolsPartDesignManipulator:
def modifyMenuBar(self):
return [
{
"insert": "ZTools_DatumCreator",
"menuItem": "PartDesign_Boolean",
"after": "",
},
{
"insert": "ZTools_DatumManager",
"menuItem": "ZTools_DatumCreator",
"after": "",
},
{
"insert": "ZTools_EnhancedPocket",
"menuItem": "ZTools_DatumManager",
"after": "",
},
{
"insert": "ZTools_RotatedLinearPattern",
"menuItem": "ZTools_EnhancedPocket",
"after": "",
},
{"append": "ZTools_DatumCreator", "menuItem": "PartDesign_Body"},
{"append": "ZTools_DatumManager", "menuItem": "PartDesign_Body"},
{"append": "ZTools_EnhancedPocket", "menuItem": "PartDesign_Body"},
{"append": "ZTools_RotatedLinearPattern", "menuItem": "PartDesign_Body"},
]
Gui.addWorkbenchManipulator(_ZToolsPartDesignManipulator())

View File

@@ -2,6 +2,7 @@
# Custom ViewProvider for ZTools datum objects
import json
import math
import FreeCAD as App
import FreeCADGui as Gui
@@ -141,6 +142,23 @@ class ZToolsDatumViewProvider:
return None
def _resolve_source_refs(datum_obj):
"""Parse ZTools_SourceRefs and resolve to (object, subname, shape) tuples."""
refs_json = getattr(datum_obj, "ZTools_SourceRefs", "[]")
try:
refs = json.loads(refs_json)
except json.JSONDecodeError:
return []
doc = datum_obj.Document
resolved = []
for ref in refs:
obj = doc.getObject(ref.get("object", ""))
sub = ref.get("subname", "")
shape = obj.getSubObject(sub) if obj and sub else None
resolved.append((obj, sub, shape))
return resolved
class DatumEditTaskPanel:
"""
Task panel for editing existing ZTools datum objects.
@@ -364,8 +382,45 @@ class DatumEditTaskPanel:
self.datum_obj.AttachmentOffset = new_offset
self._update_params({"distance": distance})
elif ztools_type in ("angled", "tangent_cylinder"):
self._update_params({"angle": self.angle_spin.value()})
elif ztools_type == "angled":
angle = self.angle_spin.value()
if self._has_attachment():
refs = _resolve_source_refs(self.datum_obj)
if len(refs) >= 2 and refs[0][2] and refs[1][2]:
face_normal = refs[0][2].normalAt(0, 0)
edge_shape = refs[1][2]
edge_dir = (
edge_shape.Vertexes[-1].Point - edge_shape.Vertexes[0].Point
).normalize()
face_rot = App.Rotation(App.Vector(0, 0, 1), face_normal)
local_edge_dir = face_rot.inverted().multVec(edge_dir)
angle_rot = App.Rotation(local_edge_dir, angle)
self.datum_obj.AttachmentOffset = App.Placement(
App.Vector(0, 0, 0), angle_rot
)
self._update_params({"angle": angle})
elif ztools_type == "tangent_cylinder":
angle = self.angle_spin.value()
refs = _resolve_source_refs(self.datum_obj)
if refs and refs[0][2]:
face = refs[0][2]
if isinstance(face.Surface, Part.Cylinder):
cyl = face.Surface
axis = cyl.Axis
center = cyl.Center
radius = cyl.Radius
rad = math.radians(angle)
if abs(axis.dot(App.Vector(1, 0, 0))) < 0.99:
local_x = axis.cross(App.Vector(1, 0, 0)).normalize()
else:
local_x = axis.cross(App.Vector(0, 1, 0)).normalize()
local_y = axis.cross(local_x)
radial = local_x * math.cos(rad) + local_y * math.sin(rad)
tangent_point = center + radial * radius
rot = App.Rotation(App.Vector(0, 0, 1), radial)
self.datum_obj.Placement = App.Placement(tangent_point, rot)
self._update_params({"angle": angle})
elif ztools_type in ("normal_to_edge", "on_edge"):
parameter = self.param_spin.value()