diff --git a/.gitmodules b/.gitmodules
index b78d9a2daf..2c1f01bde1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,3 +22,7 @@
path = mods/gears
url = https://git.kindred-systems.com/kindred/gears.git
branch = main
+[submodule "mods/datums"]
+ path = mods/datums
+ url = https://git.kindred-systems.com/kindred/datums.git
+ branch = main
diff --git a/mods/datums b/mods/datums
new file mode 160000
index 0000000000..e075dc9256
--- /dev/null
+++ b/mods/datums
@@ -0,0 +1 @@
+Subproject commit e075dc925641d8549d80c38754e894b032d32775
diff --git a/mods/datums/Init.py b/mods/datums/Init.py
deleted file mode 100644
index 247f54dc1b..0000000000
--- a/mods/datums/Init.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Datums addon — console initialization (no-op)."""
diff --git a/mods/datums/InitGui.py b/mods/datums/InitGui.py
deleted file mode 100644
index 08693fcfef..0000000000
--- a/mods/datums/InitGui.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Datums addon — GUI initialization.
-
-Registers the unified datum creator command and injects it into
-PartDesign editing contexts via the Kindred SDK.
-"""
-
-
-def _register_datum_commands():
- """Register datum creator command and inject into PartDesign contexts."""
- try:
- from datums.command import register_commands
-
- register_commands()
- except Exception as e:
- import FreeCAD
-
- FreeCAD.Console.PrintWarning(f"kindred-datums: command registration failed: {e}\n")
-
- try:
- from kindred_sdk import inject_commands
-
- inject_commands(
- "partdesign.body",
- "Part Design Helper Features",
- ["Create_DatumCreator"],
- )
- inject_commands(
- "partdesign.feature",
- "Part Design Helper Features",
- ["Create_DatumCreator"],
- )
- except Exception as e:
- import FreeCAD
-
- FreeCAD.Console.PrintWarning(f"kindred-datums: context injection failed: {e}\n")
-
-
-from PySide6.QtCore import QTimer
-
-QTimer.singleShot(500, _register_datum_commands)
diff --git a/mods/datums/datums/__init__.py b/mods/datums/datums/__init__.py
deleted file mode 100644
index 54b1f6a61d..0000000000
--- a/mods/datums/datums/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Unified datum creator for Kindred Create."""
diff --git a/mods/datums/datums/command.py b/mods/datums/datums/command.py
deleted file mode 100644
index 8b7adf8757..0000000000
--- a/mods/datums/datums/command.py
+++ /dev/null
@@ -1,145 +0,0 @@
-"""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)
diff --git a/mods/datums/datums/core.py b/mods/datums/datums/core.py
deleted file mode 100644
index 4156e03c71..0000000000
--- a/mods/datums/datums/core.py
+++ /dev/null
@@ -1,1061 +0,0 @@
-"""Core datum creation functions with attachment system.
-
-Each function creates the appropriate PartDesign::Plane, PartDesign::Line,
-or PartDesign::Point in the active Body, then configures Part::AttachExtension
-properties for parametric updates on recompute.
-
-Metadata is stored as ``Datums_Type``, ``Datums_Params``, ``Datums_SourceRefs``
-properties on each datum object for the edit panel.
-"""
-
-import json
-import math
-from typing import Any, Dict, List, Optional, Tuple
-
-import FreeCAD as App
-import Part
-
-# Metadata property prefix
-META_PREFIX = "Datums_"
-
-# Default plane styling (Catppuccin Mocha mauve)
-_DEFAULT_PLANE_COLOR = (0.796, 0.651, 0.969) # #cba6f7
-_DEFAULT_PLANE_TRANSPARENCY = 70
-
-
-def _get_plane_color():
- """Get plane color from SDK theme or use default."""
- try:
- from kindred_sdk import get_theme_tokens
-
- tokens = get_theme_tokens()
- hex_color = tokens.get("mauve", "#cba6f7")
- h = hex_color.lstrip("#")
- return (int(h[0:2], 16) / 255, int(h[2:4], 16) / 255, int(h[4:6], 16) / 255)
- except Exception:
- return _DEFAULT_PLANE_COLOR
-
-
-def _get_next_index(doc, prefix):
- """Get next available index for auto-naming."""
- existing = [obj.Name for obj in doc.Objects if obj.Name.startswith(prefix)]
- if not existing:
- return 1
- indices = []
- for name in existing:
- try:
- idx = int(name.replace(prefix, "").lstrip("_").split("_")[0])
- indices.append(idx)
- except ValueError:
- continue
- return max(indices, default=0) + 1
-
-
-# ---------------------------------------------------------------------------
-# Attachment helpers
-# ---------------------------------------------------------------------------
-
-
-def _configure_attachment(obj, map_mode, support, offset=None, path_param=None):
- """Configure FreeCAD's vanilla AttachExtension on a datum object.
-
- Returns True if attachment was configured, False if unavailable.
- """
- if not hasattr(obj, "MapMode"):
- return False
- obj.AttachmentSupport = support
- obj.MapMode = map_mode
- if offset is not None:
- obj.AttachmentOffset = offset
- if path_param is not None and hasattr(obj, "MapPathParameter"):
- obj.MapPathParameter = path_param
- return True
-
-
-def _style_plane(obj):
- """Apply Catppuccin mauve styling to a datum plane."""
- if hasattr(obj, "ViewObject") and obj.ViewObject is not None:
- vo = obj.ViewObject
- color = _get_plane_color()
- if hasattr(vo, "ShapeColor"):
- vo.ShapeColor = color
- if hasattr(vo, "Transparency"):
- vo.Transparency = _DEFAULT_PLANE_TRANSPARENCY
- if hasattr(vo, "LineColor"):
- vo.LineColor = color
-
-
-def _hide_attachment_properties(obj):
- """Hide vanilla attachment properties from the property editor."""
- for prop in (
- "MapMode",
- "MapPathParameter",
- "MapReversed",
- "AttachmentOffset",
- "AttachmentSupport",
- "Support",
- ):
- try:
- if hasattr(obj, prop):
- obj.setPropertyStatus(prop, "Hidden")
- except Exception:
- pass
-
-
-def _add_metadata(obj, datum_type, params, source_refs=None):
- """Store datum metadata in object properties."""
- for suffix, doc_str in (
- ("Type", "Datum creation method"),
- ("Params", "Creation parameters (JSON)"),
- ("SourceRefs", "Source geometry references (JSON)"),
- ):
- prop_name = f"{META_PREFIX}{suffix}"
- if not hasattr(obj, prop_name):
- obj.addProperty(
- "App::PropertyString",
- prop_name,
- "Datums",
- doc_str,
- )
-
- setattr(obj, f"{META_PREFIX}Type", datum_type)
-
- # Serialize params — convert vectors/placements to dicts
- serializable = {}
- for k, v in params.items():
- if isinstance(v, App.Vector):
- serializable[k] = {"_type": "Vector", "x": v.x, "y": v.y, "z": v.z}
- elif isinstance(v, App.Placement):
- serializable[k] = {
- "_type": "Placement",
- "base": {"x": v.Base.x, "y": v.Base.y, "z": v.Base.z},
- "rotation": list(v.Rotation.Q),
- }
- else:
- serializable[k] = v
- setattr(obj, f"{META_PREFIX}Params", json.dumps(serializable))
-
- # Source refs
- if source_refs:
- ref_data = [{"object": src.Name, "subname": sub or ""} for src, sub in source_refs if src]
- setattr(obj, f"{META_PREFIX}SourceRefs", json.dumps(ref_data))
- else:
- setattr(obj, f"{META_PREFIX}SourceRefs", "[]")
-
-
-def _setup_datum(
- obj,
- placement,
- datum_type,
- params,
- source_refs=None,
- is_plane=False,
- map_mode=None,
- support=None,
- offset=None,
- path_param=None,
-):
- """Set up a datum with attachment or manual placement."""
- if map_mode and support and hasattr(obj, "MapMode"):
- _configure_attachment(obj, map_mode, support, offset, path_param)
- else:
- if hasattr(obj, "MapMode"):
- obj.MapMode = "Deactivated"
- obj.Placement = placement
-
- _add_metadata(obj, datum_type, params, source_refs)
- if is_plane:
- _style_plane(obj)
- _hide_attachment_properties(obj)
-
-
-def _get_subname_from_shape(parent_obj, shape):
- """Find the sub-element name for a shape within a parent object."""
- if not hasattr(parent_obj, "Shape"):
- return None
- parent_shape = parent_obj.Shape
- if isinstance(shape, Part.Face):
- for i, face in enumerate(parent_shape.Faces, 1):
- if face.isSame(shape):
- return f"Face{i}"
- elif isinstance(shape, Part.Edge):
- for i, edge in enumerate(parent_shape.Edges, 1):
- if edge.isSame(shape):
- return f"Edge{i}"
- elif isinstance(shape, Part.Vertex):
- for i, vertex in enumerate(parent_shape.Vertexes, 1):
- if vertex.isSame(shape):
- return f"Vertex{i}"
- return None
-
-
-def _find_shape_owner(doc, shape):
- """Find the object that owns a given shape."""
- for obj in doc.Objects:
- if not hasattr(obj, "Shape"):
- continue
- subname = _get_subname_from_shape(obj, shape)
- if subname:
- return obj, subname
- return None, None
-
-
-def _link_to_spreadsheet(doc, obj, param_name, value, alias):
- """Optionally link a parameter to spreadsheet."""
- sheet = doc.getObject("Spreadsheet")
- if not sheet:
- sheet = doc.addObject("Spreadsheet::Sheet", "Spreadsheet")
- row = 1
- while sheet.getContents(f"A{row}"):
- row += 1
- sheet.set(f"A{row}", alias)
- sheet.set(f"B{row}", f"{value} mm")
- sheet.setAlias(f"B{row}", alias)
- obj.setExpression(param_name, f"Spreadsheet.{alias}")
- return alias
-
-
-def _get_active_body():
- """Get the active PartDesign body, or raise."""
- import FreeCADGui as Gui
-
- if hasattr(Gui, "ActiveDocument") and Gui.ActiveDocument:
- view = Gui.ActiveDocument.ActiveView
- if hasattr(view, "getActiveObject"):
- body = view.getActiveObject("pdbody")
- if body:
- return body
- raise RuntimeError("No active PartDesign body. Select a body first.")
-
-
-# ==========================================================================
-# DATUM PLANES
-# ==========================================================================
-
-
-def plane_offset_from_face(
- face,
- distance,
- *,
- name=None,
- body=None,
- link_spreadsheet=False,
- source_object=None,
- source_subname=None,
-):
- """Create datum plane offset from a planar face."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not face.Surface.isPlanar():
- raise ValueError("Face must be planar for offset plane")
-
- if source_object is None or source_subname is None:
- source_object, source_subname = _find_shape_owner(doc, face)
-
- uv = face.Surface.parameter(face.CenterOfMass)
- normal = face.normalAt(uv[0], uv[1])
- base = face.CenterOfMass + normal * distance
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Offset")
- name = f"DPlane_Offset_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), normal)
- placement = App.Placement(base, rot)
- source_refs = [(source_object, source_subname)] if source_object else None
-
- plane = body.newObject("PartDesign::Plane", name)
- att_support = [(source_object, source_subname)] if source_object else None
- att_offset = App.Placement(App.Vector(0, 0, distance), App.Rotation())
- _setup_datum(
- plane,
- placement,
- "offset_from_face",
- {"distance": distance},
- source_refs,
- is_plane=True,
- map_mode="FlatFace" if att_support else None,
- support=att_support,
- offset=att_offset,
- )
-
- if link_spreadsheet:
- _link_to_spreadsheet(doc, plane, "AttachmentOffset.Base.z", distance, f"{name}_offset")
-
- doc.recompute()
- return plane
-
-
-def plane_offset_from_plane(
- source_plane,
- distance,
- *,
- name=None,
- body=None,
- link_spreadsheet=False,
-):
- """Create datum plane offset from another datum plane."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not hasattr(source_plane, "Shape") or not source_plane.Shape.Faces:
- raise ValueError("Source must be a plane object with a Shape")
-
- face = source_plane.Shape.Faces[0]
- if not face.Surface.isPlanar():
- raise ValueError("Source must be a planar object")
-
- uv = face.Surface.parameter(face.CenterOfMass)
- normal = face.normalAt(uv[0], uv[1])
- base = face.CenterOfMass + normal * distance
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Offset")
- name = f"DPlane_Offset_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), normal)
- placement = App.Placement(base, rot)
- source_refs = [(source_plane, "")]
-
- plane = body.newObject("PartDesign::Plane", name)
- att_offset = App.Placement(App.Vector(0, 0, distance), App.Rotation())
- _setup_datum(
- plane,
- placement,
- "offset_from_plane",
- {"distance": distance, "source_plane": source_plane.Name},
- source_refs,
- is_plane=True,
- map_mode="FlatFace",
- support=[(source_plane, "")],
- offset=att_offset,
- )
-
- if link_spreadsheet:
- _link_to_spreadsheet(doc, plane, "AttachmentOffset.Base.z", distance, f"{name}_offset")
-
- doc.recompute()
- return plane
-
-
-def plane_midplane(
- face1,
- face2,
- *,
- name=None,
- body=None,
- source_object1=None,
- source_subname1=None,
- source_object2=None,
- source_subname2=None,
-):
- """Create datum plane midway between two parallel faces."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not (face1.Surface.isPlanar() and face2.Surface.isPlanar()):
- raise ValueError("Both faces must be planar")
-
- uv1 = face1.Surface.parameter(face1.CenterOfMass)
- uv2 = face2.Surface.parameter(face2.CenterOfMass)
- n1 = face1.normalAt(uv1[0], uv1[1])
- n2 = face2.normalAt(uv2[0], uv2[1])
-
- if abs(n1.dot(n2)) < 0.9999:
- raise ValueError("Faces must be parallel for midplane")
-
- c1, c2 = face1.CenterOfMass, face2.CenterOfMass
- half_dist = (c2 - c1).dot(n1) / 2.0
- mid = (c1 + c2) * 0.5
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Mid")
- name = f"DPlane_Mid_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), n1)
- placement = App.Placement(mid, rot)
-
- source_refs = []
- if source_object1:
- source_refs.append((source_object1, source_subname1))
- if source_object2:
- source_refs.append((source_object2, source_subname2))
-
- plane = body.newObject("PartDesign::Plane", name)
- if source_object1 and source_subname1:
- att_offset = App.Placement(App.Vector(0, 0, half_dist), App.Rotation())
- _setup_datum(
- plane,
- placement,
- "midplane",
- {"half_distance": half_dist},
- source_refs or None,
- is_plane=True,
- map_mode="FlatFace",
- support=[(source_object1, source_subname1)],
- offset=att_offset,
- )
- else:
- _setup_datum(
- plane,
- placement,
- "midplane",
- {"half_distance": half_dist},
- source_refs or None,
- is_plane=True,
- )
-
- doc.recompute()
- return plane
-
-
-def plane_from_3_points(
- p1,
- p2,
- p3,
- *,
- name=None,
- body=None,
- source_refs=None,
-):
- """Create datum plane from 3 non-collinear points."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- normal = (p2 - p1).cross(p3 - p1)
- if normal.Length < 1e-6:
- raise ValueError("Points are collinear, cannot define plane")
- normal.normalize()
- center = (p1 + p2 + p3) / 3
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_3Pt")
- name = f"DPlane_3Pt_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), normal)
- placement = App.Placement(center, rot)
-
- plane = body.newObject("PartDesign::Plane", name)
- att_support = [(ref[0], ref[1]) for ref in source_refs] if source_refs else None
- _setup_datum(
- plane,
- placement,
- "3_points",
- {"p1": p1, "p2": p2, "p3": p3},
- source_refs,
- is_plane=True,
- map_mode="ThreePointsPlane" if att_support and len(att_support) == 3 else None,
- support=att_support,
- )
-
- doc.recompute()
- return plane
-
-
-def plane_normal_to_edge(
- edge,
- *,
- parameter=0.5,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum plane normal to edge at parameter location (0-1)."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- param = edge.FirstParameter + parameter * (edge.LastParameter - edge.FirstParameter)
- point = edge.valueAt(param)
- tangent = edge.tangentAt(param)
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Normal")
- name = f"DPlane_Normal_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), tangent)
- placement = App.Placement(point, rot)
- source_refs = [(source_object, source_subname)] if source_object else None
-
- plane = body.newObject("PartDesign::Plane", name)
- att_support = [(source_object, source_subname)] if source_object else None
- _setup_datum(
- plane,
- placement,
- "normal_to_edge",
- {"parameter": parameter},
- source_refs,
- is_plane=True,
- map_mode="NormalToEdge" if att_support else None,
- support=att_support,
- path_param=parameter,
- )
-
- doc.recompute()
- return plane
-
-
-def plane_angled(
- face,
- edge,
- angle,
- *,
- name=None,
- body=None,
- link_spreadsheet=False,
- source_face_obj=None,
- source_face_sub=None,
- source_edge_obj=None,
- source_edge_sub=None,
-):
- """Create datum plane at angle to face, rotating about edge."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not face.Surface.isPlanar():
- raise ValueError("Face must be planar")
-
- uv = face.Surface.parameter(face.CenterOfMass)
- face_normal = face.normalAt(uv[0], uv[1])
-
- edge_dir = (
- edge.Curve.Direction
- if hasattr(edge.Curve, "Direction")
- else (edge.valueAt(edge.LastParameter) - edge.valueAt(edge.FirstParameter)).normalize()
- )
- edge_mid = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2)
-
- rot_axis = App.Rotation(edge_dir, angle)
- new_normal = rot_axis.multVec(face_normal)
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Angled")
- name = f"DPlane_Angled_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), new_normal)
- placement = App.Placement(edge_mid, rot)
-
- source_refs = []
- if source_face_obj:
- source_refs.append((source_face_obj, source_face_sub))
- if source_edge_obj:
- source_refs.append((source_edge_obj, source_edge_sub))
-
- plane = body.newObject("PartDesign::Plane", name)
- if source_face_obj and source_face_sub:
- 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)
- att_offset = App.Placement(App.Vector(0, 0, 0), angle_rot)
- _setup_datum(
- plane,
- placement,
- "angled",
- {"angle": angle},
- source_refs or None,
- is_plane=True,
- map_mode="FlatFace",
- support=[(source_face_obj, source_face_sub)],
- offset=att_offset,
- )
- else:
- _setup_datum(
- plane,
- placement,
- "angled",
- {"angle": angle},
- source_refs or None,
- is_plane=True,
- )
-
- if link_spreadsheet:
- _link_to_spreadsheet(doc, plane, "AttachmentOffset.Angle", angle, f"{name}_angle")
-
- doc.recompute()
- return plane
-
-
-def _find_cylinder_vertex(obj, face_subname):
- """Find a vertex subname from a cylindrical face's edges."""
- face = obj.getSubObject(face_subname)
- if not face or not face.Edges:
- return None
- edge = face.Edges[0]
- if not edge.Vertexes:
- return None
- vertex_point = edge.Vertexes[0].Point
- for i, v in enumerate(obj.Shape.Vertexes, 1):
- if v.Point.isEqual(vertex_point, 1e-6):
- return f"Vertex{i}"
- return None
-
-
-def _vertex_angle_on_cylinder(obj, vertex_sub, cylinder):
- """Compute the angular position of a vertex on a cylinder surface."""
- vertex = obj.getSubObject(vertex_sub)
- if not vertex:
- return 0.0
- relative = vertex.Point - cylinder.Center
- axis = cylinder.Axis
- radial = relative - axis * relative.dot(axis)
- if radial.Length < 1e-10:
- return 0.0
- radial.normalize()
- 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)
- return math.degrees(math.atan2(radial.dot(local_y), radial.dot(local_x)))
-
-
-def plane_tangent_to_cylinder(
- face,
- *,
- angle=0,
- name=None,
- body=None,
- link_spreadsheet=False,
- source_object=None,
- source_subname=None,
-):
- """Create datum plane tangent to cylindrical face at angle."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not isinstance(face.Surface, Part.Cylinder):
- raise ValueError("Face must be cylindrical")
-
- 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
-
- if name is None:
- idx = _get_next_index(doc, "DPlane_Tangent")
- name = f"DPlane_Tangent_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), radial)
- placement = App.Placement(tangent_point, rot)
- source_refs = [(source_object, source_subname)] if source_object else None
-
- plane = body.newObject("PartDesign::Plane", name)
- vertex_sub = (
- _find_cylinder_vertex(source_object, source_subname)
- if source_object and source_subname
- else None
- )
- if vertex_sub:
- vertex_angle = _vertex_angle_on_cylinder(source_object, vertex_sub, cyl)
- offset_angle = angle - vertex_angle
- offset_rot = App.Rotation(App.Vector(0, 0, 1), offset_angle)
- att_offset = App.Placement(App.Vector(0, 0, 0), offset_rot)
- _setup_datum(
- plane,
- placement,
- "tangent_cylinder",
- {"angle": angle, "radius": radius, "vertex_angle": vertex_angle},
- source_refs,
- is_plane=True,
- map_mode="TangentPlane",
- support=[(source_object, source_subname), (source_object, vertex_sub)],
- offset=att_offset,
- )
- else:
- _setup_datum(
- plane,
- placement,
- "tangent_cylinder",
- {"angle": angle, "radius": radius},
- source_refs,
- is_plane=True,
- )
-
- doc.recompute()
- return plane
-
-
-# ==========================================================================
-# DATUM AXES
-# ==========================================================================
-
-
-def axis_from_2_points(
- p1,
- p2,
- *,
- name=None,
- body=None,
- source_refs=None,
-):
- """Create datum axis from two points."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- direction = p2 - p1
- if direction.Length < 1e-6:
- raise ValueError("Points must be distinct")
- length = direction.Length
- direction.normalize()
- midpoint = (p1 + p2) * 0.5
-
- if name is None:
- idx = _get_next_index(doc, "DAxis_2Pt")
- name = f"DAxis_2Pt_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), direction)
- placement = App.Placement(midpoint, rot)
-
- axis = body.newObject("PartDesign::Line", name)
- _setup_datum(
- axis,
- placement,
- "2_points",
- {"p1": p1, "p2": p2, "direction": direction, "length": length},
- source_refs,
- )
-
- doc.recompute()
- return axis
-
-
-def axis_from_edge(
- edge,
- *,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum axis from linear edge."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not isinstance(edge.Curve, Part.Line):
- raise ValueError("Edge must be linear")
-
- if name is None:
- idx = _get_next_index(doc, "DAxis_Edge")
- name = f"DAxis_Edge_{idx:03d}"
-
- p1 = edge.valueAt(edge.FirstParameter)
- p2 = edge.valueAt(edge.LastParameter)
- direction = (p2 - p1).normalize()
- midpoint = (p1 + p2) * 0.5
-
- rot = App.Rotation(App.Vector(0, 0, 1), direction)
- placement = App.Placement(midpoint, rot)
- source_refs = [(source_object, source_subname)] if source_object else None
-
- axis = body.newObject("PartDesign::Line", name)
- _setup_datum(
- axis,
- placement,
- "from_edge",
- {"p1": p1, "p2": p2, "direction": direction},
- source_refs,
- )
-
- doc.recompute()
- return axis
-
-
-def axis_cylinder_center(
- face,
- *,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum axis at center of cylindrical face."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not isinstance(face.Surface, Part.Cylinder):
- raise ValueError("Face must be cylindrical")
-
- cyl = face.Surface
- center = cyl.Center
- axis_dir = cyl.Axis
-
- if name is None:
- idx = _get_next_index(doc, "DAxis_Cyl")
- name = f"DAxis_Cyl_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), axis_dir)
- placement = App.Placement(center, rot)
- source_refs = [(source_object, source_subname)] if source_object else None
-
- axis = body.newObject("PartDesign::Line", name)
- _setup_datum(
- axis,
- placement,
- "cylinder_center",
- {"center": center, "direction": axis_dir, "radius": cyl.Radius},
- source_refs,
- )
-
- doc.recompute()
- return axis
-
-
-def axis_intersection_planes(
- plane1,
- plane2,
- *,
- name=None,
- body=None,
- source_object1=None,
- source_subname1=None,
- source_object2=None,
- source_subname2=None,
-):
- """Create datum axis at intersection of two planes."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- shape1 = plane1.Shape if hasattr(plane1, "Shape") else plane1
- shape2 = plane2.Shape if hasattr(plane2, "Shape") else plane2
-
- common = shape1.common(shape2)
- if not common.Edges:
- raise ValueError("Planes do not intersect or are parallel")
-
- edge = common.Edges[0]
- p1 = edge.valueAt(edge.FirstParameter)
- p2 = edge.valueAt(edge.LastParameter)
- direction = (p2 - p1).normalize()
- midpoint = (p1 + p2) * 0.5
-
- if name is None:
- idx = _get_next_index(doc, "DAxis_Intersect")
- name = f"DAxis_Intersect_{idx:03d}"
-
- rot = App.Rotation(App.Vector(0, 0, 1), direction)
- placement = App.Placement(midpoint, rot)
-
- source_refs = []
- if source_object1:
- source_refs.append((source_object1, source_subname1))
- if source_object2:
- source_refs.append((source_object2, source_subname2))
-
- axis = body.newObject("PartDesign::Line", name)
- _setup_datum(
- axis,
- placement,
- "plane_intersection",
- {"point1": p1, "point2": p2, "direction": direction},
- source_refs or None,
- )
-
- doc.recompute()
- return axis
-
-
-# ==========================================================================
-# DATUM POINTS
-# ==========================================================================
-
-
-def point_at_vertex(
- vertex,
- *,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum point at vertex location."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- pos = vertex.Point
-
- if name is None:
- idx = _get_next_index(doc, "DPoint_Vtx")
- name = f"DPoint_Vtx_{idx:03d}"
-
- placement = App.Placement(pos, App.Rotation())
- source_refs = [(source_object, source_subname)] if source_object else None
-
- point = body.newObject("PartDesign::Point", name)
- _setup_datum(
- point,
- placement,
- "vertex",
- {"position": pos},
- source_refs,
- )
-
- doc.recompute()
- return point
-
-
-def point_at_coordinates(
- x,
- y,
- z,
- *,
- name=None,
- body=None,
- link_spreadsheet=False,
-):
- """Create datum point at XYZ coordinates."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if name is None:
- idx = _get_next_index(doc, "DPoint_XYZ")
- name = f"DPoint_XYZ_{idx:03d}"
-
- pos = App.Vector(x, y, z)
- placement = App.Placement(pos, App.Rotation())
-
- point = body.newObject("PartDesign::Point", name)
- _setup_datum(
- point,
- placement,
- "coordinates",
- {"x": x, "y": y, "z": z},
- None,
- )
-
- doc.recompute()
- return point
-
-
-def point_on_edge(
- edge,
- *,
- parameter=0.5,
- name=None,
- body=None,
- link_spreadsheet=False,
- source_object=None,
- source_subname=None,
-):
- """Create datum point on edge at parameter (0-1)."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- param = edge.FirstParameter + parameter * (edge.LastParameter - edge.FirstParameter)
- pos = edge.valueAt(param)
-
- if name is None:
- idx = _get_next_index(doc, "DPoint_Edge")
- name = f"DPoint_Edge_{idx:03d}"
-
- placement = App.Placement(pos, App.Rotation())
- source_refs = [(source_object, source_subname)] if source_object else None
-
- point = body.newObject("PartDesign::Point", name)
- _setup_datum(
- point,
- placement,
- "on_edge",
- {"parameter": parameter, "position": pos},
- source_refs,
- )
-
- doc.recompute()
- return point
-
-
-def point_center_of_face(
- face,
- *,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum point at center of mass of face."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- pos = face.CenterOfMass
-
- if name is None:
- idx = _get_next_index(doc, "DPoint_FaceCenter")
- name = f"DPoint_FaceCenter_{idx:03d}"
-
- placement = App.Placement(pos, App.Rotation())
- source_refs = [(source_object, source_subname)] if source_object else None
-
- point = body.newObject("PartDesign::Point", name)
- _setup_datum(
- point,
- placement,
- "face_center",
- {"position": pos},
- source_refs,
- )
-
- doc.recompute()
- return point
-
-
-def point_center_of_circle(
- edge,
- *,
- name=None,
- body=None,
- source_object=None,
- source_subname=None,
-):
- """Create datum point at center of circular edge."""
- doc = App.ActiveDocument
- if body is None:
- body = _get_active_body()
-
- if not isinstance(edge.Curve, (Part.Circle, Part.ArcOfCircle)):
- raise ValueError("Edge must be circular")
-
- pos = edge.Curve.Center
-
- if name is None:
- idx = _get_next_index(doc, "DPoint_CircleCenter")
- name = f"DPoint_CircleCenter_{idx:03d}"
-
- placement = App.Placement(pos, App.Rotation())
- source_refs = [(source_object, source_subname)] if source_object else None
-
- point = body.newObject("PartDesign::Point", name)
- _setup_datum(
- point,
- placement,
- "circle_center",
- {"position": pos, "radius": edge.Curve.Radius},
- source_refs,
- )
-
- doc.recompute()
- return point
diff --git a/mods/datums/datums/detection.py b/mods/datums/datums/detection.py
deleted file mode 100644
index 3b96e648ae..0000000000
--- a/mods/datums/datums/detection.py
+++ /dev/null
@@ -1,169 +0,0 @@
-"""Selection auto-detection system for the unified datum creator.
-
-Provides geometry type classification and mode matching to automatically
-determine the best datum creation mode from user selection.
-"""
-
-import Part
-
-
-class SelectionItem:
- """Wraps a selected geometry element with auto-detected type."""
-
- def __init__(self, obj, subname, shape=None):
- self.obj = obj
- self.subname = subname
- self.shape = shape
- self.geo_type = self._determine_type()
-
- def _determine_type(self):
- """Determine the geometry type of this selection."""
- if self.shape is None and hasattr(self.obj, "Shape"):
- for prefix in ("Face", "Edge", "Vertex"):
- if self.subname and self.subname.startswith(prefix):
- try:
- self.shape = self.obj.Shape.getElement(self.subname)
- except Exception:
- pass
- break
-
- if self.shape is None:
- type_id = getattr(self.obj, "TypeId", "")
- if "Plane" in type_id or (
- hasattr(self.obj, "Shape")
- and self.obj.Shape.Faces
- and self.obj.Shape.Faces[0].Surface.isPlanar()
- ):
- return "plane"
- return "unknown"
-
- if isinstance(self.shape, Part.Face):
- if isinstance(self.shape.Surface, Part.Cylinder):
- return "cylinder"
- if self.shape.Surface.isPlanar():
- return "face"
- return "face"
- elif isinstance(self.shape, Part.Edge):
- if isinstance(self.shape.Curve, (Part.Circle, Part.ArcOfCircle)):
- return "circle"
- return "edge"
- elif isinstance(self.shape, Part.Vertex):
- return "vertex"
-
- return "unknown"
-
- @property
- def display_name(self):
- if self.subname:
- return f"{self.obj.Label}.{self.subname}"
- return self.obj.Label
-
- @property
- def type_icon(self):
- icons = {
- "face": "\u25a2",
- "plane": "\u25a3",
- "cylinder": "\u25ce",
- "edge": "\u2015",
- "circle": "\u25cb",
- "vertex": "\u2022",
- "unknown": "?",
- }
- return icons.get(self.geo_type, "?")
-
-
-# Mode definitions: (display_name, mode_id, required_types, datum_category)
-MODES = (
- # Planes
- ("Offset from Face", "offset_face", ("face",), "plane"),
- ("Offset from Plane", "offset_plane", ("plane",), "plane"),
- ("Midplane (2 Faces)", "midplane", ("face", "face"), "plane"),
- ("3 Points", "3_points", ("vertex", "vertex", "vertex"), "plane"),
- ("Normal to Edge", "normal_edge", ("edge",), "plane"),
- ("Angled from Face", "angled", ("face", "edge"), "plane"),
- ("Tangent to Cylinder", "tangent_cyl", ("cylinder",), "plane"),
- # Axes
- ("Axis from 2 Points", "axis_2pt", ("vertex", "vertex"), "axis"),
- ("Axis from Edge", "axis_edge", ("edge",), "axis"),
- ("Axis at Cylinder Center", "axis_cyl", ("cylinder",), "axis"),
- ("Axis at Plane Intersection", "axis_intersect", ("plane", "plane"), "axis"),
- # Points
- ("Point at Vertex", "point_vertex", ("vertex",), "point"),
- ("Point at XYZ", "point_xyz", (), "point"),
- ("Point on Edge", "point_edge", ("edge",), "point"),
- ("Point at Face Center", "point_face", ("face",), "point"),
- ("Point at Circle Center", "point_circle", ("circle",), "point"),
-)
-
-# Category colors (Catppuccin Mocha)
-CATEGORY_COLORS = {
- "plane": "#cba6f7", # mauve
- "axis": "#94e2d5", # teal
- "point": "#f9e2af", # yellow
-}
-
-
-def _type_matches(sel_type, req_type):
- """Check if a selected type satisfies a required type."""
- if sel_type == req_type:
- return True
- if req_type == "face" and sel_type in ("face", "cylinder"):
- return True
- if req_type == "edge" and sel_type in ("edge", "circle"):
- return True
- return False
-
-
-def _match_score(sel_types, required_types):
- """Score how well selection matches required types. 0 = no match."""
- if len(sel_types) < len(required_types):
- return 0
-
- remaining = list(sel_types)
- matched = 0
- for req in required_types:
- found = False
- for i, sel in enumerate(remaining):
- if _type_matches(sel, req):
- remaining.pop(i)
- matched += 1
- found = True
- break
- if not found:
- return 0
-
- # Exact count match scores higher
- if len(sel_types) == len(required_types):
- return 100 + matched
- return matched
-
-
-def match_mode(selection_items):
- """Auto-detect the best creation mode from selection items.
-
- Parameters
- ----------
- selection_items : list[SelectionItem]
- Current selection items.
-
- Returns
- -------
- tuple or None
- ``(display_name, mode_id, category)`` for best match, or None.
- """
- sel_types = tuple(item.geo_type for item in selection_items)
- if not sel_types:
- return None
-
- best_match = None
- best_score = -1
-
- for display_name, mode_id, required_types, category in MODES:
- if not required_types:
- continue
- score = _match_score(sel_types, required_types)
- if score > best_score:
- best_score = score
- best_match = (display_name, mode_id, category)
-
- return best_match if best_score > 0 else None
diff --git a/mods/datums/datums/edit_panel.py b/mods/datums/datums/edit_panel.py
deleted file mode 100644
index 4936f72498..0000000000
--- a/mods/datums/datums/edit_panel.py
+++ /dev/null
@@ -1,334 +0,0 @@
-"""Datum edit task panel.
-
-Opened when double-clicking a datum that has ``Datums_Type`` metadata.
-Allows real-time parameter editing with live preview.
-"""
-
-import json
-import math
-
-import FreeCAD as App
-import Part
-from PySide6 import QtWidgets
-
-from datums.core import META_PREFIX
-
-
-def _resolve_source_refs(datum_obj):
- """Parse Datums_SourceRefs and resolve to (object, subname, shape) tuples."""
- refs_json = getattr(datum_obj, f"{META_PREFIX}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
-
-
-_TYPE_DISPLAY_NAMES = {
- "offset_from_face": "Offset from Face",
- "offset_from_plane": "Offset from Plane",
- "midplane": "Midplane",
- "3_points": "3 Points",
- "normal_to_edge": "Normal to Edge",
- "angled": "Angled from Face",
- "tangent_cylinder": "Tangent to Cylinder",
- "2_points": "2 Points",
- "from_edge": "From Edge",
- "cylinder_center": "Cylinder Center",
- "plane_intersection": "Plane Intersection",
- "vertex": "At Vertex",
- "coordinates": "XYZ Coordinates",
- "on_edge": "On Edge",
- "face_center": "Face Center",
- "circle_center": "Circle Center",
-}
-
-
-class DatumEditTaskPanel:
- """Task panel for editing existing datum objects with Datums_Type metadata."""
-
- def __init__(self, datum_obj):
- self.datum_obj = datum_obj
- self.form = QtWidgets.QWidget()
- self.form.setWindowTitle(f"Edit {datum_obj.Label}")
- self.original_placement = datum_obj.Placement.copy()
- self.original_offset = (
- datum_obj.AttachmentOffset.copy() if hasattr(datum_obj, "AttachmentOffset") else None
- )
- self.original_path_param = (
- datum_obj.MapPathParameter if hasattr(datum_obj, "MapPathParameter") else None
- )
- self._setup_ui()
- self._load_values()
-
- # ------------------------------------------------------------------
- # UI
- # ------------------------------------------------------------------
-
- def _setup_ui(self):
- layout = QtWidgets.QVBoxLayout(self.form)
- layout.setSpacing(8)
-
- # Info
- info_group = QtWidgets.QGroupBox("Datum Info")
- info_layout = QtWidgets.QFormLayout(info_group)
-
- self.name_edit = QtWidgets.QLineEdit(self.datum_obj.Label)
- info_layout.addRow("Name:", self.name_edit)
-
- dtype = getattr(self.datum_obj, f"{META_PREFIX}Type", "unknown")
- type_label = QtWidgets.QLabel(
- _TYPE_DISPLAY_NAMES.get(dtype, dtype),
- )
- type_label.setStyleSheet("color: #cba6f7; font-weight: bold;")
- info_layout.addRow("Type:", type_label)
- layout.addWidget(info_group)
-
- # Parameters
- self.params_group = QtWidgets.QGroupBox("Parameters")
- self.params_layout = QtWidgets.QFormLayout(self.params_group)
-
- self.offset_spin = QtWidgets.QDoubleSpinBox()
- self.offset_spin.setRange(-10000, 10000)
- self.offset_spin.setSuffix(" mm")
- self.offset_spin.valueChanged.connect(self._on_param_changed)
-
- self.angle_spin = QtWidgets.QDoubleSpinBox()
- self.angle_spin.setRange(-360, 360)
- self.angle_spin.setSuffix(" \u00b0")
- self.angle_spin.valueChanged.connect(self._on_param_changed)
-
- self.param_spin = QtWidgets.QDoubleSpinBox()
- self.param_spin.setRange(0, 1)
- self.param_spin.setSingleStep(0.1)
- self.param_spin.valueChanged.connect(self._on_param_changed)
-
- self.x_spin = QtWidgets.QDoubleSpinBox()
- self.x_spin.setRange(-10000, 10000)
- self.x_spin.setSuffix(" mm")
- self.x_spin.valueChanged.connect(self._on_param_changed)
- self.y_spin = QtWidgets.QDoubleSpinBox()
- self.y_spin.setRange(-10000, 10000)
- self.y_spin.setSuffix(" mm")
- self.y_spin.valueChanged.connect(self._on_param_changed)
- self.z_spin = QtWidgets.QDoubleSpinBox()
- self.z_spin.setRange(-10000, 10000)
- self.z_spin.setSuffix(" mm")
- self.z_spin.valueChanged.connect(self._on_param_changed)
-
- layout.addWidget(self.params_group)
-
- # Source references (read-only)
- refs_group = QtWidgets.QGroupBox("Source References")
- refs_layout = QtWidgets.QVBoxLayout(refs_group)
- self.refs_list = QtWidgets.QListWidget()
- self.refs_list.setMaximumHeight(80)
- refs_layout.addWidget(self.refs_list)
- layout.addWidget(refs_group)
-
- # Placement readout
- placement_group = QtWidgets.QGroupBox("Current Placement")
- placement_layout = QtWidgets.QFormLayout(placement_group)
- pos = self.datum_obj.Placement.Base
- self.pos_label = QtWidgets.QLabel(
- f"({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f})",
- )
- placement_layout.addRow("Position:", self.pos_label)
- layout.addWidget(placement_group)
-
- layout.addStretch()
-
- def _load_values(self):
- dtype = getattr(self.datum_obj, f"{META_PREFIX}Type", "")
- params_json = getattr(self.datum_obj, f"{META_PREFIX}Params", "{}")
- refs_json = getattr(self.datum_obj, f"{META_PREFIX}SourceRefs", "[]")
-
- try:
- params = json.loads(params_json)
- except json.JSONDecodeError:
- params = {}
- try:
- refs = json.loads(refs_json)
- except json.JSONDecodeError:
- refs = []
-
- # Clear
- while self.params_layout.rowCount() > 0:
- self.params_layout.removeRow(0)
-
- if dtype in ("offset_from_face", "offset_from_plane"):
- self.offset_spin.setValue(params.get("distance", 10))
- self.params_layout.addRow("Offset:", self.offset_spin)
- elif dtype == "midplane":
- self.offset_spin.setValue(params.get("half_distance", 0))
- self.params_layout.addRow("Half-distance:", self.offset_spin)
- elif dtype == "angled":
- self.angle_spin.setValue(params.get("angle", 45))
- self.params_layout.addRow("Angle:", self.angle_spin)
- elif dtype == "tangent_cylinder":
- self.angle_spin.setValue(params.get("angle", 0))
- self.params_layout.addRow("Angle:", self.angle_spin)
- elif dtype in ("normal_to_edge", "on_edge"):
- self.param_spin.setValue(params.get("parameter", 0.5))
- self.params_layout.addRow("Position (0-1):", self.param_spin)
- elif dtype == "coordinates":
- self.x_spin.setValue(params.get("x", 0))
- self.y_spin.setValue(params.get("y", 0))
- self.z_spin.setValue(params.get("z", 0))
- self.params_layout.addRow("X:", self.x_spin)
- self.params_layout.addRow("Y:", self.y_spin)
- self.params_layout.addRow("Z:", self.z_spin)
- else:
- lbl = QtWidgets.QLabel("No editable parameters")
- lbl.setStyleSheet("color: #888;")
- self.params_layout.addRow(lbl)
-
- # Source refs
- self.refs_list.clear()
- for ref in refs:
- obj_name = ref.get("object", "?")
- subname = ref.get("subname", "")
- text = f"{obj_name}.{subname}" if subname else obj_name
- self.refs_list.addItem(text)
- if not refs:
- self.refs_list.addItem("(no references)")
-
- # ------------------------------------------------------------------
- # Live parameter updates
- # ------------------------------------------------------------------
-
- def _has_attachment(self):
- return hasattr(self.datum_obj, "MapMode") and self.datum_obj.MapMode != "Deactivated"
-
- def _on_param_changed(self):
- dtype = getattr(self.datum_obj, f"{META_PREFIX}Type", "")
-
- if dtype == "coordinates":
- new_pos = App.Vector(
- self.x_spin.value(),
- self.y_spin.value(),
- self.z_spin.value(),
- )
- self.datum_obj.Placement.Base = new_pos
- self._store_params({"x": new_pos.x, "y": new_pos.y, "z": new_pos.z})
-
- elif dtype in ("offset_from_face", "offset_from_plane", "midplane"):
- distance = self.offset_spin.value()
- if self._has_attachment():
- self.datum_obj.AttachmentOffset = App.Placement(
- App.Vector(0, 0, distance),
- App.Rotation(),
- )
- key = "half_distance" if dtype == "midplane" else "distance"
- self._store_params({key: distance})
-
- elif dtype == "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._store_params({"angle": angle})
-
- elif dtype == "tangent_cylinder":
- angle = self.angle_spin.value()
- if self._has_attachment():
- params_json = getattr(self.datum_obj, f"{META_PREFIX}Params", "{}")
- try:
- params = json.loads(params_json)
- except json.JSONDecodeError:
- params = {}
- vertex_angle = params.get("vertex_angle", 0.0)
- offset_rot = App.Rotation(App.Vector(0, 0, 1), angle - vertex_angle)
- self.datum_obj.AttachmentOffset = App.Placement(
- App.Vector(0, 0, 0),
- offset_rot,
- )
- else:
- 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_dir = cyl.Axis
- center = cyl.Center
- radius = cyl.Radius
- rad = math.radians(angle)
- if abs(axis_dir.dot(App.Vector(1, 0, 0))) < 0.99:
- lx = axis_dir.cross(App.Vector(1, 0, 0)).normalize()
- else:
- lx = axis_dir.cross(App.Vector(0, 1, 0)).normalize()
- ly = axis_dir.cross(lx)
- radial = lx * math.cos(rad) + ly * math.sin(rad)
- tp = center + radial * radius
- rot = App.Rotation(App.Vector(0, 0, 1), radial)
- self.datum_obj.Placement = App.Placement(tp, rot)
- self._store_params({"angle": angle})
-
- elif dtype in ("normal_to_edge", "on_edge"):
- parameter = self.param_spin.value()
- if self._has_attachment() and hasattr(self.datum_obj, "MapPathParameter"):
- self.datum_obj.MapPathParameter = parameter
- self._store_params({"parameter": parameter})
-
- # Refresh readout
- pos = self.datum_obj.Placement.Base
- self.pos_label.setText(f"({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f})")
- App.ActiveDocument.recompute()
-
- def _store_params(self, new_values):
- params_json = getattr(self.datum_obj, f"{META_PREFIX}Params", "{}")
- try:
- params = json.loads(params_json)
- except json.JSONDecodeError:
- params = {}
- params.update(new_values)
-
- serializable = {}
- for k, v in params.items():
- if hasattr(v, "x") and hasattr(v, "y") and hasattr(v, "z"):
- serializable[k] = {"_type": "Vector", "x": v.x, "y": v.y, "z": v.z}
- else:
- serializable[k] = v
- setattr(self.datum_obj, f"{META_PREFIX}Params", json.dumps(serializable))
-
- # ------------------------------------------------------------------
- # Task panel protocol
- # ------------------------------------------------------------------
-
- def accept(self):
- new_label = self.name_edit.text().strip()
- if new_label and new_label != self.datum_obj.Label:
- self.datum_obj.Label = new_label
- App.ActiveDocument.recompute()
- return True
-
- def reject(self):
- self.datum_obj.Placement = self.original_placement
- if self.original_offset is not None and hasattr(self.datum_obj, "AttachmentOffset"):
- self.datum_obj.AttachmentOffset = self.original_offset
- if self.original_path_param is not None and hasattr(self.datum_obj, "MapPathParameter"):
- self.datum_obj.MapPathParameter = self.original_path_param
- App.ActiveDocument.recompute()
- return True
-
- def getStandardButtons(self):
- return QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
diff --git a/mods/datums/datums/panel.py b/mods/datums/datums/panel.py
deleted file mode 100644
index af185d273b..0000000000
--- a/mods/datums/datums/panel.py
+++ /dev/null
@@ -1,605 +0,0 @@
-"""Datum Creator task panel.
-
-Provides the main creation UI: selection table, auto-detected mode,
-mode override combo, dynamic parameter spinboxes, and OK/Cancel.
-"""
-
-import FreeCAD as App
-import FreeCADGui as Gui
-from PySide6 import QtCore, QtWidgets
-
-from datums import core
-from datums.detection import (
- CATEGORY_COLORS,
- MODES,
- SelectionItem,
- match_mode,
-)
-
-
-class DatumCreatorTaskPanel:
- """Unified task panel for creating datum planes, axes, and points."""
-
- def __init__(self):
- self.form = QtWidgets.QWidget()
- self.form.setWindowTitle("Datum Creator")
- self.selection_list = []
- self._setup_ui()
- self._setup_selection_observer()
-
- # Auto-capture any existing selection
- sel = Gui.Selection.getSelectionEx()
- if sel:
- self._capture_selection(sel)
-
- self._update_mode_from_selection()
-
- # ------------------------------------------------------------------
- # UI construction
- # ------------------------------------------------------------------
-
- def _setup_ui(self):
- layout = QtWidgets.QVBoxLayout(self.form)
- layout.setSpacing(8)
-
- # --- Selection table ---
- sel_group = QtWidgets.QGroupBox("Selection")
- sel_layout = QtWidgets.QVBoxLayout(sel_group)
-
- self.sel_table = QtWidgets.QTableWidget()
- self.sel_table.setColumnCount(3)
- self.sel_table.setHorizontalHeaderLabels(["", "Element", ""])
- header = self.sel_table.horizontalHeader()
- header.setStretchLastSection(False)
- header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
- header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
- header.setSectionResizeMode(2, QtWidgets.QHeaderView.Fixed)
- header.resizeSection(0, 28)
- header.resizeSection(2, 28)
- self.sel_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
- self.sel_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
- self.sel_table.setMaximumHeight(150)
- self.sel_table.verticalHeader().setVisible(False)
- sel_layout.addWidget(self.sel_table)
-
- btn_row = QtWidgets.QHBoxLayout()
- self.add_sel_btn = QtWidgets.QPushButton("Add Selected")
- self.add_sel_btn.clicked.connect(self._on_add_selection)
- self.remove_sel_btn = QtWidgets.QPushButton("Remove")
- self.remove_sel_btn.clicked.connect(self._on_remove_row)
- self.clear_sel_btn = QtWidgets.QPushButton("Clear All")
- self.clear_sel_btn.clicked.connect(self._on_clear)
- btn_row.addWidget(self.add_sel_btn)
- btn_row.addWidget(self.remove_sel_btn)
- btn_row.addWidget(self.clear_sel_btn)
- sel_layout.addLayout(btn_row)
- layout.addWidget(sel_group)
-
- # --- Detected mode ---
- mode_group = QtWidgets.QGroupBox("Datum Type")
- mode_layout = QtWidgets.QVBoxLayout(mode_group)
-
- self.mode_label = QtWidgets.QLabel("Select geometry to auto-detect mode")
- self.mode_label.setStyleSheet("font-weight: bold; color: #888;")
- mode_layout.addWidget(self.mode_label)
-
- override_row = QtWidgets.QHBoxLayout()
- override_row.addWidget(QtWidgets.QLabel("Override:"))
- self.mode_combo = QtWidgets.QComboBox()
- self.mode_combo.addItem("(Auto-detect)", None)
- for display_name, mode_id, _, category in MODES:
- self.mode_combo.addItem(
- f"[{category[0].upper()}] {display_name}",
- mode_id,
- )
- self.mode_combo.currentIndexChanged.connect(self._on_mode_override)
- override_row.addWidget(self.mode_combo)
- mode_layout.addLayout(override_row)
- layout.addWidget(mode_group)
-
- # --- Parameters ---
- self.params_group = QtWidgets.QGroupBox("Parameters")
- self.params_layout = QtWidgets.QFormLayout(self.params_group)
-
- self.offset_spin = QtWidgets.QDoubleSpinBox()
- self.offset_spin.setRange(-10000, 10000)
- self.offset_spin.setValue(10)
- self.offset_spin.setSuffix(" mm")
-
- self.angle_spin = QtWidgets.QDoubleSpinBox()
- self.angle_spin.setRange(-360, 360)
- self.angle_spin.setValue(45)
- self.angle_spin.setSuffix(" \u00b0")
-
- self.param_spin = QtWidgets.QDoubleSpinBox()
- self.param_spin.setRange(0, 1)
- self.param_spin.setValue(0.5)
- self.param_spin.setSingleStep(0.1)
-
- self.x_spin = QtWidgets.QDoubleSpinBox()
- self.x_spin.setRange(-10000, 10000)
- self.x_spin.setSuffix(" mm")
- self.y_spin = QtWidgets.QDoubleSpinBox()
- self.y_spin.setRange(-10000, 10000)
- self.y_spin.setSuffix(" mm")
- self.z_spin = QtWidgets.QDoubleSpinBox()
- self.z_spin.setRange(-10000, 10000)
- self.z_spin.setSuffix(" mm")
-
- layout.addWidget(self.params_group)
-
- # --- Options ---
- options_group = QtWidgets.QGroupBox("Options")
- options_layout = QtWidgets.QVBoxLayout(options_group)
-
- self.link_spreadsheet_cb = QtWidgets.QCheckBox("Link to Spreadsheet")
- options_layout.addWidget(self.link_spreadsheet_cb)
-
- name_row = QtWidgets.QHBoxLayout()
- self.custom_name_cb = QtWidgets.QCheckBox("Custom Name:")
- self.custom_name_edit = QtWidgets.QLineEdit()
- self.custom_name_edit.setEnabled(False)
- self.custom_name_cb.toggled.connect(self.custom_name_edit.setEnabled)
- name_row.addWidget(self.custom_name_cb)
- name_row.addWidget(self.custom_name_edit)
- options_layout.addLayout(name_row)
-
- layout.addWidget(options_group)
-
- self._update_params_ui(None)
-
- # ------------------------------------------------------------------
- # Selection observer
- # ------------------------------------------------------------------
-
- def _setup_selection_observer(self):
- class _Observer:
- def __init__(self, panel):
- self.panel = panel
-
- def addSelection(self, doc, obj, sub, pos):
- self.panel._on_freecad_sel_changed()
-
- def removeSelection(self, doc, obj, sub):
- self.panel._on_freecad_sel_changed()
-
- def clearSelection(self, doc):
- self.panel._on_freecad_sel_changed()
-
- self._observer = _Observer(self)
- Gui.Selection.addObserver(self._observer)
-
- def _on_freecad_sel_changed(self):
- self.add_sel_btn.setEnabled(bool(Gui.Selection.getSelectionEx()))
-
- # ------------------------------------------------------------------
- # Selection table management
- # ------------------------------------------------------------------
-
- def _capture_selection(self, sel_ex):
- """Add SelectionEx items to the internal list."""
- for s in sel_ex:
- obj = s.Object
- if s.SubElementNames:
- for i, sub in enumerate(s.SubElementNames):
- shape = s.SubObjects[i] if i < len(s.SubObjects) else None
- item = SelectionItem(obj, sub, shape)
- self._add_item(item)
- else:
- self._add_item(SelectionItem(obj, "", None))
-
- def _add_item(self, item):
- for existing in self.selection_list:
- if existing.obj == item.obj and existing.subname == item.subname:
- return
- self.selection_list.append(item)
-
- def _on_add_selection(self):
- self._capture_selection(Gui.Selection.getSelectionEx())
- self._refresh_table()
- self._update_mode_from_selection()
-
- def _on_remove_row(self):
- rows = self.sel_table.selectionModel().selectedRows()
- for row in sorted((r.row() for r in rows), reverse=True):
- if row < len(self.selection_list):
- del self.selection_list[row]
- self._refresh_table()
- self._update_mode_from_selection()
-
- def _on_clear(self):
- self.selection_list.clear()
- self._refresh_table()
- self._update_mode_from_selection()
-
- def _refresh_table(self):
- self.sel_table.setRowCount(len(self.selection_list))
- for i, item in enumerate(self.selection_list):
- type_item = QtWidgets.QTableWidgetItem(item.type_icon)
- type_item.setTextAlignment(QtCore.Qt.AlignCenter)
- type_item.setToolTip(item.geo_type)
- self.sel_table.setItem(i, 0, type_item)
-
- self.sel_table.setItem(
- i,
- 1,
- QtWidgets.QTableWidgetItem(item.display_name),
- )
-
- btn = QtWidgets.QPushButton("\u2715")
- btn.setFixedSize(24, 24)
- btn.clicked.connect(lambda checked, r=i: self._remove_at(r))
- self.sel_table.setCellWidget(i, 2, btn)
-
- def _remove_at(self, row):
- if row < len(self.selection_list):
- del self.selection_list[row]
- self._refresh_table()
- self._update_mode_from_selection()
-
- # ------------------------------------------------------------------
- # Mode detection
- # ------------------------------------------------------------------
-
- def _update_mode_from_selection(self):
- if self.mode_combo.currentIndex() > 0:
- mode_id = self.mode_combo.currentData()
- self._apply_mode(mode_id)
- return
-
- if not self.selection_list:
- self.mode_label.setText("Select geometry to auto-detect mode")
- self.mode_label.setStyleSheet("font-weight: bold; color: #888;")
- self._update_params_ui(None)
- return
-
- result = match_mode(self.selection_list)
- if result:
- display_name, mode_id, category = result
- color = CATEGORY_COLORS.get(category, "#cdd6f4")
- self.mode_label.setText(display_name)
- self.mode_label.setStyleSheet(f"font-weight: bold; color: {color};")
- self._update_params_ui(mode_id)
- else:
- self.mode_label.setText("No matching mode for selection")
- self.mode_label.setStyleSheet("font-weight: bold; color: #f38ba8;")
- self._update_params_ui(None)
-
- def _on_mode_override(self, index):
- if index == 0:
- self._update_mode_from_selection()
- else:
- self._apply_mode(self.mode_combo.currentData())
-
- def _apply_mode(self, mode_id):
- for display_name, mid, _, category in MODES:
- if mid == mode_id:
- color = CATEGORY_COLORS.get(category, "#cdd6f4")
- self.mode_label.setText(display_name)
- self.mode_label.setStyleSheet(f"font-weight: bold; color: {color};")
- self._update_params_ui(mode_id)
- return
-
- # ------------------------------------------------------------------
- # Dynamic parameter UI
- # ------------------------------------------------------------------
-
- def _clear_params_layout(self):
- while self.params_layout.count():
- item = self.params_layout.takeAt(0)
- w = item.widget()
- if w is not None:
- w.hide()
- w.setParent(None)
-
- def _update_params_ui(self, mode_id):
- self._clear_params_layout()
-
- if mode_id is None:
- self.params_group.setVisible(False)
- return
-
- self.params_group.setVisible(True)
-
- if mode_id in ("offset_face", "offset_plane"):
- self.offset_spin.show()
- self.params_layout.addRow("Offset:", self.offset_spin)
- elif mode_id == "midplane":
- self.params_group.setVisible(False)
- elif mode_id == "angled":
- self.angle_spin.show()
- self.params_layout.addRow("Angle:", self.angle_spin)
- elif mode_id == "normal_edge":
- self.param_spin.show()
- self.params_layout.addRow("Position (0-1):", self.param_spin)
- elif mode_id == "tangent_cyl":
- self.angle_spin.show()
- self.params_layout.addRow("Angle:", self.angle_spin)
- elif mode_id == "point_xyz":
- self.x_spin.show()
- self.y_spin.show()
- self.z_spin.show()
- self.params_layout.addRow("X:", self.x_spin)
- self.params_layout.addRow("Y:", self.y_spin)
- self.params_layout.addRow("Z:", self.z_spin)
- elif mode_id == "point_edge":
- self.param_spin.show()
- self.params_layout.addRow("Position (0-1):", self.param_spin)
- else:
- self.params_group.setVisible(False)
-
- # ------------------------------------------------------------------
- # Getters for create_datum
- # ------------------------------------------------------------------
-
- def _get_current_mode(self):
- if self.mode_combo.currentIndex() > 0:
- return self.mode_combo.currentData()
- result = match_mode(self.selection_list)
- return result[1] if result else None
-
- def _get_name(self):
- if self.custom_name_cb.isChecked() and self.custom_name_edit.text():
- return self.custom_name_edit.text()
- return None
-
- def _items_by_type(self, *geo_types):
- return [it for it in self.selection_list if it.geo_type in geo_types]
-
- # ------------------------------------------------------------------
- # Creation dispatch
- # ------------------------------------------------------------------
-
- def _create_datum(self):
- mode = self._get_current_mode()
- if mode is None:
- raise ValueError("No valid mode detected. Add geometry to the selection.")
-
- name = self._get_name()
- link_ss = self.link_spreadsheet_cb.isChecked()
-
- # --- Planes ---
- if mode == "offset_face":
- items = self._items_by_type("face", "cylinder")
- if not items:
- raise ValueError("Select a face")
- it = items[0]
- face = it.shape if it.shape else it.obj.Shape.Faces[0]
- core.plane_offset_from_face(
- face,
- self.offset_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "offset_plane":
- items = self._items_by_type("plane")
- if not items:
- raise ValueError("Select a datum plane")
- core.plane_offset_from_plane(
- items[0].obj,
- self.offset_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- )
-
- elif mode == "midplane":
- items = self._items_by_type("face", "cylinder")
- if len(items) < 2:
- raise ValueError("Select 2 faces")
- f1 = items[0].shape if items[0].shape else items[0].obj.Shape.Faces[0]
- f2 = items[1].shape if items[1].shape else items[1].obj.Shape.Faces[0]
- core.plane_midplane(
- f1,
- f2,
- name=name,
- source_object1=items[0].obj,
- source_subname1=items[0].subname,
- source_object2=items[1].obj,
- source_subname2=items[1].subname,
- )
-
- elif mode == "3_points":
- items = self._items_by_type("vertex")
- if len(items) < 3:
- raise ValueError("Select 3 vertices")
- verts = [it.shape if it.shape else it.obj.Shape.Vertexes[0] for it in items[:3]]
- core.plane_from_3_points(
- verts[0].Point,
- verts[1].Point,
- verts[2].Point,
- name=name,
- source_refs=[(it.obj, it.subname) for it in items[:3]],
- )
-
- elif mode == "normal_edge":
- items = self._items_by_type("edge", "circle")
- if not items:
- raise ValueError("Select an edge")
- it = items[0]
- edge = it.shape if it.shape else it.obj.Shape.Edges[0]
- core.plane_normal_to_edge(
- edge,
- parameter=self.param_spin.value(),
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "angled":
- faces = self._items_by_type("face", "cylinder")
- edges = self._items_by_type("edge", "circle")
- if not faces or not edges:
- raise ValueError("Select a face and an edge")
- face = faces[0].shape if faces[0].shape else faces[0].obj.Shape.Faces[0]
- edge = edges[0].shape if edges[0].shape else edges[0].obj.Shape.Edges[0]
- core.plane_angled(
- face,
- edge,
- self.angle_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- source_face_obj=faces[0].obj,
- source_face_sub=faces[0].subname,
- source_edge_obj=edges[0].obj,
- source_edge_sub=edges[0].subname,
- )
-
- elif mode == "tangent_cyl":
- items = self._items_by_type("cylinder")
- if not items:
- raise ValueError("Select a cylindrical face")
- it = items[0]
- face = it.shape if it.shape else it.obj.Shape.Faces[0]
- core.plane_tangent_to_cylinder(
- face,
- angle=self.angle_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- # --- Axes ---
- elif mode == "axis_2pt":
- items = self._items_by_type("vertex")
- if len(items) < 2:
- raise ValueError("Select 2 vertices")
- v1 = items[0].shape if items[0].shape else items[0].obj.Shape.Vertexes[0]
- v2 = items[1].shape if items[1].shape else items[1].obj.Shape.Vertexes[0]
- core.axis_from_2_points(
- v1.Point,
- v2.Point,
- name=name,
- source_refs=[(items[0].obj, items[0].subname), (items[1].obj, items[1].subname)],
- )
-
- elif mode == "axis_edge":
- items = self._items_by_type("edge")
- if not items:
- raise ValueError("Select a linear edge")
- it = items[0]
- edge = it.shape if it.shape else it.obj.Shape.Edges[0]
- core.axis_from_edge(
- edge,
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "axis_cyl":
- items = self._items_by_type("cylinder")
- if not items:
- raise ValueError("Select a cylindrical face")
- it = items[0]
- face = it.shape if it.shape else it.obj.Shape.Faces[0]
- core.axis_cylinder_center(
- face,
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "axis_intersect":
- items = self._items_by_type("plane")
- if len(items) < 2:
- raise ValueError("Select 2 datum planes")
- core.axis_intersection_planes(
- items[0].obj,
- items[1].obj,
- name=name,
- source_object1=items[0].obj,
- source_subname1="",
- source_object2=items[1].obj,
- source_subname2="",
- )
-
- # --- Points ---
- elif mode == "point_vertex":
- items = self._items_by_type("vertex")
- if not items:
- raise ValueError("Select a vertex")
- it = items[0]
- vert = it.shape if it.shape else it.obj.Shape.Vertexes[0]
- core.point_at_vertex(
- vert,
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "point_xyz":
- core.point_at_coordinates(
- self.x_spin.value(),
- self.y_spin.value(),
- self.z_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- )
-
- elif mode == "point_edge":
- items = self._items_by_type("edge", "circle")
- if not items:
- raise ValueError("Select an edge")
- it = items[0]
- edge = it.shape if it.shape else it.obj.Shape.Edges[0]
- core.point_on_edge(
- edge,
- parameter=self.param_spin.value(),
- name=name,
- link_spreadsheet=link_ss,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "point_face":
- items = self._items_by_type("face", "cylinder")
- if not items:
- raise ValueError("Select a face")
- it = items[0]
- face = it.shape if it.shape else it.obj.Shape.Faces[0]
- core.point_center_of_face(
- face,
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- elif mode == "point_circle":
- items = self._items_by_type("circle")
- if not items:
- raise ValueError("Select a circular edge")
- it = items[0]
- edge = it.shape if it.shape else it.obj.Shape.Edges[0]
- core.point_center_of_circle(
- edge,
- name=name,
- source_object=it.obj,
- source_subname=it.subname,
- )
-
- else:
- raise ValueError(f"Unknown mode: {mode}")
-
- # ------------------------------------------------------------------
- # Task panel protocol
- # ------------------------------------------------------------------
-
- def accept(self):
- Gui.Selection.removeObserver(self._observer)
- try:
- self._create_datum()
- App.Console.PrintMessage("Datums: Datum created successfully\n")
- except Exception as e:
- App.Console.PrintError(f"Datums: Failed to create datum: {e}\n")
- QtWidgets.QMessageBox.warning(self.form, "Error", str(e))
- return True
-
- def reject(self):
- Gui.Selection.removeObserver(self._observer)
- return True
-
- def getStandardButtons(self):
- return QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
diff --git a/mods/datums/datums/resources/icons/axis_2pt.svg b/mods/datums/datums/resources/icons/axis_2pt.svg
deleted file mode 100644
index 439f092fc1..0000000000
--- a/mods/datums/datums/resources/icons/axis_2pt.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/axis_cyl.svg b/mods/datums/datums/resources/icons/axis_cyl.svg
deleted file mode 100644
index 0e723e7dab..0000000000
--- a/mods/datums/datums/resources/icons/axis_cyl.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/axis_edge.svg b/mods/datums/datums/resources/icons/axis_edge.svg
deleted file mode 100644
index ab54592d17..0000000000
--- a/mods/datums/datums/resources/icons/axis_edge.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/axis_intersect.svg b/mods/datums/datums/resources/icons/axis_intersect.svg
deleted file mode 100644
index 4069f734ae..0000000000
--- a/mods/datums/datums/resources/icons/axis_intersect.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/datum_creator.svg b/mods/datums/datums/resources/icons/datum_creator.svg
deleted file mode 100644
index de02931b3c..0000000000
--- a/mods/datums/datums/resources/icons/datum_creator.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_3pt.svg b/mods/datums/datums/resources/icons/plane_3pt.svg
deleted file mode 100644
index 4ed7efb1e9..0000000000
--- a/mods/datums/datums/resources/icons/plane_3pt.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_angled.svg b/mods/datums/datums/resources/icons/plane_angled.svg
deleted file mode 100644
index 6790fcdfa4..0000000000
--- a/mods/datums/datums/resources/icons/plane_angled.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_midplane.svg b/mods/datums/datums/resources/icons/plane_midplane.svg
deleted file mode 100644
index bdd5ea96ae..0000000000
--- a/mods/datums/datums/resources/icons/plane_midplane.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_normal.svg b/mods/datums/datums/resources/icons/plane_normal.svg
deleted file mode 100644
index e564b74c80..0000000000
--- a/mods/datums/datums/resources/icons/plane_normal.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_offset.svg b/mods/datums/datums/resources/icons/plane_offset.svg
deleted file mode 100644
index 7f1f35bcda..0000000000
--- a/mods/datums/datums/resources/icons/plane_offset.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/plane_tangent.svg b/mods/datums/datums/resources/icons/plane_tangent.svg
deleted file mode 100644
index b683e2f3ae..0000000000
--- a/mods/datums/datums/resources/icons/plane_tangent.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/point_circle.svg b/mods/datums/datums/resources/icons/point_circle.svg
deleted file mode 100644
index b05da8bc34..0000000000
--- a/mods/datums/datums/resources/icons/point_circle.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/point_edge.svg b/mods/datums/datums/resources/icons/point_edge.svg
deleted file mode 100644
index 7032701305..0000000000
--- a/mods/datums/datums/resources/icons/point_edge.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/point_face.svg b/mods/datums/datums/resources/icons/point_face.svg
deleted file mode 100644
index 887201af3d..0000000000
--- a/mods/datums/datums/resources/icons/point_face.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/point_vertex.svg b/mods/datums/datums/resources/icons/point_vertex.svg
deleted file mode 100644
index 4db1731423..0000000000
--- a/mods/datums/datums/resources/icons/point_vertex.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/datums/resources/icons/point_xyz.svg b/mods/datums/datums/resources/icons/point_xyz.svg
deleted file mode 100644
index 37a355a363..0000000000
--- a/mods/datums/datums/resources/icons/point_xyz.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
\ No newline at end of file
diff --git a/mods/datums/package.xml b/mods/datums/package.xml
deleted file mode 100644
index 50e484f4ad..0000000000
--- a/mods/datums/package.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
- datums
- Unified datum creator for Kindred Create
- 0.1.0
- Kindred Systems
- LGPL-2.1-or-later
- https://git.kindred-systems.com/kindred/create
-
-
-
- DatumCommandProvider
- datums
-
-
-
-
- 0.1.0
- 45
-
- sdk
-
-
-