- Grouped toolbar dropdown (ZTools_AppearanceMode) replaces two separate buttons
- Set Category command (ZTools_SetCategory) with popup menu for batch tagging
- Category changes wrapped in undo transactions
- Status bar indicator showing current mode, clickable to toggle
- Debounced recompute handling (Shape property changes batched at 100ms)
- Observer re-applies colors after recompute to prevent reset
Closes#21
Add configurable appearance mode system with two modes:
- Realistic: material passthrough (restores original ShapeColor)
- Engineering: colors parts by KindredCategory using Catppuccin Mocha palette
KindredCategory enum property (custom_body, fastener, structural, electrical,
seal_gasket, bearing_bushing, spring_compliant, moving_part) is added lazily
to Part::Feature objects when Engineering mode is activated.
Includes:
- AppearanceMode base class with apply/reset/apply_to_object interface
- AppearanceManager singleton with document observer for auto-coloring
- Palette loaded from JSON config (theme-overridable)
- View > Appearance Mode menu and toolbar integration
- Preference persistence at Kindred/AppearanceMode
Closes#20
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