feat(datums): add unified datum creator addon
All checks were successful
Build and Test / build (pull_request) Successful in 29m22s
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)
This commit is contained in:
169
mods/datums/datums/detection.py
Normal file
169
mods/datums/datums/detection.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user