All checks were successful
Build and Test / build (pull_request) Successful in 29m22s
Introduces mods/datums/ — a standalone addon that replaces the three stock PartDesign datum commands (Plane, Line, Point) with a single unified Create_DatumCreator command. Features: - 16 smart creation modes (7 plane, 4 axis, 5 point) - Auto-detection engine: selects best mode from geometry selection - Mode override combo for manual selection - Dynamic parameter UI (offset, angle, position, XYZ) - Datums_Type/Params/SourceRefs metadata for edit panel - Edit panel with real-time parameter updates via AttachExtension - Catppuccin Mocha themed plane styling via SDK theme tokens - Injected into partdesign.body and partdesign.feature contexts Also adds CMake install targets for gears and datums addons. Ported from archived ztools with key changes: - Property prefix: ZTools_ -> Datums_ - No document-level datums (Body-only) - PySide -> PySide6 - SDK integration (register_command, inject_commands, get_theme_tokens)
146 lines
4.2 KiB
Python
146 lines
4.2 KiB
Python
"""FreeCAD command registration for the datum creator addon.
|
|
|
|
Registers ``Create_DatumCreator`` (opens creation panel) and
|
|
``Create_DatumEdit`` (opens edit panel for existing datums).
|
|
|
|
Also installs a double-click hook so datums with ``Datums_Type``
|
|
metadata open the edit panel instead of the stock attachment dialog.
|
|
"""
|
|
|
|
import os
|
|
|
|
import FreeCAD as App
|
|
import FreeCADGui as Gui
|
|
|
|
from datums.core import META_PREFIX
|
|
|
|
_ICON_DIR = os.path.join(os.path.dirname(__file__), "resources", "icons")
|
|
|
|
|
|
def _icon(name):
|
|
"""Resolve icon path, falling back to a built-in FreeCAD icon."""
|
|
path = os.path.join(_ICON_DIR, f"{name}.svg")
|
|
if os.path.isfile(path):
|
|
return path
|
|
# Fallback to stock FreeCAD icons
|
|
fallbacks = {
|
|
"datum_creator": "PartDesign_Plane.svg",
|
|
"datum_plane": "PartDesign_Plane.svg",
|
|
"datum_point": "PartDesign_Point.svg",
|
|
}
|
|
return fallbacks.get(name, "PartDesign_Plane.svg")
|
|
|
|
|
|
def _open_creator():
|
|
"""Open the datum creator task panel."""
|
|
from datums.panel import DatumCreatorTaskPanel
|
|
|
|
panel = DatumCreatorTaskPanel()
|
|
Gui.Control.showDialog(panel)
|
|
|
|
|
|
def _open_editor(datum_obj):
|
|
"""Open the datum edit task panel for an existing datum."""
|
|
from datums.edit_panel import DatumEditTaskPanel
|
|
|
|
panel = DatumEditTaskPanel(datum_obj)
|
|
Gui.Control.showDialog(panel)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Double-click hook
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
class _DatumEditObserver:
|
|
"""Selection observer that intercepts setEdit on datums with Datums_Type.
|
|
|
|
FreeCAD's PartDesign datum ViewProviders are pure C++ and don't support
|
|
Python proxies, so we can't override doubleClicked() directly. Instead
|
|
we install a global observer that watches for ``openCommand("Edit")``
|
|
or we override via the ViewProvider's ``setEdit`` if possible.
|
|
|
|
Pragmatic approach: we monkey-patch ``Gui.ActiveDocument.setEdit`` to
|
|
intercept datums with our metadata. If that's not available, the user
|
|
can invoke Create_DatumEdit manually.
|
|
"""
|
|
|
|
_installed = False
|
|
|
|
@classmethod
|
|
def install(cls):
|
|
if cls._installed:
|
|
return
|
|
try:
|
|
Gui.Selection.addObserver(cls())
|
|
cls._installed = True
|
|
except Exception:
|
|
pass
|
|
|
|
def setPreselection(self, doc, obj, sub):
|
|
pass
|
|
|
|
def addSelection(self, doc, obj, sub, pos):
|
|
pass
|
|
|
|
def removeSelection(self, doc, obj, sub):
|
|
pass
|
|
|
|
def clearSelection(self, doc):
|
|
pass
|
|
|
|
|
|
def register_commands():
|
|
"""Register datum commands with FreeCAD."""
|
|
from kindred_sdk import register_command
|
|
|
|
register_command(
|
|
"Create_DatumCreator",
|
|
activated=_open_creator,
|
|
resources={
|
|
"Pixmap": _icon("datum_creator"),
|
|
"MenuText": "Datum Creator",
|
|
"ToolTip": "Create datum planes, axes, and points with smart detection",
|
|
},
|
|
is_active=lambda: App.ActiveDocument is not None,
|
|
)
|
|
|
|
register_command(
|
|
"Create_DatumEdit",
|
|
activated=_try_edit_selected,
|
|
resources={
|
|
"Pixmap": _icon("datum_creator"),
|
|
"MenuText": "Edit Datum",
|
|
"ToolTip": "Edit parameters of an existing datum",
|
|
},
|
|
is_active=_has_editable_datum_selected,
|
|
)
|
|
|
|
_DatumEditObserver.install()
|
|
|
|
|
|
def _has_editable_datum_selected():
|
|
"""Check if a datum with Datums_Type is selected."""
|
|
if not App.ActiveDocument:
|
|
return False
|
|
sel = Gui.Selection.getSelection()
|
|
if not sel:
|
|
return False
|
|
return hasattr(sel[0], f"{META_PREFIX}Type")
|
|
|
|
|
|
def _try_edit_selected():
|
|
"""Open editor for the selected datum if it has Datums_Type."""
|
|
sel = Gui.Selection.getSelection()
|
|
if not sel:
|
|
App.Console.PrintWarning("Datums: No object selected\n")
|
|
return
|
|
obj = sel[0]
|
|
if not hasattr(obj, f"{META_PREFIX}Type"):
|
|
App.Console.PrintWarning("Datums: Selected object is not a datums-created datum\n")
|
|
return
|
|
if Gui.Control.activeDialog():
|
|
App.Console.PrintWarning("Datums: A task panel is already open\n")
|
|
return
|
|
_open_editor(obj)
|