3 Commits

Author SHA1 Message Date
forbes
04f9df75cb fix: update AttachmentOffset during angled datum editing (#66)
on_param_changed() now recomputes AttachmentOffset.Rotation for angled
datums and recalculates Placement for tangent_cylinder datums when the
angle spinner changes. Previously only ZTools_Params was updated,
leaving the visual representation unchanged until a manual recompute.

Add _resolve_source_refs() helper to parse ZTools_SourceRefs and
resolve stored object/subname pairs to actual shapes for the rotation
and placement math.
2026-02-08 18:42:28 -06:00
forbes
2132c4e64c fix: use append instead of fragile insert chain in PartDesign menu (#57)
Replace the chained insert operations in modifyMenuBar() with independent
append operations. The old approach anchored on PartDesign_Boolean and
chained each subsequent command off the previous insertion — a single
missing anchor caused a cascade failure.

The new approach uses append with PartDesign_Body as the parent locator.
Each operation is independent, so a failure in one does not affect the
others.
2026-02-08 18:07:18 -06:00
forbes
12e332240a fix: register commands and manipulator at module scope (#52)
Move command imports and PartDesign manipulator installation from
ZToolsWorkbench.Initialize() to module scope. This ensures commands
are registered and the manipulator is available before any workbench
activates, fixing the case where PartDesign activates before ZTools
and ztools buttons never appear.
2026-02-08 17:57:54 -06:00
13 changed files with 83 additions and 710 deletions

View File

@@ -66,14 +66,8 @@ class ZToolsWorkbench(Gui.Workbench):
except Exception:
pass
from ztools.commands import (
appearance_commands,
assembly_pattern_commands,
datum_commands,
pattern_commands,
pocket_commands,
spreadsheet_commands,
)
# Command imports moved to module scope (after Gui.addWorkbench) so they
# are available before Initialize() runs. See end of file.
# =====================================================================
# PartDesign Structure Tools
@@ -254,18 +248,6 @@ class ZToolsWorkbench(Gui.Workbench):
"ZTools_SpreadsheetQuickAlias",
]
# =====================================================================
# ZTools Appearance Mode Tools
# =====================================================================
self.ztools_appearance_tools = [
"ZTools_AppearanceMode",
"ZTools_SetCategory",
]
self.ztools_appearance_menu_tools = [
"ZTools_AppearanceRealistic",
"ZTools_AppearanceEngineering",
]
# =====================================================================
# Append Toolbars
# =====================================================================
@@ -288,7 +270,6 @@ class ZToolsWorkbench(Gui.Workbench):
self.appendToolbar("ztools Assembly", self.ztools_assembly_tools)
self.appendToolbar("Spreadsheet", self.spreadsheet_tools)
self.appendToolbar("ztools Spreadsheet", self.ztools_spreadsheet_tools)
self.appendToolbar("ztools Appearance", self.ztools_appearance_tools)
# =====================================================================
# Append Menus
@@ -311,27 +292,15 @@ class ZToolsWorkbench(Gui.Workbench):
self.appendMenu(["Assembly", "Management"], self.assembly_management_tools)
self.appendMenu(["Spreadsheet", "Edit"], self.spreadsheet_tools)
self.appendMenu(["Spreadsheet", "Format"], self.ztools_spreadsheet_tools)
self.appendMenu(
["View", "Appearance Mode"],
self.ztools_appearance_menu_tools + ["ZTools_SetCategory"],
)
self.appendMenu(
"ztools",
self.ztools_datum_tools
+ self.ztools_pattern_tools
+ self.ztools_pocket_tools
+ self.ztools_assembly_tools
+ self.ztools_spreadsheet_tools
+ self.ztools_appearance_menu_tools
+ ["ZTools_SetCategory"],
+ self.ztools_spreadsheet_tools,
)
# Register the PartDesign manipulator now that commands exist.
# Guard so it only registers once even if Initialize is called again.
if not getattr(ZToolsWorkbench, "_manipulator_installed", False):
ZToolsWorkbench._manipulator_installed = True
Gui.addWorkbenchManipulator(_ZToolsPartDesignManipulator())
App.Console.PrintMessage("ztools workbench initialized\n")
def Activated(self):
@@ -344,24 +313,11 @@ class ZToolsWorkbench(Gui.Workbench):
except Exception as e:
App.Console.PrintWarning(f"Could not apply spreadsheet colors: {e}\n")
# Activate appearance mode system
try:
from ztools.appearance import get_manager
get_manager().activate()
except Exception as e:
App.Console.PrintWarning(f"Could not activate appearance system: {e}\n")
App.Console.PrintMessage("ztools workbench activated\n")
def Deactivated(self):
"""Called when workbench is deactivated."""
try:
from ztools.appearance import get_manager
get_manager().deactivate()
except Exception:
pass
pass
def GetClassName(self):
return "Gui::PythonWorkbench"
@@ -370,11 +326,24 @@ class ZToolsWorkbench(Gui.Workbench):
Gui.addWorkbench(ZToolsWorkbench())
# ---------------------------------------------------------------------------
# Eager command registration
# ---------------------------------------------------------------------------
# Import command modules at module scope so Gui.addCommand() calls run before
# any workbench activates. This ensures the PartDesign manipulator can
# reference them regardless of workbench activation order (#52).
from ztools.commands import (
assembly_pattern_commands,
datum_commands,
pattern_commands,
pocket_commands,
spreadsheet_commands,
)
# ---------------------------------------------------------------------------
# WorkbenchManipulator: inject ZTools commands into PartDesign workbench
# ---------------------------------------------------------------------------
# Registered in ZToolsWorkbench.Initialize() after commands are imported,
# so the commands exist before the manipulator references them.
class _ZToolsPartDesignManipulator:
@@ -396,24 +365,11 @@ class _ZToolsPartDesignManipulator:
def modifyMenuBar(self):
return [
{
"insert": "ZTools_DatumCreator",
"menuItem": "PartDesign_Boolean",
"after": "",
},
{
"insert": "ZTools_DatumManager",
"menuItem": "ZTools_DatumCreator",
"after": "",
},
{
"insert": "ZTools_EnhancedPocket",
"menuItem": "ZTools_DatumManager",
"after": "",
},
{
"insert": "ZTools_RotatedLinearPattern",
"menuItem": "ZTools_EnhancedPocket",
"after": "",
},
{"append": "ZTools_DatumCreator", "menuItem": "PartDesign_Body"},
{"append": "ZTools_DatumManager", "menuItem": "PartDesign_Body"},
{"append": "ZTools_EnhancedPocket", "menuItem": "PartDesign_Body"},
{"append": "ZTools_RotatedLinearPattern", "menuItem": "PartDesign_Body"},
]
Gui.addWorkbenchManipulator(_ZToolsPartDesignManipulator())

View File

@@ -1,4 +0,0 @@
# ztools/appearance — Appearance mode system
from .manager import get_manager
__all__ = ["get_manager"]

View File

@@ -1,79 +0,0 @@
# ztools/appearance/engineering.py
# Engineering mode — colors parts by KindredCategory using palette
import FreeCAD as App
from .mode import AppearanceMode
from .palette import load_palette
KINDRED_CATEGORIES = [
"custom_body",
"fastener",
"structural",
"electrical",
"seal_gasket",
"bearing_bushing",
"spring_compliant",
"moving_part",
]
DEFAULT_CATEGORY = "custom_body"
def _is_eligible(obj) -> bool:
"""Check if an object should be processed by appearance modes."""
return (
hasattr(obj, "isDerivedFrom")
and obj.isDerivedFrom("Part::Feature")
and hasattr(obj, "ViewObject")
and obj.ViewObject is not None
and hasattr(obj.ViewObject, "ShapeColor")
)
def ensure_category_property(obj) -> None:
"""Add KindredCategory enum property to an object if it doesn't exist."""
if not hasattr(obj, "KindredCategory"):
obj.addProperty(
"App::PropertyEnumeration",
"KindredCategory",
"Kindred",
"Part category for engineering appearance mode",
)
obj.KindredCategory = KINDRED_CATEGORIES
obj.KindredCategory = DEFAULT_CATEGORY
class EngineeringMode(AppearanceMode):
"""Colors parts by their KindredCategory using the active palette."""
name = "engineering"
def apply(self, doc) -> None:
for obj in doc.Objects:
if _is_eligible(obj):
self.apply_to_object(obj)
def reset(self, doc) -> None:
from . import get_manager
manager = get_manager()
for obj in doc.Objects:
if _is_eligible(obj):
manager.restore_original(obj)
def apply_to_object(self, obj) -> None:
if not _is_eligible(obj):
return
from . import get_manager
manager = get_manager()
manager.save_original(obj)
ensure_category_property(obj)
category = obj.KindredCategory if obj.KindredCategory else DEFAULT_CATEGORY
palette = load_palette()
color = palette.get(category, palette[DEFAULT_CATEGORY])
obj.ViewObject.ShapeColor = color

View File

@@ -1,198 +0,0 @@
# ztools/appearance/manager.py
# Appearance mode manager and document observer
import FreeCAD as App
from .engineering import EngineeringMode, _is_eligible
from .realistic import RealisticMode
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/Kindred"
_PREF_KEY = "AppearanceMode"
_MODES = {
"realistic": RealisticMode,
"engineering": EngineeringMode,
}
_RECOLOR_DEBOUNCE_MS = 100
class _AppearanceObserver:
"""Document observer that auto-applies appearance mode to new/changed objects."""
def __init__(self, manager: "AppearanceManager"):
self._manager = manager
self._pending_recolor: set = set() # object names awaiting recolor
self._debounce_scheduled = False
def slotCreatedObject(self, obj) -> None:
if self._manager.active_mode.name != "engineering":
return
if not (hasattr(obj, "isDerivedFrom") and obj.isDerivedFrom("Part::Feature")):
return
# Defer to allow ViewObject to be fully initialized
try:
from PySide.QtCore import QTimer
QTimer.singleShot(0, lambda o=obj: self._apply_deferred(o))
except ImportError:
self._apply_deferred(obj)
def slotChangedObject(self, obj, prop: str) -> None:
if self._manager.active_mode.name != "engineering":
return
if not _is_eligible(obj):
return
if prop == "KindredCategory":
# Immediate recolor on category change
self._manager.active_mode.apply_to_object(obj)
elif prop == "Shape":
# Shape changed (recompute) — debounce to batch multiple updates
self._pending_recolor.add((obj.Document.Name, obj.Name))
self._schedule_flush()
def _schedule_flush(self) -> None:
if self._debounce_scheduled:
return
self._debounce_scheduled = True
try:
from PySide.QtCore import QTimer
QTimer.singleShot(_RECOLOR_DEBOUNCE_MS, self._flush_recolor)
except ImportError:
self._flush_recolor()
def _flush_recolor(self) -> None:
self._debounce_scheduled = False
pending = list(self._pending_recolor)
self._pending_recolor.clear()
for doc_name, obj_name in pending:
doc = App.getDocument(doc_name)
if doc is None:
continue
obj = doc.getObject(obj_name)
if obj is not None and _is_eligible(obj):
self._manager.active_mode.apply_to_object(obj)
def _apply_deferred(self, obj) -> None:
if _is_eligible(obj):
self._manager.active_mode.apply_to_object(obj)
class AppearanceManager:
"""Singleton manager coordinating appearance mode switching."""
def __init__(self):
self._active_mode = RealisticMode()
self._originals: dict = {} # {doc_name: {obj_name: (ShapeColor, Transparency)}}
self._observer = None
@property
def active_mode(self):
return self._active_mode
def set_mode(self, mode_name: str) -> None:
"""Switch appearance mode, applying to all open documents."""
if mode_name == self._active_mode.name:
return
mode_cls = _MODES.get(mode_name)
if mode_cls is None:
App.Console.error(f"ztools: unknown appearance mode '{mode_name}'\n")
return
# Reset current mode, then apply new one
for doc in App.listDocuments().values():
self._active_mode.reset(doc)
self._active_mode = mode_cls()
for doc in App.listDocuments().values():
self._active_mode.apply(doc)
# Persist preference
App.ParamGet(_PREF_GROUP).SetString(_PREF_KEY, mode_name)
# Update status bar
try:
from .ui import show_status_bar
show_status_bar(mode_name)
except Exception:
pass
App.Console.log(f"ztools: appearance mode set to '{mode_name}'\n")
def save_original(self, obj) -> None:
"""Snapshot ShapeColor and Transparency (save-once semantics)."""
doc_name = obj.Document.Name
obj_name = obj.Name
doc_cache = self._originals.setdefault(doc_name, {})
if obj_name in doc_cache:
return # Already saved
vo = obj.ViewObject
doc_cache[obj_name] = (
tuple(vo.ShapeColor),
vo.Transparency if hasattr(vo, "Transparency") else 0,
)
def restore_original(self, obj) -> None:
"""Restore saved ShapeColor and Transparency, removing cache entry."""
doc_name = obj.Document.Name
obj_name = obj.Name
doc_cache = self._originals.get(doc_name, {})
saved = doc_cache.pop(obj_name, None)
if saved is None:
return
shape_color, transparency = saved
vo = obj.ViewObject
vo.ShapeColor = shape_color
if hasattr(vo, "Transparency"):
vo.Transparency = transparency
def activate(self) -> None:
"""Start observer and apply persisted mode. Called from Workbench.Activated()."""
if self._observer is None:
self._observer = _AppearanceObserver(self)
App.addDocumentObserver(self._observer)
# Apply persisted mode
mode_name = App.ParamGet(_PREF_GROUP).GetString(_PREF_KEY, "realistic")
if mode_name != self._active_mode.name:
self._active_mode = _MODES.get(mode_name, RealisticMode)()
for doc in App.listDocuments().values():
self._active_mode.apply(doc)
# Show status bar
try:
from .ui import show_status_bar
show_status_bar(self._active_mode.name)
except Exception:
pass
def deactivate(self) -> None:
"""Stop observer and remove status bar. Called from Workbench.Deactivated()."""
if self._observer is not None:
App.removeDocumentObserver(self._observer)
self._observer = None
try:
from .ui import hide_status_bar
hide_status_bar()
except Exception:
pass
# Module-level singleton
_manager = None
def get_manager() -> AppearanceManager:
"""Get the global AppearanceManager singleton."""
global _manager
if _manager is None:
_manager = AppearanceManager()
return _manager

View File

@@ -1,22 +0,0 @@
# ztools/appearance/mode.py
# Abstract base class for appearance modes
from abc import ABC, abstractmethod
class AppearanceMode(ABC):
"""Base class for viewport appearance modes."""
name: str = ""
@abstractmethod
def apply(self, doc) -> None:
"""Apply this mode to all eligible objects in the document."""
@abstractmethod
def reset(self, doc) -> None:
"""Undo this mode's visual changes on all objects in the document."""
@abstractmethod
def apply_to_object(self, obj) -> None:
"""Apply this mode to a single object."""

View File

@@ -1,36 +0,0 @@
# ztools/appearance/palette.py
# Palette loading for appearance modes
import json
import os
from typing import Dict, Tuple
_PALETTES_DIR = os.path.join(os.path.dirname(__file__), "palettes")
_cache: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
def hex_to_rgb(hex_str: str) -> Tuple[float, float, float]:
"""Convert hex color string to RGB tuple with 0.0-1.0 floats."""
h = hex_str.lstrip("#")
return (int(h[0:2], 16) / 255.0, int(h[2:4], 16) / 255.0, int(h[4:6], 16) / 255.0)
def load_palette(
name: str = "catppuccin_mocha",
) -> Dict[str, Tuple[float, float, float]]:
"""Load a palette by name, returning {category: (r, g, b)} with 0.0-1.0 floats.
Palettes are JSON files in the palettes/ directory. Results are cached.
"""
if name in _cache:
return _cache[name]
path = os.path.join(_PALETTES_DIR, f"{name}.json")
with open(path, "r") as f:
data = json.load(f)
palette = {
cat: hex_to_rgb(hex_color) for cat, hex_color in data["categories"].items()
}
_cache[name] = palette
return palette

View File

@@ -1,13 +0,0 @@
{
"name": "Catppuccin Mocha",
"categories": {
"custom_body": "#fab387",
"fastener": "#a6e3a1",
"structural": "#89b4fa",
"electrical": "#f9e2af",
"seal_gasket": "#cba6f7",
"bearing_bushing": "#74c7ec",
"spring_compliant": "#f2cdcd",
"moving_part": "#f38ba8"
}
}

View File

@@ -1,35 +0,0 @@
# ztools/appearance/realistic.py
# Realistic mode — restores original material appearances
from .mode import AppearanceMode
def _is_eligible(obj) -> bool:
"""Check if an object should be processed by appearance modes."""
return (
hasattr(obj, "isDerivedFrom")
and obj.isDerivedFrom("Part::Feature")
and hasattr(obj, "ViewObject")
and obj.ViewObject is not None
and hasattr(obj.ViewObject, "ShapeColor")
)
class RealisticMode(AppearanceMode):
"""Material passthrough — restores original ShapeColor and Transparency."""
name = "realistic"
def apply(self, doc) -> None:
from . import get_manager
manager = get_manager()
for obj in doc.Objects:
if _is_eligible(obj):
manager.restore_original(obj)
def reset(self, doc) -> None:
pass # Nothing to undo — realistic is the baseline
def apply_to_object(self, obj) -> None:
pass # No-op — material passthrough

View File

@@ -1,72 +0,0 @@
# ztools/appearance/ui.py
# Status bar widget for appearance mode indicator
import FreeCADGui as Gui
from PySide import QtCore, QtGui
_MODE_LABELS = {
"realistic": "Realistic",
"engineering": "Engineering",
}
_MODE_COLORS = {
"realistic": "#7f849c", # overlay1
"engineering": "#fab387", # peach
}
class AppearanceStatusBar(QtGui.QLabel):
"""Status bar label showing the current appearance mode. Clickable to toggle."""
def __init__(self, parent=None):
super().__init__(parent)
self.setCursor(QtCore.Qt.PointingHandCursor)
self.setToolTip("Click to toggle appearance mode")
self.setStyleSheet("padding: 2px 6px;")
self._current_mode = "realistic"
self._update_display()
def set_mode(self, mode_name: str) -> None:
self._current_mode = mode_name
self._update_display()
def _update_display(self):
label = _MODE_LABELS.get(self._current_mode, self._current_mode)
color = _MODE_COLORS.get(self._current_mode, "#cdd6f4")
self.setText(f"<span style='color:{color};'>&#9679;</span> {label}")
def mousePressEvent(self, event):
from . import get_manager
manager = get_manager()
next_mode = "engineering" if self._current_mode == "realistic" else "realistic"
manager.set_mode(next_mode)
super().mousePressEvent(event)
_widget = None
def show_status_bar(mode_name: str) -> None:
"""Add or update the appearance mode status bar widget."""
global _widget
mw = Gui.getMainWindow()
if mw is None:
return
status_bar = mw.statusBar()
if _widget is None:
_widget = AppearanceStatusBar(status_bar)
status_bar.addPermanentWidget(_widget)
_widget.set_mode(mode_name)
_widget.show()
def hide_status_bar() -> None:
"""Remove the appearance mode status bar widget."""
global _widget
if _widget is not None:
mw = Gui.getMainWindow()
if mw is not None:
mw.statusBar().removeWidget(_widget)
_widget.deleteLater()
_widget = None

View File

@@ -1,6 +1,5 @@
# ztools/commands - GUI commands
from . import (
appearance_commands,
assembly_pattern_commands,
datum_commands,
datum_viewprovider,
@@ -10,7 +9,6 @@ from . import (
)
__all__ = [
"appearance_commands",
"datum_commands",
"datum_viewprovider",
"pattern_commands",

View File

@@ -1,127 +0,0 @@
# ztools/commands/appearance_commands.py
# Commands for switching appearance modes and setting part categories
import FreeCAD as App
import FreeCADGui as Gui
from ztools.appearance import get_manager
from ztools.appearance.engineering import KINDRED_CATEGORIES, ensure_category_property
from ztools.resources.icons import get_icon
class ZTools_AppearanceRealistic:
"""Switch to Realistic appearance mode (material passthrough)."""
def GetResources(self):
return {
"Pixmap": get_icon("appearance_realistic"),
"MenuText": "Realistic",
"ToolTip": "Show parts with their original material appearance",
}
def Activated(self):
get_manager().set_mode("realistic")
def IsActive(self):
return App.ActiveDocument is not None
class ZTools_AppearanceEngineering:
"""Switch to Engineering appearance mode (category-based coloring)."""
def GetResources(self):
return {
"Pixmap": get_icon("appearance_engineering"),
"MenuText": "Engineering",
"ToolTip": "Color parts by KindredCategory using engineering palette",
}
def Activated(self):
get_manager().set_mode("engineering")
def IsActive(self):
return App.ActiveDocument is not None
class ZTools_AppearanceMode:
"""Grouped dropdown for appearance mode switching."""
def GetCommands(self):
return ("ZTools_AppearanceRealistic", "ZTools_AppearanceEngineering")
def GetDefaultCommand(self):
return 0
def GetResources(self):
return {
"Pixmap": get_icon("appearance_engineering"),
"MenuText": "Appearance Mode",
"ToolTip": "Switch viewport appearance mode",
}
def IsActive(self):
return App.ActiveDocument is not None
class ZTools_SetCategory:
"""Set KindredCategory on selected parts via popup menu."""
def GetResources(self):
return {
"Pixmap": get_icon("set_category"),
"MenuText": "Set Category",
"ToolTip": "Set KindredCategory on selected parts",
}
def Activated(self):
from PySide import QtGui
sel = Gui.Selection.getSelection()
eligible = [
obj
for obj in sel
if hasattr(obj, "isDerivedFrom") and obj.isDerivedFrom("Part::Feature")
]
if not eligible:
return
# Build popup menu with category choices
menu = QtGui.QMenu()
labels = {
"custom_body": "Custom Body",
"fastener": "Fastener",
"structural": "Structural",
"electrical": "Electrical",
"seal_gasket": "Seal/Gasket",
"bearing_bushing": "Bearing/Bushing",
"spring_compliant": "Spring/Compliant",
"moving_part": "Moving Part",
}
for cat in KINDRED_CATEGORIES:
action = menu.addAction(labels.get(cat, cat))
action.setData(cat)
chosen = menu.exec_(QtGui.QCursor.pos())
if chosen is None:
return
category = chosen.data()
doc = eligible[0].Document
doc.openTransaction("Set KindredCategory")
try:
for obj in eligible:
ensure_category_property(obj)
obj.KindredCategory = category
doc.commitTransaction()
except Exception:
doc.abortTransaction()
raise
def IsActive(self):
return App.ActiveDocument is not None and bool(Gui.Selection.getSelection())
Gui.addCommand("ZTools_AppearanceRealistic", ZTools_AppearanceRealistic())
Gui.addCommand("ZTools_AppearanceEngineering", ZTools_AppearanceEngineering())
Gui.addCommand("ZTools_AppearanceMode", ZTools_AppearanceMode())
Gui.addCommand("ZTools_SetCategory", ZTools_SetCategory())

View File

@@ -2,6 +2,7 @@
# Custom ViewProvider for ZTools datum objects
import json
import math
import FreeCAD as App
import FreeCADGui as Gui
@@ -141,6 +142,23 @@ class ZToolsDatumViewProvider:
return None
def _resolve_source_refs(datum_obj):
"""Parse ZTools_SourceRefs and resolve to (object, subname, shape) tuples."""
refs_json = getattr(datum_obj, "ZTools_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
class DatumEditTaskPanel:
"""
Task panel for editing existing ZTools datum objects.
@@ -364,8 +382,45 @@ class DatumEditTaskPanel:
self.datum_obj.AttachmentOffset = new_offset
self._update_params({"distance": distance})
elif ztools_type in ("angled", "tangent_cylinder"):
self._update_params({"angle": self.angle_spin.value()})
elif ztools_type == "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._update_params({"angle": angle})
elif ztools_type == "tangent_cylinder":
angle = self.angle_spin.value()
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 = cyl.Axis
center = cyl.Center
radius = cyl.Radius
rad = math.radians(angle)
if abs(axis.dot(App.Vector(1, 0, 0))) < 0.99:
local_x = axis.cross(App.Vector(1, 0, 0)).normalize()
else:
local_x = axis.cross(App.Vector(0, 1, 0)).normalize()
local_y = axis.cross(local_x)
radial = local_x * math.cos(rad) + local_y * math.sin(rad)
tangent_point = center + radial * radius
rot = App.Rotation(App.Vector(0, 0, 1), radial)
self.datum_obj.Placement = App.Placement(tangent_point, rot)
self._update_params({"angle": angle})
elif ztools_type in ("normal_to_edge", "on_edge"):
parameter = self.param_spin.value()

View File

@@ -425,50 +425,6 @@ ICON_SPREADSHEET_QUICK_ALIAS_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" v
</svg>'''
# =============================================================================
# Appearance Mode Icons
# =============================================================================
# Realistic mode icon - eye with material sphere
ICON_APPEARANCE_REALISTIC_SVG = f'''<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="{MOCHA["surface0"]}"/>
<!-- Eye shape -->
<path d="M4 16 Q16 6 28 16 Q16 26 4 16 Z" fill="none" stroke="{MOCHA["text"]}" stroke-width="1.5"/>
<!-- Material sphere with gradient-like shading -->
<circle cx="16" cy="16" r="5" fill="{MOCHA["overlay1"]}"/>
<circle cx="14" cy="14" r="2" fill="{MOCHA["overlay2"]}" opacity="0.6"/>
</svg>'''
# Engineering mode icon - eye with colored segments
ICON_APPEARANCE_ENGINEERING_SVG = f'''<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="{MOCHA["surface0"]}"/>
<!-- Eye shape -->
<path d="M4 16 Q16 6 28 16 Q16 26 4 16 Z" fill="none" stroke="{MOCHA["text"]}" stroke-width="1.5"/>
<!-- Segmented iris showing category colors -->
<path d="M16 11 L19 14 L19 18 L16 21 L13 18 L13 14 Z" fill="{MOCHA["peach"]}"/>
<path d="M16 11 L19 14 L16 16 Z" fill="{MOCHA["green"]}"/>
<path d="M19 14 L19 18 L16 16 Z" fill="{MOCHA["blue"]}"/>
<path d="M19 18 L16 21 L16 16 Z" fill="{MOCHA["yellow"]}"/>
<path d="M16 21 L13 18 L16 16 Z" fill="{MOCHA["mauve"]}"/>
<path d="M13 18 L13 14 L16 16 Z" fill="{MOCHA["sapphire"]}"/>
<path d="M13 14 L16 11 L16 16 Z" fill="{MOCHA["red"]}"/>
<!-- Pupil -->
<circle cx="16" cy="16" r="2" fill="{MOCHA["crust"]}"/>
</svg>'''
# Set Category icon - tag with color swatches
ICON_SET_CATEGORY_SVG = f'''<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="{MOCHA["surface0"]}"/>
<!-- Tag shape -->
<path d="M5 8 L18 8 L25 16 L18 24 L5 24 Z" fill="{MOCHA["surface1"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1.5"/>
<circle cx="10" cy="16" r="2" fill="{MOCHA["surface0"]}"/>
<!-- Color dots representing categories -->
<circle cx="22" cy="8" r="3" fill="{MOCHA["peach"]}"/>
<circle cx="28" cy="8" r="3" fill="{MOCHA["green"]}"/>
<circle cx="22" cy="14" r="3" fill="{MOCHA["blue"]}"/>
<circle cx="28" cy="14" r="3" fill="{MOCHA["yellow"]}"/>
</svg>'''
# =============================================================================
# Icon Registry - Base64 encoded for FreeCAD
# =============================================================================
@@ -516,9 +472,6 @@ def get_icon(name: str) -> str:
"spreadsheet_bg_color": ICON_SPREADSHEET_BG_COLOR_SVG,
"spreadsheet_text_color": ICON_SPREADSHEET_TEXT_COLOR_SVG,
"spreadsheet_quick_alias": ICON_SPREADSHEET_QUICK_ALIAS_SVG,
"appearance_realistic": ICON_APPEARANCE_REALISTIC_SVG,
"appearance_engineering": ICON_APPEARANCE_ENGINEERING_SVG,
"set_category": ICON_SET_CATEGORY_SVG,
}
if name not in icons:
@@ -576,9 +529,6 @@ def save_icons_to_disk(directory: str):
"ztools_spreadsheet_bg_color": ICON_SPREADSHEET_BG_COLOR_SVG,
"ztools_spreadsheet_text_color": ICON_SPREADSHEET_TEXT_COLOR_SVG,
"ztools_spreadsheet_quick_alias": ICON_SPREADSHEET_QUICK_ALIAS_SVG,
"ztools_appearance_realistic": ICON_APPEARANCE_REALISTIC_SVG,
"ztools_appearance_engineering": ICON_APPEARANCE_ENGINEERING_SVG,
"ztools_set_category": ICON_SET_CATEGORY_SVG,
}
for name, svg in icons.items():