chore(datums): convert datums addon to submodule #375

Merged
forbes merged 1 commits from chore/datums-submodule into main 2026-03-03 14:29:50 +00:00
27 changed files with 5 additions and 2529 deletions

4
.gitmodules vendored
View File

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

1
mods/datums Submodule

Submodule mods/datums added at e075dc9256

View File

@@ -1 +0,0 @@
"""Datums addon — console initialization (no-op)."""

View File

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

View File

@@ -1 +0,0 @@
"""Unified datum creator for Kindred Create."""

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Axis line -->
<line x1="6" y1="26" x2="26" y2="6" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
<!-- End points -->
<circle cx="6" cy="26" r="3" fill="#fab387"/>
<circle cx="26" cy="6" r="3" fill="#fab387"/>
</svg>

Before

Width:  |  Height:  |  Size: 372 B

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Cylinder -->
<ellipse cx="16" cy="8" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
<path d="M8 8 L8 24" stroke="#7f849c" stroke-width="1"/>
<path d="M24 8 L24 24" stroke="#7f849c" stroke-width="1"/>
<ellipse cx="16" cy="24" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
<!-- Center axis -->
<line x1="16" y1="4" x2="16" y2="28" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="16" cy="16" r="2" fill="#fab387"/>
</svg>

Before

Width:  |  Height:  |  Size: 629 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Box edge representation -->
<path d="M8 24 L8 12 L20 8 L20 20 Z" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
<!-- Selected edge highlighted -->
<line x1="8" y1="24" x2="8" y2="12" stroke="#f9e2af" stroke-width="3" stroke-linecap="round"/>
<!-- Resulting axis -->
<line x1="8" y1="28" x2="8" y2="4" stroke="#f38ba8" stroke-width="2" stroke-dasharray="4,2"/>
</svg>

Before

Width:  |  Height:  |  Size: 515 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- First plane -->
<path d="M4 12 L16 6 L28 12 L16 18 Z" fill="#89b4fa" fill-opacity="0.5" stroke="#74c7ec" stroke-width="1"/>
<!-- Second plane -->
<path d="M4 20 L16 14 L28 20 L16 26 Z" fill="#cba6f7" fill-opacity="0.5" stroke="#b4befe" stroke-width="1"/>
<!-- Intersection axis -->
<line x1="4" y1="16" x2="28" y2="16" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Plane representation -->
<path d="M6 20 L16 8 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Plus sign -->
<circle cx="24" cy="8" r="6" fill="#a6e3a1"/>
<path d="M24 5 L24 11 M21 8 L27 8" stroke="#1e1e2e" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 443 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Plane -->
<path d="M4 20 L16 8 L28 18 L14 28 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Three points -->
<circle cx="8" cy="20" r="3" fill="#fab387"/>
<circle cx="16" cy="10" r="3" fill="#fab387"/>
<circle cx="24" cy="18" r="3" fill="#fab387"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Base plane -->
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
<!-- Angled plane -->
<path d="M8 10 L20 6 L24 18 L12 22 Z" fill="#cba6f7" fill-opacity="0.7" stroke="#b4befe" stroke-width="1.5"/>
<!-- Angle arc -->
<path d="M14 20 Q18 18 18 14" stroke="#fab387" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 508 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Top plane -->
<path d="M4 10 L16 4 L28 10 L16 14 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
<!-- Middle plane (result) -->
<path d="M4 16 L16 10 L28 16 L16 20 Z" fill="#a6e3a1" fill-opacity="0.7" stroke="#94e2d5" stroke-width="1.5"/>
<!-- Bottom plane -->
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 508 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Edge/curve -->
<path d="M6 26 Q16 6 26 16" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Plane perpendicular -->
<path d="M12 8 L20 12 L20 24 L12 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Point on curve -->
<circle cx="16" cy="16" r="2.5" fill="#fab387"/>
</svg>

Before

Width:  |  Height:  |  Size: 480 B

View File

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Base plane -->
<path d="M4 22 L16 14 L28 22 L16 28 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
<!-- Offset plane -->
<path d="M4 14 L16 6 L28 14 L16 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Offset arrow -->
<path d="M16 24 L16 18" stroke="#fab387" stroke-width="2" stroke-linecap="round"/>
<path d="M14 20 L16 17 L18 20" stroke="#fab387" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 621 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Cylinder outline -->
<ellipse cx="12" cy="16" rx="6" ry="10" fill="none" stroke="#f9e2af" stroke-width="2"/>
<!-- Tangent plane -->
<path d="M18 6 L28 10 L28 26 L18 22 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Tangent point -->
<circle cx="18" cy="16" r="2.5" fill="#fab387"/>
</svg>

Before

Width:  |  Height:  |  Size: 466 B

View File

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Circle -->
<circle cx="16" cy="16" r="10" fill="none" stroke="#f9e2af" stroke-width="2.5"/>
<!-- Center point -->
<circle cx="16" cy="16" r="4" fill="#a6e3a1"/>
<circle cx="16" cy="16" r="2" fill="#94e2d5"/>
<!-- Radius line -->
<line x1="16" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Edge/curve -->
<path d="M6 24 Q16 4 26 20" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<!-- Point on edge -->
<circle cx="14" cy="12" r="4" fill="#a6e3a1"/>
<!-- Parameter indicator -->
<text x="20" y="10" font-family="monospace" font-size="8" fill="#cdd6f4">t</text>
</svg>

Before

Width:  |  Height:  |  Size: 448 B

View File

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Face -->
<path d="M6 20 L16 10 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
<!-- Center point -->
<circle cx="16" cy="19" r="4" fill="#a6e3a1"/>
<circle cx="16" cy="19" r="2" fill="#94e2d5"/>
</svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Wireframe box corner -->
<path d="M10 20 L10 10 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
<path d="M10 20 L20 16 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
<path d="M10 20 L4 24" stroke="#7f849c" stroke-width="1.5" fill="none"/>
<!-- Vertex point -->
<circle cx="10" cy="20" r="4" fill="#a6e3a1"/>
<circle cx="10" cy="20" r="2" fill="#94e2d5"/>
</svg>

Before

Width:  |  Height:  |  Size: 527 B

View File

@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Coordinate axes -->
<line x1="6" y1="24" x2="26" y2="24" stroke="#f38ba8" stroke-width="1.5"/>
<line x1="6" y1="24" x2="6" y2="6" stroke="#a6e3a1" stroke-width="1.5"/>
<line x1="6" y1="24" x2="16" y2="28" stroke="#89b4fa" stroke-width="1.5"/>
<!-- Point -->
<circle cx="18" cy="12" r="4" fill="#fab387"/>
<!-- Projection lines -->
<line x1="18" y1="12" x2="18" y2="24" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
<line x1="18" y1="12" x2="6" y2="12" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
</svg>

Before

Width:  |  Height:  |  Size: 681 B

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>datums</name>
<description>Unified datum creator for Kindred Create</description>
<version>0.1.0</version>
<maintainer email="development@kindred-systems.com">Kindred Systems</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository">https://git.kindred-systems.com/kindred/create</url>
<content>
<workbench>
<classname>DatumCommandProvider</classname>
<subdirectory>datums</subdirectory>
</workbench>
</content>
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>45</load_priority>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
</kindred>
</package>