plane_tangent_to_cylinder() now derives a vertex from the cylinder
face's edges and uses TangentPlane MapMode with AttachmentOffset to
encode the angular position. This makes tangent datums parametric —
they auto-update when cylinder geometry changes.
Add _find_cylinder_vertex() and _vertex_angle_on_cylinder() helpers.
Store vertex_angle in ZTools_Params for the edit panel to compute
AttachmentOffset updates. Falls back to manual placement when no
vertex can be resolved (non-Body datums).
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.
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.
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.
The _ZToolsPartDesignManipulator was registered at module load time via
Gui.addWorkbenchManipulator(), but the commands it references
(ZTools_DatumCreator, ZTools_DatumManager, ZTools_EnhancedPocket,
ZTools_RotatedLinearPattern) are not registered until Initialize()
imports the command modules.
Move the addWorkbenchManipulator() call into Initialize() with a guard
to prevent duplicate registration. This eliminates the 'Unknown command'
warnings on startup.
Replace simple open/closed arrow indicators with full spanning tree
branch lines (vline, T-junction, L-junction) using the _dark SVG
variants. This renders the model tree with ASCII pipe-style connectors
instead of bare disclosure arrows.
Use fixed-width columns (28px) for the type icon and remove button
instead of ResizeToContents, which was sizing them to the header text
width rather than the content. The 'Type' header label is removed
since the narrow icon column is self-explanatory. The remove button
uses setFixedSize(24,24) to sit cleanly within its fixed column.
QFormLayout.removeRow() destroys the widgets it owns, transferring
ownership to Qt which immediately deletes the underlying C++ objects.
The update_params_ui() method was calling removeRow(0) in a loop to
clear the parameter fields before re-populating them for the new mode.
This destroyed the long-lived QDoubleSpinBox instances (offset_spin,
angle_spin, param_spin, x/y/z_spin) stored as instance attributes.
On the next call to update_params_ui() — triggered by selection
changes, row removal, or mode override — the method attempted to
addRow() with these same Python references, but the C++ objects behind
them had already been freed by Qt. This produced:
RuntimeError: Internal C++ object (PySide6.QtWidgets.QDoubleSpinBox)
already deleted.
The fix replaces the removeRow() loop with a new _clear_params_layout()
method that uses QLayout.takeAt() to detach items from the layout
without destroying them. Each widget is hidden and reparented to None
(releasing Qt's ownership) so it survives the layout clearing and can
be safely re-added with addRow() and show() on the next mode switch.
ZTools datum planes now use FreeCAD's built-in attachment engine instead
of setting MapMode='Deactivated' with manual placement. This means
datums automatically update when source geometry changes on recompute.
Each datum type maps to a vanilla MapMode:
offset_from_face -> FlatFace + AttachmentOffset.Base.z = distance
offset_from_plane -> FlatFace + AttachmentOffset.Base.z = distance
midplane -> FlatFace on face1 + offset = half gap distance
3_points -> ThreePointsPlane (3 vertex refs)
normal_to_edge -> NormalToEdge + MapPathParameter for position
angled -> FlatFace + AttachmentOffset.Rotation for angle
tangent_cylinder -> manual fallback (TangentPlane needs vertex ref)
The C++ AttachExtension handles: automatic recompute via
extensionExecute(), dependency tracking via PropertyLinkSubList,
topology change handling, and live preview via extensionOnChanged().
Non-Body datums (Part::Plane) lack AttachExtension and continue to
use manual placement as before.
Other changes:
- New _configure_attachment() helper sets MapMode, AttachmentSupport,
AttachmentOffset, and MapPathParameter on datum objects
- _setup_ztools_datum() accepts optional attachment parameters and
falls back to manual placement when not provided
- DatumEditTaskPanel.on_param_changed() writes to AttachmentOffset
and MapPathParameter for live editing instead of TODO stubs
- AttachmentSupport added to hidden properties list
- Spreadsheet links target AttachmentOffset.Base.z instead of
Placement.Base.z for attached datums
- ZTools metadata (ZTools_Type, ZTools_Params, ZTools_SourceRefs)
preserved for edit UI and styling
Three fixes:
1. Remove int() wrapper from getStandardButtons() return values.
In Qt6/PySide6, the | operator on QDialogButtonBox.StandardButton
returns a StandardButton enum that is not convertible to int via
int(). FreeCAD's task panel system accepts the enum directly.
Affected files: datum_commands.py, datum_viewprovider.py,
assembly_pattern_commands.py
2. Guard _setup_ztools_viewprovider() against C++ ViewProviders.
PartDesign datum objects (Plane, Line, Point) use pure C++
ViewProviders (e.g. ViewProviderDatumPlane) that don't expose a
Proxy attribute. The previous code crashed with:
'Gui.ViewProviderGeometryObject' object has no attribute 'Proxy'
Now checks hasattr(vo, 'Proxy') before attempting assignment and
wraps the assignment in try/except as a secondary guard.
3. Use persistent setPropertyStatus('Hidden') instead of transient
setEditorMode(2) for hiding attachment properties (MapMode,
Support, etc.). setEditorMode is session-only and resets on
document reload, leaving 'MapMode: Deactivated' visible in the
property panel. setPropertyStatus persists in the document file.
- Use Gui.activateWorkbench() instead of calling Initialize() directly
on dependent workbenches. Direct calls skip the C++ __Workbench__
injection step, causing 'AssemblyWorkbench has no attribute
__Workbench__' errors.
- Fix duplicated generator expression in spreadsheet_commands.py
- Fix missing else clause in spreadsheet_commands.py