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)
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
"""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
|