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 - - -