From 3f688873c6589eb01c4d2e732e7fb719a0484c52 Mon Sep 17 00:00:00 2001 From: forbes Date: Tue, 3 Mar 2026 08:27:31 -0600 Subject: [PATCH] chore(datums): convert datums addon to submodule Convert mods/datums/ from tracked files to a git submodule pointing to https://git.kindred-systems.com/kindred/datums.git (branch: main). Follows the same submodule pattern as silo, solver, and gears. The datums remote repo was initialized from the existing tracked files with the package.xml repository URL updated to point to the new repo. CMake install targets in src/Mod/Create/CMakeLists.txt continue to work unchanged since the files remain at the same paths. --- .gitmodules | 4 + mods/datums | 1 + mods/datums/Init.py | 1 - mods/datums/InitGui.py | 40 - mods/datums/datums/__init__.py | 1 - mods/datums/datums/command.py | 145 --- mods/datums/datums/core.py | 1061 ----------------- mods/datums/datums/detection.py | 169 --- mods/datums/datums/edit_panel.py | 334 ------ mods/datums/datums/panel.py | 605 ---------- .../datums/resources/icons/axis_2pt.svg | 8 - .../datums/resources/icons/axis_cyl.svg | 11 - .../datums/resources/icons/axis_edge.svg | 9 - .../datums/resources/icons/axis_intersect.svg | 9 - .../datums/resources/icons/datum_creator.svg | 8 - .../datums/resources/icons/plane_3pt.svg | 9 - .../datums/resources/icons/plane_angled.svg | 9 - .../datums/resources/icons/plane_midplane.svg | 9 - .../datums/resources/icons/plane_normal.svg | 9 - .../datums/resources/icons/plane_offset.svg | 10 - .../datums/resources/icons/plane_tangent.svg | 9 - .../datums/resources/icons/point_circle.svg | 10 - .../datums/resources/icons/point_edge.svg | 9 - .../datums/resources/icons/point_face.svg | 8 - .../datums/resources/icons/point_vertex.svg | 10 - .../datums/resources/icons/point_xyz.svg | 12 - mods/datums/package.xml | 24 - 27 files changed, 5 insertions(+), 2529 deletions(-) create mode 160000 mods/datums delete mode 100644 mods/datums/Init.py delete mode 100644 mods/datums/InitGui.py delete mode 100644 mods/datums/datums/__init__.py delete mode 100644 mods/datums/datums/command.py delete mode 100644 mods/datums/datums/core.py delete mode 100644 mods/datums/datums/detection.py delete mode 100644 mods/datums/datums/edit_panel.py delete mode 100644 mods/datums/datums/panel.py delete mode 100644 mods/datums/datums/resources/icons/axis_2pt.svg delete mode 100644 mods/datums/datums/resources/icons/axis_cyl.svg delete mode 100644 mods/datums/datums/resources/icons/axis_edge.svg delete mode 100644 mods/datums/datums/resources/icons/axis_intersect.svg delete mode 100644 mods/datums/datums/resources/icons/datum_creator.svg delete mode 100644 mods/datums/datums/resources/icons/plane_3pt.svg delete mode 100644 mods/datums/datums/resources/icons/plane_angled.svg delete mode 100644 mods/datums/datums/resources/icons/plane_midplane.svg delete mode 100644 mods/datums/datums/resources/icons/plane_normal.svg delete mode 100644 mods/datums/datums/resources/icons/plane_offset.svg delete mode 100644 mods/datums/datums/resources/icons/plane_tangent.svg delete mode 100644 mods/datums/datums/resources/icons/point_circle.svg delete mode 100644 mods/datums/datums/resources/icons/point_edge.svg delete mode 100644 mods/datums/datums/resources/icons/point_face.svg delete mode 100644 mods/datums/datums/resources/icons/point_vertex.svg delete mode 100644 mods/datums/datums/resources/icons/point_xyz.svg delete mode 100644 mods/datums/package.xml 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 @@ - - - - - - - - t - \ 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 - - -