All checks were successful
Build and Test / build (pull_request) Successful in 29m22s
Introduces mods/datums/ — a standalone addon that replaces the three stock PartDesign datum commands (Plane, Line, Point) with a single unified Create_DatumCreator command. Features: - 16 smart creation modes (7 plane, 4 axis, 5 point) - Auto-detection engine: selects best mode from geometry selection - Mode override combo for manual selection - Dynamic parameter UI (offset, angle, position, XYZ) - Datums_Type/Params/SourceRefs metadata for edit panel - Edit panel with real-time parameter updates via AttachExtension - Catppuccin Mocha themed plane styling via SDK theme tokens - Injected into partdesign.body and partdesign.feature contexts Also adds CMake install targets for gears and datums addons. Ported from archived ztools with key changes: - Property prefix: ZTools_ -> Datums_ - No document-level datums (Body-only) - PySide -> PySide6 - SDK integration (register_command, inject_commands, get_theme_tokens)
335 lines
13 KiB
Python
335 lines
13 KiB
Python
"""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
|