chore(datums): convert datums addon to submodule
Some checks failed
Build and Test / build (pull_request) Has been cancelled

Convert mods/datums/ from tracked files to a git submodule pointing
to https://git.kindred-systems.com/kindred/datums.git (branch: main).

Follows the same submodule pattern as silo, solver, and gears.
The datums remote repo was initialized from the existing tracked files
with the package.xml repository URL updated to point to the new repo.

CMake install targets in src/Mod/Create/CMakeLists.txt continue to
work unchanged since the files remain at the same paths.
This commit is contained in:
forbes
2026-03-03 08:27:31 -06:00
parent c60b4dbee1
commit 3f688873c6
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>