Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3298d1c6dc | ||
|
|
7b1e76c791 |
@@ -67,6 +67,7 @@ class ZToolsWorkbench(Gui.Workbench):
|
||||
pass
|
||||
|
||||
from ztools.commands import (
|
||||
appearance_commands,
|
||||
assembly_pattern_commands,
|
||||
datum_commands,
|
||||
pattern_commands,
|
||||
@@ -253,6 +254,18 @@ 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
|
||||
# =====================================================================
|
||||
@@ -275,6 +288,7 @@ 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
|
||||
@@ -297,13 +311,19 @@ 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_spreadsheet_tools
|
||||
+ self.ztools_appearance_menu_tools
|
||||
+ ["ZTools_SetCategory"],
|
||||
)
|
||||
|
||||
# Register the PartDesign manipulator now that commands exist.
|
||||
@@ -324,10 +344,23 @@ 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
|
||||
|
||||
def GetClassName(self):
|
||||
|
||||
4
ztools/ztools/appearance/__init__.py
Normal file
4
ztools/ztools/appearance/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# ztools/appearance — Appearance mode system
|
||||
from .manager import get_manager
|
||||
|
||||
__all__ = ["get_manager"]
|
||||
79
ztools/ztools/appearance/engineering.py
Normal file
79
ztools/ztools/appearance/engineering.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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
|
||||
198
ztools/ztools/appearance/manager.py
Normal file
198
ztools/ztools/appearance/manager.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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
|
||||
22
ztools/ztools/appearance/mode.py
Normal file
22
ztools/ztools/appearance/mode.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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."""
|
||||
36
ztools/ztools/appearance/palette.py
Normal file
36
ztools/ztools/appearance/palette.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
13
ztools/ztools/appearance/palettes/catppuccin_mocha.json
Normal file
13
ztools/ztools/appearance/palettes/catppuccin_mocha.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
35
ztools/ztools/appearance/realistic.py
Normal file
35
ztools/ztools/appearance/realistic.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
72
ztools/ztools/appearance/ui.py
Normal file
72
ztools/ztools/appearance/ui.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# 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};'>●</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
|
||||
@@ -1,5 +1,6 @@
|
||||
# ztools/commands - GUI commands
|
||||
from . import (
|
||||
appearance_commands,
|
||||
assembly_pattern_commands,
|
||||
datum_commands,
|
||||
datum_viewprovider,
|
||||
@@ -9,6 +10,7 @@ from . import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"appearance_commands",
|
||||
"datum_commands",
|
||||
"datum_viewprovider",
|
||||
"pattern_commands",
|
||||
|
||||
127
ztools/ztools/commands/appearance_commands.py
Normal file
127
ztools/ztools/commands/appearance_commands.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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())
|
||||
@@ -425,6 +425,50 @@ 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
|
||||
# =============================================================================
|
||||
@@ -472,6 +516,9 @@ 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:
|
||||
@@ -529,6 +576,9 @@ 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():
|
||||
|
||||
Reference in New Issue
Block a user