chore: archive QuickNav and ZTools into reference folder (#345)
All checks were successful
Build and Test / build (pull_request) Successful in 32m17s
Copy QuickNav and ZTools source trees into reference/ for developer reference during the UI/UX rework. These are plain directories (not submodules) and are not included in the build. - reference/quicknav/ — QuickNav addon source - reference/ztools/ — ZTools addon source Part of the UI/UX rework preparation. See #346.
1
reference/quicknav/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
3
reference/quicknav/Init.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import FreeCAD
|
||||
|
||||
FreeCAD.Console.PrintLog("quicknav addon loaded\n")
|
||||
37
reference/quicknav/InitGui.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
|
||||
|
||||
class QuickNavWorkbench(Gui.Workbench):
|
||||
"""Invisible workbench that installs QuickNav on load.
|
||||
|
||||
QuickNav does not replace the active workbench -- it layers on top.
|
||||
Loading QuickNav installs the event filter and nav bar, then
|
||||
immediately re-activates the previously active workbench.
|
||||
"""
|
||||
|
||||
MenuText = "QuickNav"
|
||||
ToolTip = "Keyboard-driven command navigation"
|
||||
|
||||
def Initialize(self):
|
||||
from quicknav.core import QuickNavManager
|
||||
|
||||
QuickNavManager.instance().install()
|
||||
App.Console.PrintMessage("QuickNav workbench initialized\n")
|
||||
|
||||
def Activated(self):
|
||||
from quicknav.core import QuickNavManager
|
||||
|
||||
QuickNavManager.instance().handle_workbench_activated()
|
||||
|
||||
def Deactivated(self):
|
||||
pass
|
||||
|
||||
def GetClassName(self):
|
||||
return "Gui::PythonWorkbench"
|
||||
|
||||
|
||||
Gui.addWorkbench(QuickNavWorkbench())
|
||||
|
||||
# Eager command registration
|
||||
from quicknav.commands import QuickNav_Toggle # noqa: F401
|
||||
25
reference/quicknav/package.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
<name>QuickNav</name>
|
||||
<description>Keyboard-driven command navigation</description>
|
||||
<version>0.1.0</version>
|
||||
<maintainer email="dev@kindred-systems.com">Kindred Systems</maintainer>
|
||||
<license>LGPL-2.1</license>
|
||||
<url type="repository">https://git.kindred-systems.com/kindred/quicknav</url>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<classname>QuickNavWorkbench</classname>
|
||||
<subdirectory>./</subdirectory>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
<kindred>
|
||||
<min_create_version>0.1.0</min_create_version>
|
||||
<load_priority>20</load_priority>
|
||||
<pure_python>true</pure_python>
|
||||
<dependencies>
|
||||
<dependency>sdk</dependency>
|
||||
</dependencies>
|
||||
</kindred>
|
||||
</package>
|
||||
1
reference/quicknav/quicknav/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""QuickNav -- keyboard-driven command navigation for FreeCAD and Kindred Create."""
|
||||
24
reference/quicknav/quicknav/commands.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""FreeCAD command registrations for QuickNav."""
|
||||
|
||||
import FreeCADGui as Gui
|
||||
|
||||
|
||||
class QuickNav_Toggle:
|
||||
"""Toggle QuickNav keyboard navigation on/off."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Toggle QuickNav",
|
||||
"ToolTip": "Toggle keyboard-driven command navigation on/off",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return True
|
||||
|
||||
def Activated(self):
|
||||
from quicknav.core import QuickNavManager
|
||||
|
||||
QuickNavManager.instance().toggle_active()
|
||||
|
||||
|
||||
Gui.addCommand("QuickNav_Toggle", QuickNav_Toggle())
|
||||
208
reference/quicknav/quicknav/core.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""QuickNav manager singleton.
|
||||
|
||||
Orchestrates the event filter, navigation bar, and workbench/grouping
|
||||
state. Created once when the QuickNavWorkbench is first activated.
|
||||
"""
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
|
||||
from quicknav.workbench_map import (
|
||||
get_command,
|
||||
get_grouping,
|
||||
get_groupings,
|
||||
get_workbench_slot,
|
||||
)
|
||||
|
||||
_PREF_PATH = "User parameter:BaseApp/Preferences/Mod/QuickNav"
|
||||
|
||||
|
||||
class QuickNavManager:
|
||||
"""Singleton managing QuickNav lifecycle and state."""
|
||||
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self._active = False
|
||||
self._installed = False
|
||||
self._event_filter = None
|
||||
self._nav_bar = None
|
||||
self._current_workbench_slot = 2 # PartDesign default
|
||||
self._current_grouping_idx = 0
|
||||
self.previous_workbench = None
|
||||
self._reactivating = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def install(self):
|
||||
"""Install the event filter and navigation bar."""
|
||||
if self._installed:
|
||||
return
|
||||
|
||||
mw = Gui.getMainWindow()
|
||||
if mw is None:
|
||||
App.Console.PrintWarning("QuickNav: main window not available\n")
|
||||
return
|
||||
|
||||
# Capture which workbench was active before QuickNav loaded.
|
||||
try:
|
||||
wb = Gui.activeWorkbench()
|
||||
if wb:
|
||||
self.previous_workbench = wb.__class__.__name__
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Event filter
|
||||
from quicknav.event_filter import QuickNavEventFilter
|
||||
|
||||
self._event_filter = QuickNavEventFilter(manager=self, parent=mw)
|
||||
mw.installEventFilter(self._event_filter)
|
||||
|
||||
# Navigation bar
|
||||
from quicknav.nav_bar import QuickNavBar
|
||||
|
||||
self._nav_bar = QuickNavBar(manager=self, parent=mw)
|
||||
mw.addToolBar(self._nav_bar)
|
||||
mw.insertToolBarBreak(self._nav_bar)
|
||||
|
||||
# Read saved preference
|
||||
self._active = self._load_preference()
|
||||
if not self._active:
|
||||
self._nav_bar.hide()
|
||||
|
||||
self._installed = True
|
||||
self._update_nav_bar()
|
||||
App.Console.PrintLog("QuickNav: installed\n")
|
||||
|
||||
def uninstall(self):
|
||||
"""Remove the event filter and navigation bar."""
|
||||
if not self._installed:
|
||||
return
|
||||
|
||||
mw = Gui.getMainWindow()
|
||||
if mw and self._event_filter:
|
||||
mw.removeEventFilter(self._event_filter)
|
||||
self._event_filter = None
|
||||
|
||||
if self._nav_bar:
|
||||
self._nav_bar.hide()
|
||||
self._nav_bar.setParent(None)
|
||||
self._nav_bar.deleteLater()
|
||||
self._nav_bar = None
|
||||
|
||||
self._installed = False
|
||||
App.Console.PrintLog("QuickNav: uninstalled\n")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# State queries
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def is_active(self):
|
||||
return self._active
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def toggle_active(self):
|
||||
"""Toggle QuickNav on/off."""
|
||||
self._active = not self._active
|
||||
self._save_preference()
|
||||
if self._nav_bar:
|
||||
if self._active:
|
||||
self._nav_bar.show()
|
||||
self._update_nav_bar()
|
||||
else:
|
||||
self._nav_bar.hide()
|
||||
state = "on" if self._active else "off"
|
||||
App.Console.PrintMessage(f"QuickNav: {state}\n")
|
||||
|
||||
def switch_workbench(self, n):
|
||||
"""Switch to the workbench assigned to Ctrl+N."""
|
||||
slot = get_workbench_slot(n)
|
||||
if slot is None:
|
||||
return
|
||||
self._current_workbench_slot = n
|
||||
self._current_grouping_idx = 0
|
||||
|
||||
# Activate the target workbench in FreeCAD.
|
||||
try:
|
||||
Gui.activateWorkbench(slot["class_name"])
|
||||
except Exception as e:
|
||||
App.Console.PrintWarning(f"QuickNav: could not activate {slot['class_name']}: {e}\n")
|
||||
|
||||
self._update_nav_bar()
|
||||
|
||||
def switch_grouping(self, n):
|
||||
"""Switch to the Nth grouping (1-based) in the current workbench."""
|
||||
slot = get_workbench_slot(self._current_workbench_slot)
|
||||
if slot is None:
|
||||
return
|
||||
groupings = get_groupings(slot["key"])
|
||||
idx = n - 1
|
||||
if 0 <= idx < len(groupings):
|
||||
self._current_grouping_idx = idx
|
||||
self._update_nav_bar()
|
||||
|
||||
def execute_command(self, n):
|
||||
"""Execute the Nth command (1-based) in the active grouping."""
|
||||
slot = get_workbench_slot(self._current_workbench_slot)
|
||||
if slot is None:
|
||||
return
|
||||
cmd_id = get_command(slot["key"], self._current_grouping_idx, n)
|
||||
if cmd_id:
|
||||
try:
|
||||
Gui.runCommand(cmd_id)
|
||||
except Exception as e:
|
||||
App.Console.PrintWarning(f"QuickNav: command {cmd_id} failed: {e}\n")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Workbench re-activation guard
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def handle_workbench_activated(self):
|
||||
"""Called from QuickNavWorkbench.Activated() to restore the
|
||||
previous workbench without infinite recursion."""
|
||||
if self._reactivating:
|
||||
return
|
||||
if self.previous_workbench:
|
||||
self._reactivating = True
|
||||
try:
|
||||
Gui.activateWorkbench(self.previous_workbench)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._reactivating = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _update_nav_bar(self):
|
||||
if not self._nav_bar:
|
||||
return
|
||||
slot = get_workbench_slot(self._current_workbench_slot)
|
||||
if slot is None:
|
||||
return
|
||||
groupings = get_groupings(slot["key"])
|
||||
grouping = get_grouping(slot["key"], self._current_grouping_idx)
|
||||
commands = grouping["commands"] if grouping else []
|
||||
self._nav_bar.update_display(
|
||||
slot["display"], groupings, self._current_grouping_idx, commands
|
||||
)
|
||||
|
||||
def _load_preference(self):
|
||||
param = App.ParamGet(_PREF_PATH)
|
||||
return param.GetBool("Enabled", True)
|
||||
|
||||
def _save_preference(self):
|
||||
param = App.ParamGet(_PREF_PATH)
|
||||
param.SetBool("Enabled", self._active)
|
||||
84
reference/quicknav/quicknav/event_filter.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Key event filter for QuickNav.
|
||||
|
||||
Installed on FreeCAD's main window to intercept number key presses
|
||||
when QuickNav is active. Passes through all events when inactive
|
||||
(except Key_0 for re-enable toggle).
|
||||
"""
|
||||
|
||||
from PySide.QtCore import QEvent, QObject, Qt
|
||||
from PySide.QtWidgets import (
|
||||
QAbstractSpinBox,
|
||||
QApplication,
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QTextEdit,
|
||||
)
|
||||
|
||||
# Widget types that accept text input. QAbstractSpinBox covers
|
||||
# QSpinBox, QDoubleSpinBox, and FreeCAD's custom QuantitySpinBox
|
||||
# (used for Sketcher inline dimension entry).
|
||||
_TEXT_INPUT_TYPES = (QLineEdit, QTextEdit, QPlainTextEdit, QAbstractSpinBox)
|
||||
|
||||
|
||||
class QuickNavEventFilter(QObject):
|
||||
"""Intercepts key presses for QuickNav dispatch."""
|
||||
|
||||
def __init__(self, manager, parent=None):
|
||||
super().__init__(parent)
|
||||
self._manager = manager
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() != QEvent.KeyPress:
|
||||
return False
|
||||
|
||||
key = event.key()
|
||||
modifiers = event.modifiers() & ~Qt.KeypadModifier
|
||||
|
||||
# Key_0 with no modifiers always toggles, even when inactive.
|
||||
if key == Qt.Key_0 and modifiers == Qt.NoModifier:
|
||||
if not self._focus_is_text_input():
|
||||
self._manager.toggle_active()
|
||||
return True
|
||||
return False
|
||||
|
||||
if not self._manager.is_active():
|
||||
return False
|
||||
|
||||
# Don't intercept when a text input has focus.
|
||||
if self._focus_is_text_input():
|
||||
return False
|
||||
|
||||
if Qt.Key_1 <= key <= Qt.Key_9:
|
||||
n = key - Qt.Key_0
|
||||
if modifiers == Qt.ControlModifier:
|
||||
self._manager.switch_workbench(n)
|
||||
return True
|
||||
elif modifiers == Qt.ShiftModifier:
|
||||
self._manager.switch_grouping(n)
|
||||
return True
|
||||
elif modifiers == Qt.NoModifier:
|
||||
self._manager.execute_command(n)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _focus_is_text_input(self):
|
||||
"""Return True if the focused widget is a text/number input."""
|
||||
focused = QApplication.focusWidget()
|
||||
if focused is None:
|
||||
return False
|
||||
if isinstance(focused, _TEXT_INPUT_TYPES):
|
||||
return True
|
||||
return self._is_in_task_panel(focused)
|
||||
|
||||
@staticmethod
|
||||
def _is_in_task_panel(widget):
|
||||
"""Return True if *widget* is inside FreeCAD's task panel."""
|
||||
parent = widget.parent() if widget else None
|
||||
while parent is not None:
|
||||
name = parent.objectName() or ""
|
||||
cls = parent.__class__.__name__
|
||||
if "TaskView" in name or "TaskView" in cls:
|
||||
return True
|
||||
parent = parent.parent() if hasattr(parent, "parent") else None
|
||||
return False
|
||||
142
reference/quicknav/quicknav/nav_bar.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Navigation bar widget for QuickNav.
|
||||
|
||||
A QToolBar that displays the current workbench, command groupings,
|
||||
and numbered commands. Positioned at the bottom of the toolbar area.
|
||||
"""
|
||||
|
||||
from PySide.QtCore import Signal
|
||||
from PySide.QtWidgets import QHBoxLayout, QLabel, QToolBar, QWidget
|
||||
|
||||
|
||||
class _ClickableLabel(QLabel):
|
||||
"""QLabel that emits *clicked* on mouse press."""
|
||||
|
||||
clicked = Signal()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.clicked.emit()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
class QuickNavBar(QToolBar):
|
||||
"""Bottom toolbar showing QuickNav state."""
|
||||
|
||||
_MAX_SLOTS = 9
|
||||
|
||||
def __init__(self, manager, parent=None):
|
||||
super().__init__("QuickNav", parent)
|
||||
self._manager = manager
|
||||
self.setObjectName("QuickNavBar")
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(4, 2, 4, 2)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Workbench label
|
||||
self._wb_label = QLabel()
|
||||
self._wb_label.setContentsMargins(4, 0, 8, 0)
|
||||
layout.addWidget(self._wb_label)
|
||||
|
||||
# Vertical separator
|
||||
sep1 = QLabel("|")
|
||||
sep1.setContentsMargins(4, 0, 4, 0)
|
||||
layout.addWidget(sep1)
|
||||
|
||||
# Grouping labels
|
||||
self._grouping_labels = []
|
||||
for i in range(self._MAX_SLOTS):
|
||||
label = _ClickableLabel()
|
||||
label.setContentsMargins(4, 0, 4, 0)
|
||||
label.clicked.connect(lambda n=i + 1: self._manager.switch_grouping(n))
|
||||
self._grouping_labels.append(label)
|
||||
layout.addWidget(label)
|
||||
|
||||
# Vertical separator
|
||||
self._cmd_sep = QLabel("|")
|
||||
self._cmd_sep.setContentsMargins(4, 0, 4, 0)
|
||||
layout.addWidget(self._cmd_sep)
|
||||
|
||||
# Command labels
|
||||
self._command_labels = []
|
||||
for i in range(self._MAX_SLOTS):
|
||||
label = _ClickableLabel()
|
||||
label.setContentsMargins(4, 0, 4, 0)
|
||||
label.clicked.connect(lambda n=i + 1: self._manager.execute_command(n))
|
||||
self._command_labels.append(label)
|
||||
layout.addWidget(label)
|
||||
|
||||
layout.addStretch()
|
||||
self.addWidget(container)
|
||||
|
||||
self._apply_theme()
|
||||
|
||||
def update_display(self, wb_name, groupings, active_grouping_idx, commands):
|
||||
"""Refresh the bar with current state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wb_name : str
|
||||
Workbench display name.
|
||||
groupings : list[dict]
|
||||
Grouping dicts with ``"name"`` keys.
|
||||
active_grouping_idx : int
|
||||
0-based index of the active grouping.
|
||||
commands : list[tuple[str, str]]
|
||||
``(command_id, display_name)`` for the active grouping.
|
||||
"""
|
||||
self._wb_label.setText(f"WB: {wb_name}")
|
||||
|
||||
for i, label in enumerate(self._grouping_labels):
|
||||
if i < len(groupings):
|
||||
prefix = f"\u276{i + 1}" if i == active_grouping_idx else f"{i + 1}"
|
||||
label.setText(f" {prefix}: {groupings[i]['name']} ")
|
||||
if i == active_grouping_idx:
|
||||
label.setStyleSheet(self._active_grouping_style)
|
||||
else:
|
||||
label.setStyleSheet(self._inactive_grouping_style)
|
||||
label.show()
|
||||
else:
|
||||
label.hide()
|
||||
|
||||
for i, label in enumerate(self._command_labels):
|
||||
if i < len(commands):
|
||||
label.setText(f" {i + 1}: {commands[i][1]} ")
|
||||
label.setStyleSheet(self._command_style)
|
||||
label.show()
|
||||
else:
|
||||
label.hide()
|
||||
|
||||
def _apply_theme(self):
|
||||
"""Apply Catppuccin Mocha theme if SDK is available, else Qt defaults."""
|
||||
palette = None
|
||||
try:
|
||||
from kindred_sdk import load_palette
|
||||
|
||||
palette = load_palette()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if palette:
|
||||
bg = palette.get("background.toolbar") or "#181825"
|
||||
fg = palette.get("foreground") or "#cdd6f4"
|
||||
accent = palette.get("accent.info") or "#89b4fa"
|
||||
border = palette.get("border") or "#45475a"
|
||||
muted = palette.get("foreground.muted") or "#a6adc8"
|
||||
else:
|
||||
# No theme — leave styles empty so Qt defaults apply.
|
||||
self._active_grouping_style = "font-weight: bold;"
|
||||
self._inactive_grouping_style = ""
|
||||
self._command_style = ""
|
||||
self._wb_label.setStyleSheet("font-weight: bold;")
|
||||
return
|
||||
|
||||
self.setStyleSheet(
|
||||
f"QToolBar#QuickNavBar {{ background: {bg}; border-top: 1px solid {border}; }}"
|
||||
)
|
||||
self._wb_label.setStyleSheet(f"color: {accent}; font-weight: bold;")
|
||||
self._active_grouping_style = f"color: {accent}; font-weight: bold;"
|
||||
self._inactive_grouping_style = f"color: {muted};"
|
||||
self._command_style = f"color: {fg};"
|
||||
285
reference/quicknav/quicknav/workbench_map.py
Normal file
@@ -0,0 +1,285 @@
|
||||
"""Static workbench slot assignments and command groupings.
|
||||
|
||||
Phase 1 uses hardcoded data from the QuickNav spec (SPEC.md section 5).
|
||||
Phase 2 replaces this with dynamic toolbar introspection.
|
||||
"""
|
||||
|
||||
# Fixed Ctrl+N workbench assignments
|
||||
WORKBENCH_SLOTS = {
|
||||
1: {
|
||||
"key": "sketcher",
|
||||
"class_name": "SketcherWorkbench",
|
||||
"display": "Sketcher",
|
||||
},
|
||||
2: {
|
||||
"key": "partdesign",
|
||||
"class_name": "PartDesignWorkbench",
|
||||
"display": "Part Design",
|
||||
},
|
||||
3: {
|
||||
"key": "assembly",
|
||||
"class_name": "AssemblyWorkbench",
|
||||
"display": "Assembly",
|
||||
},
|
||||
4: {
|
||||
"key": "spreadsheet",
|
||||
"class_name": "SpreadsheetWorkbench",
|
||||
"display": "Spreadsheet",
|
||||
},
|
||||
5: {
|
||||
"key": "techdraw",
|
||||
"class_name": "TechDrawWorkbench",
|
||||
"display": "TechDraw",
|
||||
},
|
||||
}
|
||||
|
||||
# Command groupings per workbench. Each grouping has a name and up to
|
||||
# 9 commands as (FreeCAD_command_id, display_name) tuples.
|
||||
WORKBENCH_GROUPINGS = {
|
||||
"sketcher": [
|
||||
{
|
||||
"name": "Primitives",
|
||||
"commands": [
|
||||
("Sketcher_CreateLine", "Line"),
|
||||
("Sketcher_CreateRectangle", "Rectangle"),
|
||||
("Sketcher_CreateCircle", "Circle"),
|
||||
("Sketcher_CreateArc", "Arc"),
|
||||
("Sketcher_CreatePoint", "Point"),
|
||||
("Sketcher_CreateSlot", "Slot"),
|
||||
("Sketcher_CreateBSpline", "B-Spline"),
|
||||
("Sketcher_CreatePolyline", "Polyline"),
|
||||
("Sketcher_CreateEllipseByCenter", "Ellipse"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Constraints",
|
||||
"commands": [
|
||||
("Sketcher_ConstrainCoincidentUnified", "Coincident"),
|
||||
("Sketcher_ConstrainHorizontal", "Horizontal"),
|
||||
("Sketcher_ConstrainVertical", "Vertical"),
|
||||
("Sketcher_ConstrainParallel", "Parallel"),
|
||||
("Sketcher_ConstrainPerpendicular", "Perpendicular"),
|
||||
("Sketcher_ConstrainTangent", "Tangent"),
|
||||
("Sketcher_ConstrainEqual", "Equal"),
|
||||
("Sketcher_ConstrainSymmetric", "Symmetric"),
|
||||
("Sketcher_ConstrainBlock", "Block"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Dimensions",
|
||||
"commands": [
|
||||
("Sketcher_ConstrainDistance", "Distance"),
|
||||
("Sketcher_ConstrainDistanceX", "Horiz. Distance"),
|
||||
("Sketcher_ConstrainDistanceY", "Vert. Distance"),
|
||||
("Sketcher_ConstrainRadius", "Radius"),
|
||||
("Sketcher_ConstrainDiameter", "Diameter"),
|
||||
("Sketcher_ConstrainAngle", "Angle"),
|
||||
("Sketcher_ConstrainLock", "Lock"),
|
||||
("Sketcher_ConstrainSnellsLaw", "Refraction"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Construction",
|
||||
"commands": [
|
||||
("Sketcher_ToggleConstruction", "Toggle Constr."),
|
||||
("Sketcher_External", "External Geom."),
|
||||
("Sketcher_CarbonCopy", "Carbon Copy"),
|
||||
("Sketcher_Offset", "Offset"),
|
||||
("Sketcher_Trimming", "Trim"),
|
||||
("Sketcher_Extend", "Extend"),
|
||||
("Sketcher_Split", "Split"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Tools",
|
||||
"commands": [
|
||||
("Sketcher_Symmetry", "Mirror"),
|
||||
("Sketcher_RectangularArray", "Linear Array"),
|
||||
("Sketcher_Move", "Move"),
|
||||
("Sketcher_Rotate", "Rotate"),
|
||||
("Sketcher_Scale", "Scale"),
|
||||
("Sketcher_CloseShape", "Close Shape"),
|
||||
("Sketcher_ConnectLines", "Connect Edges"),
|
||||
],
|
||||
},
|
||||
],
|
||||
"partdesign": [
|
||||
{
|
||||
"name": "Additive",
|
||||
"commands": [
|
||||
("PartDesign_Pad", "Pad"),
|
||||
("PartDesign_Revolution", "Revolution"),
|
||||
("PartDesign_AdditiveLoft", "Add. Loft"),
|
||||
("PartDesign_AdditivePipe", "Add. Pipe"),
|
||||
("PartDesign_AdditiveHelix", "Add. Helix"),
|
||||
("PartDesign_CompPrimitiveAdditive", "Add. Primitive"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Subtractive",
|
||||
"commands": [
|
||||
("PartDesign_Pocket", "Pocket"),
|
||||
("PartDesign_Hole", "Hole"),
|
||||
("PartDesign_Groove", "Groove"),
|
||||
("PartDesign_SubtractiveLoft", "Sub. Loft"),
|
||||
("PartDesign_SubtractivePipe", "Sub. Pipe"),
|
||||
("PartDesign_SubtractiveHelix", "Sub. Helix"),
|
||||
("PartDesign_CompPrimitiveSubtractive", "Sub. Primitive"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Datums",
|
||||
"commands": [
|
||||
("PartDesign_NewSketch", "New Sketch"),
|
||||
("PartDesign_Plane", "Datum Plane"),
|
||||
("PartDesign_Line", "Datum Line"),
|
||||
("PartDesign_Point", "Datum Point"),
|
||||
("PartDesign_ShapeBinder", "Shape Binder"),
|
||||
("PartDesign_SubShapeBinder", "Sub-Shape Binder"),
|
||||
("ZTools_DatumCreator", "ZT Datum Creator"),
|
||||
("ZTools_DatumManager", "ZT Datum Manager"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Transformations",
|
||||
"commands": [
|
||||
("PartDesign_Mirrored", "Mirrored"),
|
||||
("PartDesign_LinearPattern", "Linear Pattern"),
|
||||
("PartDesign_PolarPattern", "Polar Pattern"),
|
||||
("PartDesign_MultiTransform", "MultiTransform"),
|
||||
("ZTools_RotatedLinearPattern", "ZT Rot. Linear"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Modeling",
|
||||
"commands": [
|
||||
("PartDesign_Fillet", "Fillet"),
|
||||
("PartDesign_Chamfer", "Chamfer"),
|
||||
("PartDesign_Draft", "Draft"),
|
||||
("PartDesign_Thickness", "Thickness"),
|
||||
("PartDesign_Boolean", "Boolean"),
|
||||
("ZTools_EnhancedPocket", "ZT Enh. Pocket"),
|
||||
],
|
||||
},
|
||||
],
|
||||
"assembly": [
|
||||
{
|
||||
"name": "Components",
|
||||
"commands": [
|
||||
("Assembly_InsertLink", "Insert Component"),
|
||||
("Assembly_InsertNewPart", "Create Part"),
|
||||
("Assembly_CreateAssembly", "Create Assembly"),
|
||||
("Assembly_ToggleGrounded", "Ground"),
|
||||
("Assembly_CreateBom", "BOM"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Joints",
|
||||
"commands": [
|
||||
("Assembly_CreateJointFixed", "Fixed"),
|
||||
("Assembly_CreateJointRevolute", "Revolute"),
|
||||
("Assembly_CreateJointCylindrical", "Cylindrical"),
|
||||
("Assembly_CreateJointSlider", "Slider"),
|
||||
("Assembly_CreateJointBall", "Ball"),
|
||||
("Assembly_CreateJointDistance", "Distance"),
|
||||
("Assembly_CreateJointAngle", "Angle"),
|
||||
("Assembly_CreateJointParallel", "Parallel"),
|
||||
("Assembly_CreateJointPerpendicular", "Perpendicular"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Patterns",
|
||||
"commands": [
|
||||
("ZTools_AssemblyLinearPattern", "Linear Pattern"),
|
||||
("ZTools_AssemblyPolarPattern", "Polar Pattern"),
|
||||
],
|
||||
},
|
||||
],
|
||||
"spreadsheet": [
|
||||
{
|
||||
"name": "Editing",
|
||||
"commands": [
|
||||
("Spreadsheet_MergeCells", "Merge Cells"),
|
||||
("Spreadsheet_SplitCell", "Split Cell"),
|
||||
("Spreadsheet_SetAlias", "Alias"),
|
||||
("Spreadsheet_Import", "Import CSV"),
|
||||
("Spreadsheet_Export", "Export CSV"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Formatting",
|
||||
"commands": [
|
||||
("ZTools_SpreadsheetStyleBold", "Bold"),
|
||||
("ZTools_SpreadsheetStyleItalic", "Italic"),
|
||||
("ZTools_SpreadsheetStyleUnderline", "Underline"),
|
||||
("ZTools_SpreadsheetAlignLeft", "Align Left"),
|
||||
("ZTools_SpreadsheetAlignCenter", "Align Center"),
|
||||
("ZTools_SpreadsheetAlignRight", "Align Right"),
|
||||
("ZTools_SpreadsheetBgColor", "BG Color"),
|
||||
("ZTools_SpreadsheetTextColor", "Text Color"),
|
||||
("ZTools_SpreadsheetQuickAlias", "Quick Alias"),
|
||||
],
|
||||
},
|
||||
],
|
||||
"techdraw": [
|
||||
{
|
||||
"name": "Views",
|
||||
"commands": [
|
||||
("TechDraw_PageDefault", "New Page"),
|
||||
("TechDraw_View", "Insert View"),
|
||||
("TechDraw_ProjectionGroup", "Projection Group"),
|
||||
("TechDraw_SectionView", "Section View"),
|
||||
("TechDraw_DetailView", "Detail View"),
|
||||
("TechDraw_ActiveView", "Active View"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Dimensions",
|
||||
"commands": [
|
||||
("TechDraw_LengthDimension", "Length"),
|
||||
("TechDraw_HorizontalDimension", "Horizontal"),
|
||||
("TechDraw_VerticalDimension", "Vertical"),
|
||||
("TechDraw_RadiusDimension", "Radius"),
|
||||
("TechDraw_DiameterDimension", "Diameter"),
|
||||
("TechDraw_AngleDimension", "Angle"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Annotations",
|
||||
"commands": [
|
||||
("TechDraw_Annotation", "Annotation"),
|
||||
("TechDraw_Balloon", "Balloon"),
|
||||
("TechDraw_LeaderLine", "Leader Line"),
|
||||
("TechDraw_CosmeticVertex", "Cosmetic Vertex"),
|
||||
("TechDraw_Midpoints", "Midpoints"),
|
||||
("TechDraw_CenterLine", "Center Line"),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_workbench_slot(n):
|
||||
"""Return workbench info dict for Ctrl+N, or None if unassigned."""
|
||||
return WORKBENCH_SLOTS.get(n)
|
||||
|
||||
|
||||
def get_groupings(workbench_key):
|
||||
"""Return the grouping list for a workbench key."""
|
||||
return WORKBENCH_GROUPINGS.get(workbench_key, [])
|
||||
|
||||
|
||||
def get_grouping(workbench_key, grouping_idx):
|
||||
"""Return a specific grouping dict by 0-based index, or None."""
|
||||
groupings = get_groupings(workbench_key)
|
||||
if 0 <= grouping_idx < len(groupings):
|
||||
return groupings[grouping_idx]
|
||||
return None
|
||||
|
||||
|
||||
def get_command(workbench_key, grouping_idx, n):
|
||||
"""Return the FreeCAD command ID for the Nth command (1-based), or None."""
|
||||
grouping = get_grouping(workbench_key, grouping_idx)
|
||||
if grouping and 1 <= n <= len(grouping["commands"]):
|
||||
return grouping["commands"][n - 1][0]
|
||||
return None
|
||||
385
reference/ztools/KINDRED_INTEGRATION.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# Kindred Create Integration Guide
|
||||
|
||||
This document outlines the requirements, options, and steps for integrating the ztools workbench into Kindred Create as a native workbench.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Current Architecture Overview](#current-architecture-overview)
|
||||
2. [Integration Options](#integration-options)
|
||||
3. [Files Requiring Modification](#files-requiring-modification)
|
||||
4. [Branding Changes](#branding-changes)
|
||||
5. [FreeCAD API Dependencies](#freecad-api-dependencies)
|
||||
6. [Recommended Integration Path](#recommended-integration-path)
|
||||
7. [Testing Checklist](#testing-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Overview
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ztools-0065/
|
||||
├── package.xml # FreeCAD addon metadata
|
||||
├── CatppuccinMocha/ # Preference pack (theme)
|
||||
└── ztools/ # Main addon directory
|
||||
├── Init.py # Pre-GUI initialization
|
||||
├── InitGui.py # Workbench class definition
|
||||
└── ztools/ # Python package
|
||||
├── __init__.py
|
||||
├── commands/ # GUI commands
|
||||
│ ├── __init__.py
|
||||
│ ├── datum_commands.py
|
||||
│ ├── datum_viewprovider.py
|
||||
│ ├── pattern_commands.py
|
||||
│ ├── pocket_commands.py
|
||||
│ ├── assembly_pattern_commands.py
|
||||
│ └── spreadsheet_commands.py
|
||||
├── datums/ # Datum creation logic
|
||||
│ ├── __init__.py
|
||||
│ └── core.py
|
||||
└── resources/ # Icons and theming
|
||||
├── __init__.py
|
||||
├── icons.py
|
||||
└── theme.py
|
||||
```
|
||||
|
||||
### Module Statistics
|
||||
|
||||
| Component | Files | Commands | Description |
|
||||
|-----------|-------|----------|-------------|
|
||||
| Datum Tools | 3 | 2 | 15 attachment modes, DatumCreator + DatumManager |
|
||||
| Pattern Tools | 1 | 1 | Rotated linear pattern |
|
||||
| Pocket Tools | 1 | 1 | Enhanced pocket with face selection |
|
||||
| Assembly Tools | 1 | 2 | Linear and polar assembly patterns |
|
||||
| Spreadsheet Tools | 1 | 9 | Formatting and quick alias |
|
||||
| Resources | 2 | - | 33 SVG icons, theme colors |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- **Python Standard Library Only** - No pip dependencies
|
||||
- **FreeCAD Modules**: FreeCAD, FreeCADGui, Part, PartDesign, Sketcher, Assembly (1.0+), Spreadsheet
|
||||
- **Qt/PySide**: QtCore, QtGui, QtWidgets (via FreeCAD's bundled PySide)
|
||||
|
||||
---
|
||||
|
||||
## Integration Options
|
||||
|
||||
### Option A: Addon-Style Integration (Minimal Changes)
|
||||
|
||||
**Effort**: Low
|
||||
**Compatibility**: Maintains upstream FreeCAD compatibility
|
||||
|
||||
Bundle ztools as a pre-installed addon in Kindred Create's addon directory.
|
||||
|
||||
**Pros**:
|
||||
- Minimal code changes
|
||||
- Can still be distributed as standalone addon
|
||||
- Easy to update independently
|
||||
|
||||
**Cons**:
|
||||
- Not truly "native" - still appears as addon
|
||||
- Users could accidentally uninstall
|
||||
|
||||
**Changes Required**:
|
||||
- Update `package.xml` metadata with Kindred Create branding
|
||||
- Optionally rename workbench display name
|
||||
|
||||
### Option B: Native Workbench Integration (Moderate Changes)
|
||||
|
||||
**Effort**: Medium
|
||||
**Compatibility**: Kindred Create specific
|
||||
|
||||
Move ztools into FreeCAD's `Mod/` directory as a first-party workbench.
|
||||
|
||||
**Pros**:
|
||||
- Appears alongside PartDesign, Assembly, etc.
|
||||
- Cannot be uninstalled via Addon Manager
|
||||
- Full integration with Kindred Create identity
|
||||
|
||||
**Cons**:
|
||||
- Requires maintaining fork-specific code
|
||||
- Branding changes throughout codebase
|
||||
|
||||
**Changes Required**:
|
||||
- Rename all `ZTools_*` commands to `KindredCreate_*` or similar
|
||||
- Update workbench class name and identifiers
|
||||
- Modify directory structure to match FreeCAD conventions
|
||||
- Update all user-facing strings
|
||||
|
||||
### Option C: Full Workbench Replacement (Major Changes)
|
||||
|
||||
**Effort**: High
|
||||
**Compatibility**: Kindred Create only
|
||||
|
||||
Replace existing PartDesign/Assembly workbenches with unified Kindred workbench.
|
||||
|
||||
**Pros**:
|
||||
- Unified user experience
|
||||
- Complete control over workflow
|
||||
|
||||
**Cons**:
|
||||
- Significant development effort
|
||||
- Diverges heavily from upstream FreeCAD
|
||||
- Harder to merge upstream improvements
|
||||
|
||||
---
|
||||
|
||||
## Files Requiring Modification
|
||||
|
||||
### Critical Files
|
||||
|
||||
| File | Changes Needed |
|
||||
|------|----------------|
|
||||
| `package.xml` | Package name, description, workbench name, classname |
|
||||
| `ztools/InitGui.py` | Class name, MenuText, ToolTip, Icon, console messages |
|
||||
| `ztools/Init.py` | Console messages |
|
||||
| `ztools/ztools/commands/__init__.py` | No changes (internal imports) |
|
||||
|
||||
### Command Registration (All Command Files)
|
||||
|
||||
Each command file registers commands with `Gui.addCommand("ZTools_*", ...)`. These need renaming:
|
||||
|
||||
| File | Commands to Rename |
|
||||
|------|-------------------|
|
||||
| `datum_commands.py` | `ZTools_DatumCreator`, `ZTools_DatumManager` |
|
||||
| `pattern_commands.py` | `ZTools_RotatedLinearPattern` |
|
||||
| `pocket_commands.py` | `ZTools_EnhancedPocket` |
|
||||
| `assembly_pattern_commands.py` | `ZTools_AssemblyLinearPattern`, `ZTools_AssemblyPolarPattern` |
|
||||
| `spreadsheet_commands.py` | `ZTools_SpreadsheetStyleBold`, `ZTools_SpreadsheetStyleItalic`, `ZTools_SpreadsheetStyleUnderline`, `ZTools_SpreadsheetAlignLeft`, `ZTools_SpreadsheetAlignCenter`, `ZTools_SpreadsheetAlignRight`, `ZTools_SpreadsheetBgColor`, `ZTools_SpreadsheetTextColor`, `ZTools_SpreadsheetQuickAlias` |
|
||||
|
||||
### Internal References
|
||||
|
||||
Custom properties use `ZTools_` prefix for disambiguation:
|
||||
|
||||
| File | Properties |
|
||||
|------|-----------|
|
||||
| `datums/core.py` | `ZTools_SourceRefs`, `ZTools_Params`, `ZTools_DatumMode` |
|
||||
| `datum_viewprovider.py` | Same properties accessed |
|
||||
|
||||
**Note**: These property names are stored in FreeCAD documents. Renaming them would break backward compatibility with existing documents. Consider keeping these internal or implementing migration.
|
||||
|
||||
---
|
||||
|
||||
## Branding Changes
|
||||
|
||||
### User-Visible Strings
|
||||
|
||||
| Location | Current | Suggested |
|
||||
|----------|---------|-----------|
|
||||
| `InitGui.py` MenuText | `"ztools"` | `"Kindred Design"` or similar |
|
||||
| `InitGui.py` ToolTip | `"Extended PartDesign replacement..."` | Kindred-specific description |
|
||||
| `InitGui.py` console messages | `"ztools workbench..."` | `"Kindred Design workbench..."` |
|
||||
| `Init.py` console messages | `"ztools addon loaded"` | `"Kindred Design loaded"` |
|
||||
| Toolbar names | `"ztools Datums"`, etc. | `"Kindred Datums"`, etc. |
|
||||
| Menu names | `"ztools"` | `"Kindred"` |
|
||||
|
||||
### Command Prefixes
|
||||
|
||||
Current: `ZTools_*`
|
||||
Suggested: `KindredCreate_*`, `Kindred_*`, or `KC_*`
|
||||
|
||||
### Class Names
|
||||
|
||||
| Current | Suggested |
|
||||
|---------|-----------|
|
||||
| `ZToolsWorkbench` | `KindredDesignWorkbench` |
|
||||
| `ZToolsDatumObject` | `KindredDatumObject` |
|
||||
| `ZToolsDatumViewProvider` | `KindredDatumViewProvider` |
|
||||
|
||||
---
|
||||
|
||||
## FreeCAD API Dependencies
|
||||
|
||||
### Core APIs Used
|
||||
|
||||
```python
|
||||
# Application
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
|
||||
# Part/PartDesign
|
||||
import Part
|
||||
import PartDesign
|
||||
|
||||
# Workbench patterns
|
||||
Gui.Workbench # Base class
|
||||
Gui.addCommand() # Command registration
|
||||
Gui.Control.showDialog() # Task panels
|
||||
Gui.Selection # Selection observer
|
||||
|
||||
# Document operations
|
||||
App.ActiveDocument
|
||||
doc.openTransaction()
|
||||
doc.commitTransaction()
|
||||
doc.abortTransaction()
|
||||
doc.recompute()
|
||||
|
||||
# Feature Python
|
||||
Part.makeCompound()
|
||||
doc.addObject("Part::FeaturePython", name)
|
||||
doc.addObject("PartDesign::Plane", name)
|
||||
doc.addObject("PartDesign::Line", name)
|
||||
doc.addObject("PartDesign::Point", name)
|
||||
doc.addObject("App::Link", name)
|
||||
```
|
||||
|
||||
### Assembly Workbench APIs (FreeCAD 1.0+)
|
||||
|
||||
```python
|
||||
# Assembly operations
|
||||
asm.TypeId == "Assembly::AssemblyObject"
|
||||
App.Link objects for component instances
|
||||
```
|
||||
|
||||
### Spreadsheet APIs
|
||||
|
||||
```python
|
||||
sheet.set(cell, value)
|
||||
sheet.getContents(cell)
|
||||
sheet.setStyle(cell, style)
|
||||
sheet.setAlignment(cell, alignment)
|
||||
sheet.setBackground(cell, (r, g, b))
|
||||
sheet.setForeground(cell, (r, g, b))
|
||||
sheet.setAlias(cell, alias)
|
||||
```
|
||||
|
||||
### Qt/PySide APIs
|
||||
|
||||
```python
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
|
||||
# Dialogs
|
||||
QtWidgets.QColorDialog.getColor()
|
||||
|
||||
# Task panels
|
||||
QtWidgets.QWidget subclasses
|
||||
|
||||
# Selection from table views
|
||||
QTableView.selectionModel()
|
||||
QItemSelectionModel.selectedIndexes()
|
||||
```
|
||||
|
||||
### Preference System
|
||||
|
||||
```python
|
||||
App.ParamGet("User parameter:BaseApp/Preferences/Mod/Spreadsheet")
|
||||
params.SetUnsigned(key, value)
|
||||
params.GetUnsigned(key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Integration Path
|
||||
|
||||
### Phase 1: Branding Update (Option B)
|
||||
|
||||
1. **Fork the repository** for Kindred Create
|
||||
2. **Rename package and workbench**:
|
||||
- `package.xml`: Update name, description
|
||||
- `InitGui.py`: Update class name, MenuText, ToolTip
|
||||
- `Init.py`: Update log messages
|
||||
3. **Rename command prefixes**:
|
||||
- Global find/replace: `ZTools_` → `Kindred_`
|
||||
- Update all `Gui.addCommand()` calls
|
||||
- Update all toolbar/menu references in `InitGui.py`
|
||||
4. **Update toolbar/menu labels**:
|
||||
- `"ztools Datums"` → `"Kindred Datums"`
|
||||
- etc.
|
||||
5. **Keep internal property names** (`ZTools_SourceRefs`, etc.) for document compatibility
|
||||
|
||||
### Phase 2: Directory Restructure
|
||||
|
||||
1. **Move to FreeCAD's Mod directory**:
|
||||
```
|
||||
freecad-source/src/Mod/KindredDesign/
|
||||
├── Init.py
|
||||
├── InitGui.py
|
||||
└── KindredDesign/
|
||||
├── commands/
|
||||
├── datums/
|
||||
└── resources/
|
||||
```
|
||||
2. **Update CMakeLists.txt** in FreeCAD source to include new module
|
||||
3. **Add to default workbench list** if desired
|
||||
|
||||
### Phase 3: Theme Integration
|
||||
|
||||
1. **Move CatppuccinMocha** to Kindred Create's default themes
|
||||
2. **Optionally set as default theme** for new installations
|
||||
3. **Update `apply_spreadsheet_colors()`** to use Kindred theme colors
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Functional Tests
|
||||
|
||||
- [ ] Workbench activates without errors
|
||||
- [ ] All toolbars appear with correct icons
|
||||
- [ ] All menus appear with correct structure
|
||||
- [ ] Datum Creator: All 15 modes work
|
||||
- [ ] Datum Manager: Opens (stub functionality)
|
||||
- [ ] Rotated Linear Pattern: Creates patterns correctly
|
||||
- [ ] Enhanced Pocket: Face selection works
|
||||
- [ ] Assembly Linear Pattern: Creates component arrays
|
||||
- [ ] Assembly Polar Pattern: Creates circular arrays
|
||||
- [ ] Spreadsheet Bold/Italic/Underline: Toggle correctly
|
||||
- [ ] Spreadsheet Alignment: Left/Center/Right work
|
||||
- [ ] Spreadsheet Colors: Dialogs open, colors apply
|
||||
- [ ] Spreadsheet Quick Alias: Creates aliases from labels
|
||||
- [ ] Undo/Redo works for all operations
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- [ ] Existing FreeCAD documents open correctly
|
||||
- [ ] New documents created with Kindred tools save/load properly
|
||||
- [ ] PartDesign features work alongside Kindred datums
|
||||
- [ ] Assembly joints work with Kindred patterns
|
||||
- [ ] Spreadsheet aliases work in PartDesign expressions
|
||||
|
||||
### Theme Tests
|
||||
|
||||
- [ ] Catppuccin Mocha preference pack loads
|
||||
- [ ] Spreadsheet text colors are correct (light on dark)
|
||||
- [ ] Icons render with correct theme colors
|
||||
- [ ] QSS styling applies to all custom widgets
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Document Compatibility
|
||||
|
||||
Documents created with ztools will contain:
|
||||
- Objects with `ZTools_*` custom properties
|
||||
- References to `ZTools_*` in expressions (unlikely but possible)
|
||||
|
||||
**Recommendation**: Keep internal property names unchanged, or implement a document migration script that updates property names on load.
|
||||
|
||||
### User Preference Migration
|
||||
|
||||
If users have existing FreeCAD installations:
|
||||
- Spreadsheet color preferences are stored per-user
|
||||
- Consider running `apply_spreadsheet_colors()` on first Kindred Create launch
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The ztools workbench is well-structured for integration into Kindred Create. The recommended path is **Option B (Native Workbench Integration)** which provides:
|
||||
|
||||
- Native appearance alongside other FreeCAD workbenches
|
||||
- Kindred Create branding throughout
|
||||
- Maintained document compatibility
|
||||
- Reasonable development effort
|
||||
|
||||
Key points:
|
||||
- **No external dependencies** - clean integration
|
||||
- **Standard FreeCAD APIs** - future-compatible
|
||||
- **Modular structure** - easy to extend
|
||||
- **33 custom icons** - complete visual identity ready for recoloring
|
||||
|
||||
Estimated effort for full integration: 2-4 hours for branding changes, additional time for build system integration depending on Kindred Create's structure.
|
||||
420
reference/ztools/PLAN.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# ZTools Development Plan
|
||||
|
||||
## Current Status: v0.3.0 (80% complete)
|
||||
|
||||
### What's Working
|
||||
- Workbench registration with 17 toolbars and menus
|
||||
- All 15 datum creation functions with custom ZTools attachment system
|
||||
- Datum Creator GUI (task panel with Planes/Axes/Points tabs)
|
||||
- OK button creates datum, Cancel dismisses without creating
|
||||
- Rotated Linear Pattern feature (complete)
|
||||
- Icon system (32+ Catppuccin-themed SVGs)
|
||||
- Metadata storage system (ZTools_Type, ZTools_Params, ZTools_SourceRefs)
|
||||
- Spreadsheet linking for parametric control
|
||||
- FreeCAD 1.0+ Assembly workbench integration (all stock commands)
|
||||
- Assembly Linear Pattern tool (complete)
|
||||
- Assembly Polar Pattern tool (complete)
|
||||
- FreeCAD Spreadsheet workbench integration (all stock commands)
|
||||
- zSpreadsheet formatting toolbar (9 commands)
|
||||
|
||||
### Recent Changes (2026-01-25)
|
||||
- Added zSpreadsheet module with formatting toolbar
|
||||
- Native Spreadsheet commands exposed (CreateSheet, Import, Export, SetAlias, MergeCells, SplitCell)
|
||||
- Created 9 formatting commands: Bold, Italic, Underline, Align Left/Center/Right, Background Color, Text Color, Quick Alias
|
||||
- Added 9 spreadsheet icons (Catppuccin Mocha theme)
|
||||
- Spreadsheet text color now defaults to white for dark theme compatibility
|
||||
|
||||
### Previous Changes (2026-01-25)
|
||||
- Added FreeCAD 1.0+ Assembly workbench integration
|
||||
- All native Assembly commands exposed in ztools workbench (21 commands)
|
||||
- Created Assembly Linear Pattern tool with task panel UI
|
||||
- Created Assembly Polar Pattern tool with task panel UI
|
||||
- Added assembly pattern icons (Catppuccin Mocha theme)
|
||||
|
||||
### Previous Changes (2026-01-24)
|
||||
- Replaced FreeCAD's vanilla attachment system with custom ZTools attachment
|
||||
- All datums now use `MapMode='Deactivated'` with calculated placements
|
||||
- Source references stored in `ZTools_SourceRefs` property for future update capability
|
||||
- Fixed all 3 point functions (`point_on_edge`, `point_center_of_face`, `point_center_of_circle`) to accept source parameters
|
||||
- Removed redundant "Create Datum" button - OK now creates the datum
|
||||
- Task panel properly cleans up selection observer on close
|
||||
|
||||
---
|
||||
|
||||
## ZTools Attachment System
|
||||
|
||||
FreeCAD's vanilla attachment system has reliability issues. ZTools uses a custom approach:
|
||||
|
||||
1. **Calculate placement directly** from source geometry at creation time
|
||||
2. **Store source references** in `ZTools_SourceRefs` property (JSON)
|
||||
3. **Use `MapMode='Deactivated'`** to prevent FreeCAD attachment interference
|
||||
4. **Store creation parameters** in `ZTools_Params` for potential recalculation
|
||||
|
||||
This gives full control over datum positioning while maintaining the ability to update datums when source geometry changes (future feature).
|
||||
|
||||
### Metadata Properties
|
||||
|
||||
All ZTools datums have these custom properties:
|
||||
- `ZTools_Type`: Creation method identifier (e.g., "offset_from_face", "midplane")
|
||||
- `ZTools_Params`: JSON-encoded creation parameters
|
||||
- `ZTools_SourceRefs`: JSON-encoded list of source geometry references
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Complete (Assembly Integration)
|
||||
|
||||
### FreeCAD 1.0+ Assembly Workbench Commands
|
||||
|
||||
ZTools exposes all native FreeCAD Assembly workbench commands in 3 toolbars:
|
||||
|
||||
**Assembly Structure:**
|
||||
- `Assembly_CreateAssembly` - Create new assembly container
|
||||
- `Assembly_InsertLink` - Insert component as link
|
||||
- `Assembly_InsertNewPart` - Create and insert new part
|
||||
|
||||
**Assembly Joints (13 types):**
|
||||
- `Assembly_CreateJointFixed` - Lock parts together (0 DOF)
|
||||
- `Assembly_CreateJointRevolute` - Rotation around axis
|
||||
- `Assembly_CreateJointCylindrical` - Rotation + translation along axis
|
||||
- `Assembly_CreateJointSlider` - Translation along axis
|
||||
- `Assembly_CreateJointBall` - Spherical rotation
|
||||
- `Assembly_CreateJointDistance` - Maintain distance
|
||||
- `Assembly_CreateJointParallel` - Keep parallel
|
||||
- `Assembly_CreateJointPerpendicular` - Keep perpendicular
|
||||
- `Assembly_CreateJointAngle` - Maintain angle
|
||||
- `Assembly_CreateJointRackPinion` - Rack and pinion motion
|
||||
- `Assembly_CreateJointScrew` - Helical motion
|
||||
- `Assembly_CreateJointGears` - Gear ratio constraint
|
||||
- `Assembly_CreateJointBelt` - Belt/pulley constraint
|
||||
|
||||
**Assembly Management:**
|
||||
- `Assembly_ToggleGrounded` - Lock part in place
|
||||
- `Assembly_SolveAssembly` - Run constraint solver
|
||||
- `Assembly_CreateView` - Create exploded view
|
||||
- `Assembly_CreateBom` - Generate bill of materials
|
||||
- `Assembly_ExportASMT` - Export assembly file
|
||||
|
||||
### ZTools Assembly Pattern Tools
|
||||
|
||||
**Assembly Linear Pattern** (`ZTools_AssemblyLinearPattern`)
|
||||
|
||||
Creates copies of assembly components along a linear direction.
|
||||
|
||||
Features:
|
||||
- Multi-component selection via table UI
|
||||
- Direction vector (X, Y, Z)
|
||||
- Occurrence count (2-100)
|
||||
- Spacing modes: Total Length or Fixed Spacing
|
||||
- Creates as Links (recommended) or copies
|
||||
- Option to hide original components
|
||||
- Auto-detects parent assembly
|
||||
|
||||
UI Layout:
|
||||
```
|
||||
+----------------------------------+
|
||||
| Components |
|
||||
| +------------------------------+ |
|
||||
| | Component_1 [X] | |
|
||||
| | Component_2 [X] | |
|
||||
| +------------------------------+ |
|
||||
| Select components in 3D view |
|
||||
+----------------------------------+
|
||||
| Pattern Parameters |
|
||||
| Direction: X[1] Y[0] Z[0] |
|
||||
| Occurrences: [3] |
|
||||
| Mode: [Total Length v] |
|
||||
| Total Length: [100 mm] |
|
||||
+----------------------------------+
|
||||
| Options |
|
||||
| [x] Create as Links |
|
||||
| [ ] Hide original components |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
**Assembly Polar Pattern** (`ZTools_AssemblyPolarPattern`)
|
||||
|
||||
Creates copies of assembly components around a rotation axis.
|
||||
|
||||
Features:
|
||||
- Multi-component selection via table UI
|
||||
- Axis presets (X, Y, Z) or custom axis vector
|
||||
- Center point specification
|
||||
- Occurrence count (2-100)
|
||||
- Angle modes: Full Circle (360) or Custom Angle
|
||||
- Creates as Links (recommended) or copies
|
||||
- Option to hide original components
|
||||
|
||||
UI Layout:
|
||||
```
|
||||
+----------------------------------+
|
||||
| Components |
|
||||
| +------------------------------+ |
|
||||
| | Component_1 [X] | |
|
||||
| +------------------------------+ |
|
||||
+----------------------------------+
|
||||
| Rotation Axis |
|
||||
| Axis: [Z Axis v] |
|
||||
| Direction: X[0] Y[0] Z[1] |
|
||||
| Center: X[0] Y[0] Z[0] |
|
||||
+----------------------------------+
|
||||
| Pattern Parameters |
|
||||
| Occurrences: [6] |
|
||||
| Mode: [Full Circle v] |
|
||||
| Total Angle: [360 deg] |
|
||||
+----------------------------------+
|
||||
| Options |
|
||||
| [x] Create as Links |
|
||||
| [ ] Hide original components |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5: Complete (zSpreadsheet)
|
||||
|
||||
### FreeCAD Spreadsheet Workbench Commands
|
||||
|
||||
ZTools exposes native Spreadsheet commands in the "Spreadsheet" toolbar:
|
||||
|
||||
- `Spreadsheet_CreateSheet` - Create new spreadsheet
|
||||
- `Spreadsheet_Import` - Import CSV file
|
||||
- `Spreadsheet_Export` - Export to CSV
|
||||
- `Spreadsheet_SetAlias` - Set cell alias
|
||||
- `Spreadsheet_MergeCells` - Merge selected cells
|
||||
- `Spreadsheet_SplitCell` - Split merged cell
|
||||
|
||||
### ZTools Spreadsheet Formatting Tools
|
||||
|
||||
Quick formatting toolbar for cell styling without dialogs:
|
||||
|
||||
**Style Commands:**
|
||||
- `ZTools_SpreadsheetStyleBold` - Toggle bold (B icon)
|
||||
- `ZTools_SpreadsheetStyleItalic` - Toggle italic (I icon)
|
||||
- `ZTools_SpreadsheetStyleUnderline` - Toggle underline (U icon)
|
||||
|
||||
**Alignment Commands:**
|
||||
- `ZTools_SpreadsheetAlignLeft` - Align text left
|
||||
- `ZTools_SpreadsheetAlignCenter` - Align text center
|
||||
- `ZTools_SpreadsheetAlignRight` - Align text right
|
||||
|
||||
**Color Commands:**
|
||||
- `ZTools_SpreadsheetBgColor` - Set cell background color (color picker)
|
||||
- `ZTools_SpreadsheetTextColor` - Set cell text color (color picker)
|
||||
|
||||
**Utility Commands:**
|
||||
- `ZTools_SpreadsheetQuickAlias` - Create alias from adjacent label cell
|
||||
|
||||
### Implementation Details
|
||||
|
||||
**Cell Selection Helper:**
|
||||
The `get_selected_cells()` function:
|
||||
1. Gets active MDI subwindow
|
||||
2. Finds QTableView widget
|
||||
3. Gets selected indexes from selection model
|
||||
4. Converts to A1 notation (handles AA, AB, etc.)
|
||||
|
||||
**Style Toggle Pattern:**
|
||||
```python
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
if "bold" in styles:
|
||||
styles.discard("bold")
|
||||
else:
|
||||
styles.add("bold")
|
||||
sheet.setStyle(cell, "|".join(sorted(styles)))
|
||||
```
|
||||
|
||||
**Color Picker Integration:**
|
||||
Uses Qt's `QColorDialog.getColor()` with Catppuccin defaults for dark theme.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Complete (Datum Tools)
|
||||
|
||||
All datum creation functions now work:
|
||||
|
||||
### Planes (6 modes)
|
||||
- Offset from Face
|
||||
- Midplane (2 Faces)
|
||||
- 3 Points
|
||||
- Normal to Edge
|
||||
- Angled from Face
|
||||
- Tangent to Cylinder
|
||||
|
||||
### Axes (4 modes)
|
||||
- 2 Points
|
||||
- From Edge
|
||||
- Cylinder Center
|
||||
- Plane Intersection
|
||||
|
||||
### Points (5 modes)
|
||||
- At Vertex
|
||||
- XYZ Coordinates
|
||||
- On Edge (with parameter)
|
||||
- Face Center
|
||||
- Circle Center
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Complete Enhanced Pocket
|
||||
|
||||
### 2.1 Wire Up Pocket Execution (pocket_commands.py)
|
||||
|
||||
The EnhancedPocketTaskPanel has complete UI but no execute logic.
|
||||
|
||||
Required implementation:
|
||||
1. Get selected sketch from user
|
||||
2. Create PartDesign::Pocket with selected type
|
||||
3. Apply "Flip Side to Cut" by:
|
||||
- Reversing the pocket direction, OR
|
||||
- Using a boolean cut approach with inverted profile
|
||||
4. Handle all pocket types: Dimension, Through All, To First, Up To Face, Two Dimensions
|
||||
|
||||
### 2.2 Register Pocket Command
|
||||
|
||||
Add to InitGui.py toolbar if not already present.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Datum Manager
|
||||
|
||||
### 3.1 Implement DatumManagerTaskPanel
|
||||
|
||||
Replace the stub in datum_commands.py with functional panel:
|
||||
|
||||
Features:
|
||||
- List all datum objects (planes, axes, points) in document
|
||||
- Filter by type (ZTools-created vs native)
|
||||
- Toggle visibility (eye icon per item)
|
||||
- Rename datums inline
|
||||
- Delete selected datums
|
||||
- Jump to datum in model tree
|
||||
|
||||
UI Layout:
|
||||
```
|
||||
+----------------------------------+
|
||||
| Filter: [All v] [ZTools only ☐] |
|
||||
+----------------------------------+
|
||||
| ☑ ZPlane_Offset_001 [👁] [🗑] |
|
||||
| ☑ ZPlane_Mid_001 [👁] [🗑] |
|
||||
| ☐ ZAxis_Cyl_001 [👁] [🗑] |
|
||||
+----------------------------------+
|
||||
| [Rename] [Show All] [Hide All] |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Additional Features (Future)
|
||||
|
||||
### 4.1 Module 2 Completion: Enhanced Pad
|
||||
- Multi-body support
|
||||
- Draft angles on pad
|
||||
- Lip/groove profiles
|
||||
|
||||
### 4.2 Module 3: Body Operations
|
||||
- Split body at plane
|
||||
- Combine bodies
|
||||
- Shell improvements
|
||||
|
||||
### 4.3 Module 4: Pattern Tools
|
||||
- Curve-driven pattern (sweep instances along spline)
|
||||
- Fill pattern (populate region with instances)
|
||||
- Pattern with variable spacing
|
||||
|
||||
### 4.4 Datum Update Feature
|
||||
- Use stored `ZTools_SourceRefs` to recalculate datum positions
|
||||
- Handle topology changes gracefully
|
||||
- Option to "freeze" datums (disconnect from sources)
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose | Lines |
|
||||
|------|---------|-------|
|
||||
| `ztools/ztools/datums/core.py` | Datum creation functions | ~750 |
|
||||
| `ztools/ztools/commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 |
|
||||
| `ztools/ztools/commands/pocket_commands.py` | Enhanced Pocket GUI | ~600 |
|
||||
| `ztools/ztools/commands/pattern_commands.py` | Rotated Linear Pattern | ~206 |
|
||||
| `ztools/ztools/commands/assembly_pattern_commands.py` | Assembly Linear/Polar Patterns | ~580 |
|
||||
| `ztools/ztools/commands/spreadsheet_commands.py` | Spreadsheet formatting tools | ~480 |
|
||||
| `ztools/InitGui.py` | Workbench registration | ~330 |
|
||||
| `ztools/ztools/resources/icons.py` | SVG icon definitions | ~540 |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Phase 1 Tests (Datum Tools)
|
||||
- [ ] Create plane offset from face
|
||||
- [ ] Create midplane between 2 faces
|
||||
- [ ] Create plane from 3 points
|
||||
- [ ] Create plane normal to edge at various parameters
|
||||
- [ ] Create angled plane from face about edge
|
||||
- [ ] Create plane tangent to cylinder
|
||||
- [ ] Create axis from 2 points
|
||||
- [ ] Create axis from edge
|
||||
- [ ] Create axis at cylinder center
|
||||
- [ ] Create axis at plane intersection
|
||||
- [ ] Create point at vertex
|
||||
- [ ] Create point at XYZ coordinates
|
||||
- [ ] Create point on edge at parameter 0.0, 0.5, 1.0
|
||||
- [ ] Create point at face center (planar and cylindrical)
|
||||
- [ ] Create point at circle center (full circle and arc)
|
||||
- [ ] Verify ZTools_Type, ZTools_Params, ZTools_SourceRefs properties exist
|
||||
- [ ] Verify no "deactivated attachment mode" warnings in console
|
||||
|
||||
### Phase 2 Tests (Enhanced Pocket)
|
||||
- [ ] Create pocket with Dimension type
|
||||
- [ ] Create pocket with Through All
|
||||
- [ ] Create pocket with Flip Side to Cut enabled
|
||||
- [ ] Verify pocket respects taper angle
|
||||
|
||||
### Phase 3 Tests (Datum Manager)
|
||||
- [ ] Datum Manager lists all datums
|
||||
- [ ] Visibility toggle works
|
||||
- [ ] Rename persists after recompute
|
||||
- [ ] Delete removes datum cleanly
|
||||
|
||||
### Assembly Integration Tests
|
||||
- [ ] Assembly workbench commands appear in toolbars
|
||||
- [ ] Assembly_CreateAssembly works from ztools
|
||||
- [ ] Assembly_InsertLink works from ztools
|
||||
- [ ] All joint commands accessible
|
||||
- [ ] Assembly_SolveAssembly works
|
||||
|
||||
### Assembly Pattern Tests
|
||||
- [ ] Linear pattern with 3 occurrences along X axis
|
||||
- [ ] Linear pattern with Total Length mode
|
||||
- [ ] Linear pattern with Spacing mode
|
||||
- [ ] Linear pattern creates links (not copies)
|
||||
- [ ] Polar pattern with 6 occurrences (full circle)
|
||||
- [ ] Polar pattern with custom angle (90 degrees, 4 occurrences)
|
||||
- [ ] Polar pattern around Z axis
|
||||
- [ ] Polar pattern with custom center point
|
||||
- [ ] Multiple components can be patterned simultaneously
|
||||
- [ ] Pattern instances added to parent assembly
|
||||
|
||||
### zSpreadsheet Tests
|
||||
- [ ] Spreadsheet toolbar appears with stock commands
|
||||
- [ ] ztools Spreadsheet toolbar appears with formatting commands
|
||||
- [ ] Create new spreadsheet via toolbar
|
||||
- [ ] Select cells and toggle Bold style
|
||||
- [ ] Select cells and toggle Italic style
|
||||
- [ ] Select cells and toggle Underline style
|
||||
- [ ] Align cells left/center/right
|
||||
- [ ] Set background color via color picker
|
||||
- [ ] Set text color via color picker (verify white default for dark theme)
|
||||
- [ ] Quick Alias creates alias from left-adjacent cell content
|
||||
- [ ] Undo/redo works for all formatting operations
|
||||
- [ ] Commands are disabled when no cells selected
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- FreeCAD 1.0+ required (TNP mitigation assumed)
|
||||
- ZTools uses custom attachment system (not FreeCAD's vanilla attachment)
|
||||
- Catppuccin Mocha theme is bundled as preference pack
|
||||
- LGPL-3.0-or-later license
|
||||
388
reference/ztools/ROADMAP.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# ZTools Roadmap
|
||||
|
||||
**Version:** 0.3.0
|
||||
**Last Updated:** 2026-01-25
|
||||
**Target Platform:** FreeCAD 1.0+
|
||||
**License:** LGPL-3.0-or-later
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
ZTools is an extended PartDesign workbench replacement for FreeCAD, focused on velocity-driven CAD workflows. It integrates enhanced datum creation, assembly patterning, spreadsheet formatting, and a unified dark theme (Catppuccin Mocha).
|
||||
|
||||
**Current State:** 80% complete for v1.0 release
|
||||
**Active Development Areas:** Datum management, Enhanced Pocket completion, documentation
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Implemented Features](#implemented-features)
|
||||
2. [Known Gaps & Incomplete Features](#known-gaps--incomplete-features)
|
||||
3. [FreeCAD Ecosystem Alignment](#freecad-ecosystem-alignment)
|
||||
4. [Development Roadmap](#development-roadmap)
|
||||
5. [Technical Architecture](#technical-architecture)
|
||||
6. [File Reference](#file-reference)
|
||||
|
||||
---
|
||||
|
||||
## Implemented Features
|
||||
|
||||
### 1. Workbench Integration (17 Toolbars)
|
||||
|
||||
ZTools consolidates multiple FreeCAD workbenches into a single unified interface:
|
||||
|
||||
| Category | Toolbars | Commands |
|
||||
|----------|----------|----------|
|
||||
| PartDesign | Structure, Datums, Additive, Subtractive, Transformations, Dress-Up, Boolean | 35+ native commands |
|
||||
| Sketcher | Sketcher | 4 native commands |
|
||||
| Assembly | Assembly, Assembly Joints, Assembly Management | 21 native commands |
|
||||
| Spreadsheet | Spreadsheet | 6 native commands |
|
||||
| ZTools Custom | ztools Datums, ztools Patterns, ztools Features, ztools Assembly, ztools Spreadsheet | 14 custom commands |
|
||||
|
||||
### 2. Datum Creation System (16 Functions)
|
||||
|
||||
**Custom Attachment System** - Replaces FreeCAD's unreliable vanilla attachment:
|
||||
- Calculates placement directly from source geometry
|
||||
- Stores source references in `ZTools_SourceRefs` (JSON)
|
||||
- Uses `MapMode='Deactivated'` to prevent interference
|
||||
- Stores creation parameters in `ZTools_Params` for recalculation
|
||||
|
||||
#### Datum Planes (7 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| Offset from Face | `plane_offset_from_face()` | distance (mm) |
|
||||
| Offset from Plane | `plane_offset_from_plane()` | distance (mm) |
|
||||
| Midplane | `plane_midplane()` | 2 parallel faces |
|
||||
| 3 Points | `plane_from_3_points()` | 3 vertices |
|
||||
| Normal to Edge | `plane_normal_to_edge()` | parameter (0.0-1.0) |
|
||||
| Angled | `plane_angled()` | angle (degrees) |
|
||||
| Tangent to Cylinder | `plane_tangent_to_cylinder()` | angle (degrees) |
|
||||
|
||||
#### Datum Axes (4 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| 2 Points | `axis_from_2_points()` | 2 vertices |
|
||||
| From Edge | `axis_from_edge()` | linear edge |
|
||||
| Cylinder Center | `axis_cylinder_center()` | cylindrical face |
|
||||
| Plane Intersection | `axis_intersection_planes()` | 2 planes |
|
||||
|
||||
#### Datum Points (5 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| At Vertex | `point_at_vertex()` | vertex |
|
||||
| XYZ Coordinates | `point_at_coordinates()` | x, y, z |
|
||||
| On Edge | `point_on_edge()` | parameter (0.0-1.0) |
|
||||
| Face Center | `point_center_of_face()` | face |
|
||||
| Circle Center | `point_center_of_circle()` | circular edge |
|
||||
|
||||
**Datum Creator GUI:**
|
||||
- Auto-detection of 15+ creation modes based on selection
|
||||
- Manual mode override
|
||||
- Spreadsheet linking option
|
||||
- Custom naming
|
||||
- Real-time selection observer
|
||||
|
||||
### 3. Pattern Tools (3 Commands)
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation per instance | Complete |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Complete |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Complete |
|
||||
|
||||
**Assembly Pattern Features:**
|
||||
- Multi-component selection via table UI
|
||||
- Creates as Links (recommended) or copies
|
||||
- Direction/axis presets or custom vectors
|
||||
- Spacing modes: Total Length or Fixed Spacing
|
||||
- Angle modes: Full Circle or Custom Angle
|
||||
- Auto-detects parent assembly container
|
||||
|
||||
### 4. Spreadsheet Formatting (9 Commands)
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_SpreadsheetStyleBold` | Toggle bold | Complete |
|
||||
| `ZTools_SpreadsheetStyleItalic` | Toggle italic | Complete |
|
||||
| `ZTools_SpreadsheetStyleUnderline` | Toggle underline | Complete |
|
||||
| `ZTools_SpreadsheetAlignLeft` | Left align | Complete |
|
||||
| `ZTools_SpreadsheetAlignCenter` | Center align | Complete |
|
||||
| `ZTools_SpreadsheetAlignRight` | Right align | Complete |
|
||||
| `ZTools_SpreadsheetBgColor` | Background color picker | Complete |
|
||||
| `ZTools_SpreadsheetTextColor` | Text color picker | Complete |
|
||||
| `ZTools_SpreadsheetQuickAlias` | Auto-create alias from label | Complete |
|
||||
|
||||
### 5. Enhanced Features
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_EnhancedPocket` | Pocket with "Flip Side to Cut" (SOLIDWORKS-style) | 90% Complete |
|
||||
|
||||
**Flip Side to Cut:** Removes material OUTSIDE the sketch profile instead of inside, using Boolean Common operation.
|
||||
|
||||
### 6. Theme System (Catppuccin Mocha)
|
||||
|
||||
- Complete Qt StyleSheet (QSS) for entire FreeCAD interface
|
||||
- 26-color palette consistently applied
|
||||
- 50+ widget types styled
|
||||
- FreeCAD-specific widgets: PropertyEditor, Python Console, Spreadsheet
|
||||
- Spreadsheet colors auto-applied on workbench activation
|
||||
|
||||
### 7. Icon System (33 Icons)
|
||||
|
||||
All icons use Catppuccin Mocha palette:
|
||||
- Workbench icon
|
||||
- Datum icons (planes, axes, points - 13 total)
|
||||
- Pattern icons (3 total)
|
||||
- Pocket icons (2 total)
|
||||
- Assembly pattern icons (2 total)
|
||||
- Spreadsheet formatting icons (9 total)
|
||||
|
||||
---
|
||||
|
||||
## Known Gaps & Incomplete Features
|
||||
|
||||
### Critical (Must Fix)
|
||||
|
||||
| Issue | Location | Description | Priority |
|
||||
|-------|----------|-------------|----------|
|
||||
| Datum Manager stub | `datum_commands.py:853` | Placeholder only - needs full implementation | High |
|
||||
| Datum edit recalculation | `datum_viewprovider.py:351,355,359` | Parameter changes don't recalculate placement from source geometry | High |
|
||||
|
||||
### Non-Critical (Should Fix)
|
||||
|
||||
| Issue | Location | Description | Priority |
|
||||
|-------|----------|-------------|----------|
|
||||
| Enhanced Pocket incomplete | `pocket_commands.py` | Taper angle disabled for flipped pockets | Medium |
|
||||
| Pocket execution logic | `pocket_commands.py` | UI complete but execution needs verification | Medium |
|
||||
|
||||
### Future Enhancements (Nice to Have)
|
||||
|
||||
| Feature | Description | Priority |
|
||||
|---------|-------------|----------|
|
||||
| Curve-driven pattern | Sweep instances along spline | Low |
|
||||
| Fill pattern | Populate region with instances | Low |
|
||||
| Variable spacing pattern | Non-uniform spacing | Low |
|
||||
| Enhanced Pad | Multi-body support, draft angles | Low |
|
||||
| Body operations | Split, combine, shell improvements | Low |
|
||||
|
||||
---
|
||||
|
||||
## FreeCAD Ecosystem Alignment
|
||||
|
||||
### FreeCAD 1.0 (November 2024) - Current Target
|
||||
|
||||
**Key Features ZTools Leverages:**
|
||||
- **TNP Mitigation:** Topological Naming Problem largely resolved
|
||||
- **Integrated Assembly Workbench:** Ondsel's assembly system now core
|
||||
- **Material System Overhaul:** New material handling
|
||||
- **UI/UX Improvements:** Dark/light themes, selection filters
|
||||
|
||||
**ZTools Alignment:**
|
||||
- Custom attachment system complements TNP fix
|
||||
- Full integration with native Assembly workbench
|
||||
- Catppuccin theme extends FreeCAD's theming
|
||||
|
||||
### FreeCAD 1.1 (Expected Late 2025)
|
||||
|
||||
**Planned Features:**
|
||||
- New Transform Manipulator
|
||||
- UI Material Rendering Improvements
|
||||
- Continued TNP refinement for Sketcher/PartDesign
|
||||
|
||||
**ZTools Opportunities:**
|
||||
- Watch for new Assembly API standardization
|
||||
- Monitor Sketcher improvements for datum integration
|
||||
|
||||
### FreeCAD Strategic Priorities (from Roadmap)
|
||||
|
||||
| FreeCAD Priority | ZTools Alignment |
|
||||
|------------------|------------------|
|
||||
| Model Stability | Custom attachment system prevents fragile models |
|
||||
| Assembly Integration | Full native Assembly command exposure |
|
||||
| Flatten Learning Curve | Unified toolbar consolidation |
|
||||
| UI Modernization | Catppuccin Mocha theme |
|
||||
| Streamlined Workflow | Quick formatting toolbars, auto-detection |
|
||||
|
||||
### Ondsel Contributions (Note: Ondsel shut down October 2025)
|
||||
|
||||
Ondsel's contributions now maintained by FreeCAD community:
|
||||
- Assembly workbench (ZTools integrates)
|
||||
- VarSets custom properties (potential future integration)
|
||||
- Sketcher improvements
|
||||
|
||||
---
|
||||
|
||||
## Development Roadmap
|
||||
|
||||
### Phase 1: v0.3.x - Stability & Completion (Current)
|
||||
|
||||
**Timeline:** Q1 2026
|
||||
|
||||
| Task | Status | Priority |
|
||||
|------|--------|----------|
|
||||
| Complete Datum Manager GUI | Not Started | High |
|
||||
| Implement datum parameter recalculation | Not Started | High |
|
||||
| Verify Enhanced Pocket execution | Partial | Medium |
|
||||
| Add comprehensive test coverage | Not Started | Medium |
|
||||
| Documentation completion | In Progress | Medium |
|
||||
|
||||
### Phase 2: v0.4.0 - Polish & UX
|
||||
|
||||
**Timeline:** Q2 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Keyboard shortcuts | Add hotkeys for common operations |
|
||||
| Context menus | Right-click menus in 3D view |
|
||||
| Undo/redo improvements | Better transaction naming |
|
||||
| Error handling | User-friendly error messages |
|
||||
| Preferences panel | ZTools configuration UI |
|
||||
|
||||
### Phase 3: v0.5.0 - Advanced Features
|
||||
|
||||
**Timeline:** Q3 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Curve-driven pattern | Pattern along splines |
|
||||
| Enhanced Pad | Draft angles, lip/groove |
|
||||
| Body operations | Split, combine, shell |
|
||||
| Datum freeze/update | Control source geometry updates |
|
||||
|
||||
### Phase 4: v1.0.0 - Production Release
|
||||
|
||||
**Timeline:** Q4 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Full test suite | Automated testing |
|
||||
| User documentation | Complete user guide |
|
||||
| Video tutorials | Getting started series |
|
||||
| FreeCAD Addon Manager | Official listing |
|
||||
|
||||
---
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ztools/
|
||||
├── Init.py # Startup (non-GUI)
|
||||
├── InitGui.py # Workbench registration
|
||||
└── ztools/
|
||||
├── __init__.py
|
||||
├── commands/ # GUI commands
|
||||
│ ├── __init__.py
|
||||
│ ├── datum_commands.py # Datum Creator/Manager
|
||||
│ ├── datum_viewprovider.py # Custom ViewProvider
|
||||
│ ├── pattern_commands.py # Rotated Linear Pattern
|
||||
│ ├── pocket_commands.py # Enhanced Pocket
|
||||
│ ├── assembly_pattern_commands.py # Assembly patterns
|
||||
│ └── spreadsheet_commands.py # Spreadsheet formatting
|
||||
├── datums/ # Core datum library
|
||||
│ ├── __init__.py
|
||||
│ └── core.py # 16 datum functions
|
||||
└── resources/ # Assets
|
||||
├── __init__.py
|
||||
├── icons.py # 33 SVG icons
|
||||
├── theme.py # Catppuccin QSS
|
||||
└── icons/ # Generated SVG files
|
||||
```
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
1. **Command Pattern:** All tools follow FreeCAD's `GetResources()`, `Activated()`, `IsActive()` pattern
|
||||
2. **Task Panel Pattern:** Complex UIs use `QWidget` with selection observers
|
||||
3. **Feature Python Pattern:** Custom features use `Part::FeaturePython` with ViewProvider
|
||||
4. **Metadata System:** JSON properties for tracking ZTools objects
|
||||
|
||||
### Metadata Properties
|
||||
|
||||
All ZTools objects have:
|
||||
- `ZTools_Type`: Feature type identifier
|
||||
- `ZTools_Params`: JSON creation parameters
|
||||
- `ZTools_SourceRefs`: JSON source geometry references
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose | Lines |
|
||||
|------|---------|-------|
|
||||
| `InitGui.py` | Workbench registration, toolbars, menus | ~330 |
|
||||
| `datums/core.py` | 16 datum creation functions | ~1300 |
|
||||
| `commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 |
|
||||
| `commands/datum_viewprovider.py` | Custom ViewProvider, edit panel | ~400 |
|
||||
| `commands/pattern_commands.py` | Rotated Linear Pattern | ~206 |
|
||||
| `commands/pocket_commands.py` | Enhanced Pocket | ~600 |
|
||||
| `commands/assembly_pattern_commands.py` | Assembly patterns | ~580 |
|
||||
| `commands/spreadsheet_commands.py` | Spreadsheet formatting | ~480 |
|
||||
| `resources/icons.py` | 33 SVG icon definitions | ~540 |
|
||||
| `resources/theme.py` | Catppuccin Mocha QSS | ~1500 |
|
||||
|
||||
**Total:** ~6,400+ lines of code
|
||||
|
||||
---
|
||||
|
||||
## Statistics Summary
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| Command Files | 6 |
|
||||
| Command Classes | 24+ |
|
||||
| Datum Creation Functions | 16 |
|
||||
| Icons Defined | 33 |
|
||||
| Toolbars Registered | 17 |
|
||||
| Menu Hierarchies | 7 major |
|
||||
| Native Commands Exposed | 66 |
|
||||
| Custom ZTools Commands | 14 |
|
||||
| Theme Colors | 26 |
|
||||
| Styled Widget Types | 50+ |
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
ZTools follows FreeCAD's contribution guidelines. Key areas needing help:
|
||||
|
||||
1. **Testing:** Manual testing on different platforms
|
||||
2. **Documentation:** User guides and tutorials
|
||||
3. **Translations:** Internationalization support
|
||||
4. **Bug Reports:** Issue tracking and reproduction
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0-or-later
|
||||
|
||||
Compatible with FreeCAD's licensing model.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.3.0 (2026-01-25)
|
||||
- Added zSpreadsheet module with 9 formatting commands
|
||||
- Added Spreadsheet workbench integration (6 native commands)
|
||||
- Added 9 spreadsheet formatting icons
|
||||
|
||||
### v0.2.0 (2026-01-25)
|
||||
- Added Assembly workbench integration (21 native commands)
|
||||
- Added Assembly Linear Pattern tool
|
||||
- Added Assembly Polar Pattern tool
|
||||
- Added assembly pattern icons
|
||||
|
||||
### v0.1.0 (2026-01-24)
|
||||
- Initial release
|
||||
- Custom attachment system for datums
|
||||
- 16 datum creation functions
|
||||
- Datum Creator GUI with auto-detection
|
||||
- Rotated Linear Pattern
|
||||
- Enhanced Pocket (partial)
|
||||
- Catppuccin Mocha theme
|
||||
- 21 initial icons
|
||||
58
reference/ztools/TODO_ATTACHMENT_WORK.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Datum Attachment Work - In Progress
|
||||
|
||||
## Context
|
||||
Implementing proper FreeCAD attachment for datum objects to avoid "deactivated attachment mode" warnings.
|
||||
The pattern is adding `source_object` and `source_subname` parameters to each datum function and using `_setup_datum_attachment()` with appropriate MapModes.
|
||||
|
||||
## Completed Functions (in core.py)
|
||||
|
||||
### Planes
|
||||
- `plane_offset_from_face` - MapMode='FlatFace'
|
||||
- `plane_midplane` - MapMode='TwoFace'
|
||||
- `plane_from_3_points` - MapMode='ThreePointPlane'
|
||||
- `plane_normal_to_edge` - MapMode='NormalToPath'
|
||||
- `plane_angled` - MapMode='FlatFace' with rotation offset
|
||||
- `plane_tangent_to_cylinder` - MapMode='Tangent'
|
||||
|
||||
### Axes
|
||||
- `axis_from_2_points` - MapMode='TwoPointLine'
|
||||
- `axis_from_edge` - MapMode='ObjectXY'
|
||||
- `axis_cylinder_center` - MapMode='ObjectZ'
|
||||
- `axis_intersection_planes` - MapMode='TwoFace'
|
||||
|
||||
### Points
|
||||
- `point_at_vertex` - MapMode='Vertex'
|
||||
|
||||
## Remaining Functions to Update (in core.py)
|
||||
|
||||
- `point_at_coordinates` - No attachment needed (explicit coordinates), but could use 'Translate' mode
|
||||
- `point_on_edge` - Use MapMode='OnEdge' with MapPathParameter for position
|
||||
- `point_center_of_face` - Use MapMode='CenterOfCurvature' or similar
|
||||
- `point_center_of_circle` - Use MapMode='CenterOfCurvature'
|
||||
|
||||
## After core.py Updates
|
||||
|
||||
Update `datum_commands.py` to pass source references to the remaining point functions:
|
||||
- `create_point_at_vertex` - already done
|
||||
- `create_point_on_edge` - needs update
|
||||
- `create_point_center_face` - needs update
|
||||
- `create_point_center_circle` - needs update
|
||||
|
||||
## Pattern for Updates
|
||||
|
||||
1. Add parameters to function signature:
|
||||
```python
|
||||
source_object: Optional[App.DocumentObject] = None,
|
||||
source_subname: Optional[str] = None,
|
||||
```
|
||||
|
||||
2. In the body section, use attachment instead of placement:
|
||||
```python
|
||||
if source_object and source_subname:
|
||||
support = [(source_object, source_subname)]
|
||||
_setup_datum_attachment(point, support, "MapMode")
|
||||
else:
|
||||
_setup_datum_placement(point, App.Placement(...))
|
||||
```
|
||||
|
||||
3. Update datum_commands.py to extract and pass source references from selection.
|
||||
36
reference/ztools/package.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
|
||||
<name>ZTools</name>
|
||||
|
||||
<description>Extended PartDesign workbench with velocity-focused tools and advanced datum creation.</description>
|
||||
|
||||
<version>0.1.0</version>
|
||||
|
||||
<date>2026-01-24</date>
|
||||
|
||||
<license file="LICENSE">LGPL-3.0-or-later</license>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<name>ZTools</name>
|
||||
<classname>ZToolsWorkbench</classname>
|
||||
<subdirectory>./ztools</subdirectory>
|
||||
</workbench>
|
||||
</content>
|
||||
|
||||
<!-- Kindred Create extensions -->
|
||||
<kindred>
|
||||
<min_create_version>0.1.0</min_create_version>
|
||||
<load_priority>50</load_priority>
|
||||
<pure_python>true</pure_python>
|
||||
<dependencies>
|
||||
<dependency>sdk</dependency>
|
||||
</dependencies>
|
||||
<contexts>
|
||||
<context id="partdesign.body" action="inject"/>
|
||||
<context id="partdesign.feature" action="inject"/>
|
||||
</contexts>
|
||||
</kindred>
|
||||
|
||||
</package>
|
||||
591
reference/ztools/partdesign.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# FreeCAD 1.0.2 PartDesign Workbench Command Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The PartDesign Workbench uses a **feature-based parametric methodology** where a component is represented by a Body container. Features are cumulative—each builds on the result of preceding features. Most features are based on parametric sketches and are either additive (adding material) or subtractive (removing material).
|
||||
|
||||
FreeCAD 1.0 introduced significant improvements including **Topological Naming Problem (TNP) mitigation**, making parametric models more stable when earlier features are modified.
|
||||
|
||||
---
|
||||
|
||||
## Structure & Containers
|
||||
|
||||
### Body
|
||||
The fundamental container for PartDesign features. Defines a local coordinate system and contains all features that define a single solid component.
|
||||
|
||||
```python
|
||||
body = doc.addObject('PartDesign::Body', 'Body')
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `Tip` — The feature representing the current state of the body
|
||||
- `BaseFeature` — Optional external solid to build upon
|
||||
- `Origin` — Contains reference planes (XY, XZ, YZ) and axes (X, Y, Z)
|
||||
|
||||
### Part Container
|
||||
Groups multiple Bodies for organization. Not a PartDesign-specific object but commonly used.
|
||||
|
||||
```python
|
||||
part = doc.addObject('App::Part', 'Part')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sketch Tools
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **Create Sketch** | Creates a new sketch on a selected face or datum plane |
|
||||
| **Attach Sketch** | Attaches a sketch to geometry from the active body |
|
||||
| **Edit Sketch** | Opens selected sketch for editing |
|
||||
| **Validate Sketch** | Verifies tolerance of points and adjusts them |
|
||||
| **Check Geometry** | Checks geometry for errors |
|
||||
|
||||
```python
|
||||
# Create sketch attached to XY plane
|
||||
sketch = body.newObject('Sketcher::SketchObject', 'Sketch')
|
||||
sketch.AttachmentSupport = [(body.getObject('Origin').getObject('XY_Plane'), '')]
|
||||
sketch.MapMode = 'FlatFace'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Geometry (Datums)
|
||||
|
||||
### Datum Plane
|
||||
Creates a reference plane for sketch attachment or as a mirror/pattern reference.
|
||||
|
||||
```python
|
||||
plane = body.newObject('PartDesign::Plane', 'DatumPlane')
|
||||
plane.AttachmentSupport = [(face_reference, '')]
|
||||
plane.MapMode = 'FlatFace'
|
||||
plane.Offset = App.Vector(0, 0, 10) # Offset along normal
|
||||
```
|
||||
|
||||
### Datum Line
|
||||
Creates a reference axis for revolutions, grooves, or patterns.
|
||||
|
||||
```python
|
||||
line = body.newObject('PartDesign::Line', 'DatumLine')
|
||||
line.AttachmentSupport = [(edge_reference, '')]
|
||||
line.MapMode = 'ObjectXY'
|
||||
```
|
||||
|
||||
### Datum Point
|
||||
Creates a reference point for geometry attachment.
|
||||
|
||||
```python
|
||||
point = body.newObject('PartDesign::Point', 'DatumPoint')
|
||||
point.AttachmentSupport = [(vertex_reference, '')]
|
||||
```
|
||||
|
||||
### Local Coordinate System
|
||||
Creates a local coordinate system (LCS) attached to datum geometry.
|
||||
|
||||
```python
|
||||
lcs = body.newObject('PartDesign::CoordinateSystem', 'LocalCS')
|
||||
```
|
||||
|
||||
### Shape Binder
|
||||
References geometry from a single parent object.
|
||||
|
||||
```python
|
||||
binder = body.newObject('PartDesign::ShapeBinder', 'ShapeBinder')
|
||||
binder.Support = [(external_object, ['Face1'])]
|
||||
```
|
||||
|
||||
### SubShapeBinder
|
||||
References geometry from one or more parent objects (more flexible than ShapeBinder).
|
||||
|
||||
```python
|
||||
subbinder = body.newObject('PartDesign::SubShapeBinder', 'SubShapeBinder')
|
||||
subbinder.Support = [(obj1, ['Face1']), (obj2, ['Edge2'])]
|
||||
```
|
||||
|
||||
### Clone
|
||||
Creates a clone of a selected body.
|
||||
|
||||
```python
|
||||
clone = doc.addObject('PartDesign::FeatureBase', 'Clone')
|
||||
clone.BaseFeature = source_body
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additive Features (Add Material)
|
||||
|
||||
### Pad
|
||||
Extrudes a sketch profile to create a solid.
|
||||
|
||||
```python
|
||||
pad = body.newObject('PartDesign::Pad', 'Pad')
|
||||
pad.Profile = sketch
|
||||
pad.Length = 20.0
|
||||
pad.Type = 0 # 0=Dimension, 1=UpToLast, 2=UpToFirst, 3=UpToFace, 4=TwoLengths, 5=UpToShape
|
||||
pad.Reversed = False
|
||||
pad.Midplane = False
|
||||
pad.Symmetric = False
|
||||
pad.Length2 = 10.0 # For TwoLengths type
|
||||
pad.UseCustomVector = False
|
||||
pad.Direction = App.Vector(0, 0, 1)
|
||||
pad.TaperAngle = 0.0 # Draft angle (new in 1.0)
|
||||
pad.TaperAngle2 = 0.0
|
||||
```
|
||||
|
||||
**Type Options:**
|
||||
| Value | Mode | Description |
|
||||
|-------|------|-------------|
|
||||
| 0 | Dimension | Fixed length |
|
||||
| 1 | UpToLast | Extends to last face in direction |
|
||||
| 2 | UpToFirst | Extends to first face encountered |
|
||||
| 3 | UpToFace | Extends to selected face |
|
||||
| 4 | TwoLengths | Extends in both directions |
|
||||
| 5 | UpToShape | Extends to selected shape (new in 1.0) |
|
||||
|
||||
### Revolution
|
||||
Creates a solid by revolving a sketch around an axis.
|
||||
|
||||
```python
|
||||
revolution = body.newObject('PartDesign::Revolution', 'Revolution')
|
||||
revolution.Profile = sketch
|
||||
revolution.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
revolution.Angle = 360.0
|
||||
revolution.Midplane = False
|
||||
revolution.Reversed = False
|
||||
```
|
||||
|
||||
### Additive Loft
|
||||
Creates a solid by transitioning between two or more sketch profiles.
|
||||
|
||||
```python
|
||||
loft = body.newObject('PartDesign::AdditiveLoft', 'AdditiveLoft')
|
||||
loft.Profile = sketch1
|
||||
loft.Sections = [sketch2, sketch3]
|
||||
loft.Ruled = False
|
||||
loft.Closed = False
|
||||
```
|
||||
|
||||
### Additive Pipe (Sweep)
|
||||
Creates a solid by sweeping a profile along a path.
|
||||
|
||||
```python
|
||||
pipe = body.newObject('PartDesign::AdditivePipe', 'AdditivePipe')
|
||||
pipe.Profile = profile_sketch
|
||||
pipe.Spine = path_sketch # or (object, ['Edge1', 'Edge2'])
|
||||
pipe.Transition = 0 # 0=Transformed, 1=RightCorner, 2=RoundCorner
|
||||
pipe.Mode = 0 # 0=Standard, 1=Fixed, 2=Frenet, 3=Auxiliary
|
||||
pipe.Auxiliary = None # Auxiliary spine for Mode=3
|
||||
```
|
||||
|
||||
### Additive Helix
|
||||
Creates a solid by sweeping a sketch along a helix.
|
||||
|
||||
```python
|
||||
helix = body.newObject('PartDesign::AdditiveHelix', 'AdditiveHelix')
|
||||
helix.Profile = sketch
|
||||
helix.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
helix.Pitch = 5.0
|
||||
helix.Height = 30.0
|
||||
helix.Turns = 6.0
|
||||
helix.Mode = 0 # 0=pitch-height, 1=pitch-turns, 2=height-turns
|
||||
helix.LeftHanded = False
|
||||
helix.Reversed = False
|
||||
helix.Angle = 0.0 # Taper angle
|
||||
helix.Growth = 0.0 # Radius growth per turn
|
||||
```
|
||||
|
||||
### Additive Primitives
|
||||
Direct primitive creation without sketches.
|
||||
|
||||
```python
|
||||
# Box
|
||||
box = body.newObject('PartDesign::AdditiveBox', 'Box')
|
||||
box.Length = 10.0
|
||||
box.Width = 10.0
|
||||
box.Height = 10.0
|
||||
|
||||
# Cylinder
|
||||
cyl = body.newObject('PartDesign::AdditiveCylinder', 'Cylinder')
|
||||
cyl.Radius = 5.0
|
||||
cyl.Height = 20.0
|
||||
cyl.Angle = 360.0
|
||||
|
||||
# Sphere
|
||||
sphere = body.newObject('PartDesign::AdditiveSphere', 'Sphere')
|
||||
sphere.Radius = 10.0
|
||||
sphere.Angle1 = -90.0
|
||||
sphere.Angle2 = 90.0
|
||||
sphere.Angle3 = 360.0
|
||||
|
||||
# Cone
|
||||
cone = body.newObject('PartDesign::AdditiveCone', 'Cone')
|
||||
cone.Radius1 = 10.0
|
||||
cone.Radius2 = 5.0
|
||||
cone.Height = 15.0
|
||||
cone.Angle = 360.0
|
||||
|
||||
# Ellipsoid
|
||||
ellipsoid = body.newObject('PartDesign::AdditiveEllipsoid', 'Ellipsoid')
|
||||
ellipsoid.Radius1 = 10.0
|
||||
ellipsoid.Radius2 = 5.0
|
||||
ellipsoid.Radius3 = 8.0
|
||||
|
||||
# Torus
|
||||
torus = body.newObject('PartDesign::AdditiveTorus', 'Torus')
|
||||
torus.Radius1 = 20.0
|
||||
torus.Radius2 = 5.0
|
||||
|
||||
# Prism
|
||||
prism = body.newObject('PartDesign::AdditivePrism', 'Prism')
|
||||
prism.Polygon = 6
|
||||
prism.Circumradius = 10.0
|
||||
prism.Height = 20.0
|
||||
|
||||
# Wedge
|
||||
wedge = body.newObject('PartDesign::AdditiveWedge', 'Wedge')
|
||||
wedge.Xmin = 0.0
|
||||
wedge.Xmax = 10.0
|
||||
wedge.Ymin = 0.0
|
||||
wedge.Ymax = 10.0
|
||||
wedge.Zmin = 0.0
|
||||
wedge.Zmax = 10.0
|
||||
wedge.X2min = 2.0
|
||||
wedge.X2max = 8.0
|
||||
wedge.Z2min = 2.0
|
||||
wedge.Z2max = 8.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subtractive Features (Remove Material)
|
||||
|
||||
### Pocket
|
||||
Cuts material by extruding a sketch inward.
|
||||
|
||||
```python
|
||||
pocket = body.newObject('PartDesign::Pocket', 'Pocket')
|
||||
pocket.Profile = sketch
|
||||
pocket.Length = 15.0
|
||||
pocket.Type = 0 # Same options as Pad, plus 1=ThroughAll
|
||||
pocket.Reversed = False
|
||||
pocket.Midplane = False
|
||||
pocket.Symmetric = False
|
||||
pocket.TaperAngle = 0.0
|
||||
```
|
||||
|
||||
### Hole
|
||||
Creates parametric holes with threading options.
|
||||
|
||||
```python
|
||||
hole = body.newObject('PartDesign::Hole', 'Hole')
|
||||
hole.Profile = sketch # Sketch with center points
|
||||
hole.Diameter = 6.0
|
||||
hole.Depth = 15.0
|
||||
hole.DepthType = 0 # 0=Dimension, 1=ThroughAll
|
||||
hole.Threaded = True
|
||||
hole.ThreadType = 0 # 0=None, 1=ISOMetricCoarse, 2=ISOMetricFine, 3=UNC, 4=UNF, 5=NPT, etc.
|
||||
hole.ThreadSize = 'M6'
|
||||
hole.ThreadFit = 0 # 0=Standard, 1=Close
|
||||
hole.ThreadDirection = 0 # 0=Right, 1=Left
|
||||
hole.HoleCutType = 0 # 0=None, 1=Counterbore, 2=Countersink
|
||||
hole.HoleCutDiameter = 10.0
|
||||
hole.HoleCutDepth = 3.0
|
||||
hole.HoleCutCountersinkAngle = 90.0
|
||||
hole.DrillPoint = 0 # 0=Flat, 1=Angled
|
||||
hole.DrillPointAngle = 118.0
|
||||
hole.DrillForDepth = False
|
||||
```
|
||||
|
||||
**Thread Types:**
|
||||
- ISO Metric Coarse/Fine
|
||||
- UNC/UNF (Unified National)
|
||||
- NPT/NPTF (National Pipe Thread)
|
||||
- BSW/BSF (British Standard)
|
||||
- UTS (Unified Thread Standard)
|
||||
|
||||
### Groove
|
||||
Creates a cut by revolving a sketch around an axis (subtractive revolution).
|
||||
|
||||
```python
|
||||
groove = body.newObject('PartDesign::Groove', 'Groove')
|
||||
groove.Profile = sketch
|
||||
groove.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
groove.Angle = 360.0
|
||||
groove.Midplane = False
|
||||
groove.Reversed = False
|
||||
```
|
||||
|
||||
### Subtractive Loft
|
||||
Cuts by transitioning between profiles.
|
||||
|
||||
```python
|
||||
subloft = body.newObject('PartDesign::SubtractiveLoft', 'SubtractiveLoft')
|
||||
subloft.Profile = sketch1
|
||||
subloft.Sections = [sketch2]
|
||||
```
|
||||
|
||||
### Subtractive Pipe
|
||||
Cuts by sweeping a profile along a path.
|
||||
|
||||
```python
|
||||
subpipe = body.newObject('PartDesign::SubtractivePipe', 'SubtractivePipe')
|
||||
subpipe.Profile = profile_sketch
|
||||
subpipe.Spine = path_sketch
|
||||
```
|
||||
|
||||
### Subtractive Helix
|
||||
Cuts by sweeping along a helix (e.g., for threads).
|
||||
|
||||
```python
|
||||
subhelix = body.newObject('PartDesign::SubtractiveHelix', 'SubtractiveHelix')
|
||||
subhelix.Profile = thread_profile_sketch
|
||||
subhelix.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
subhelix.Pitch = 1.0
|
||||
subhelix.Height = 10.0
|
||||
```
|
||||
|
||||
### Subtractive Primitives
|
||||
Same primitives as additive, but subtract material:
|
||||
- `PartDesign::SubtractiveBox`
|
||||
- `PartDesign::SubtractiveCylinder`
|
||||
- `PartDesign::SubtractiveSphere`
|
||||
- `PartDesign::SubtractiveCone`
|
||||
- `PartDesign::SubtractiveEllipsoid`
|
||||
- `PartDesign::SubtractiveTorus`
|
||||
- `PartDesign::SubtractivePrism`
|
||||
- `PartDesign::SubtractiveWedge`
|
||||
|
||||
---
|
||||
|
||||
## Transformation Features (Patterns)
|
||||
|
||||
### Mirrored
|
||||
Creates a mirror copy of features across a plane.
|
||||
|
||||
```python
|
||||
mirrored = body.newObject('PartDesign::Mirrored', 'Mirrored')
|
||||
mirrored.Originals = [pad, pocket]
|
||||
mirrored.MirrorPlane = (body.getObject('Origin').getObject('XZ_Plane'), [''])
|
||||
```
|
||||
|
||||
### Linear Pattern
|
||||
Creates copies in a linear arrangement.
|
||||
|
||||
```python
|
||||
linear = body.newObject('PartDesign::LinearPattern', 'LinearPattern')
|
||||
linear.Originals = [pocket]
|
||||
linear.Direction = (body.getObject('Origin').getObject('X_Axis'), [''])
|
||||
linear.Length = 100.0
|
||||
linear.Occurrences = 5
|
||||
linear.Mode = 0 # 0=OverallLength, 1=Offset
|
||||
```
|
||||
|
||||
### Polar Pattern
|
||||
Creates copies in a circular arrangement.
|
||||
|
||||
```python
|
||||
polar = body.newObject('PartDesign::PolarPattern', 'PolarPattern')
|
||||
polar.Originals = [pocket]
|
||||
polar.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
polar.Angle = 360.0
|
||||
polar.Occurrences = 6
|
||||
polar.Mode = 0 # 0=OverallAngle, 1=Offset
|
||||
```
|
||||
|
||||
### MultiTransform
|
||||
Combines multiple transformations (mirrored, linear, polar, scaled).
|
||||
|
||||
```python
|
||||
multi = body.newObject('PartDesign::MultiTransform', 'MultiTransform')
|
||||
multi.Originals = [pocket]
|
||||
|
||||
# Add transformations (created within MultiTransform)
|
||||
# Typically done via GUI or by setting Transformations property
|
||||
multi.Transformations = [mirrored_transform, linear_transform]
|
||||
```
|
||||
|
||||
### Scaled
|
||||
Scales features (only available within MultiTransform).
|
||||
|
||||
```python
|
||||
# Only accessible as part of MultiTransform
|
||||
scaled = body.newObject('PartDesign::Scaled', 'Scaled')
|
||||
scaled.Factor = 0.5
|
||||
scaled.Occurrences = 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dress-Up Features (Edge/Face Treatment)
|
||||
|
||||
### Fillet
|
||||
Rounds edges with a specified radius.
|
||||
|
||||
```python
|
||||
fillet = body.newObject('PartDesign::Fillet', 'Fillet')
|
||||
fillet.Base = (pad, ['Edge1', 'Edge5', 'Edge9'])
|
||||
fillet.Radius = 2.0
|
||||
```
|
||||
|
||||
### Chamfer
|
||||
Bevels edges.
|
||||
|
||||
```python
|
||||
chamfer = body.newObject('PartDesign::Chamfer', 'Chamfer')
|
||||
chamfer.Base = (pad, ['Edge2', 'Edge6'])
|
||||
chamfer.ChamferType = 'Equal Distance' # or 'Two Distances' or 'Distance and Angle'
|
||||
chamfer.Size = 1.5
|
||||
chamfer.Size2 = 2.0 # For asymmetric
|
||||
chamfer.Angle = 45.0 # For 'Distance and Angle'
|
||||
```
|
||||
|
||||
### Draft
|
||||
Applies angular draft to faces (for mold release).
|
||||
|
||||
```python
|
||||
draft = body.newObject('PartDesign::Draft', 'Draft')
|
||||
draft.Base = (pad, ['Face2', 'Face4'])
|
||||
draft.Angle = 3.0 # Degrees
|
||||
draft.NeutralPlane = (body.getObject('Origin').getObject('XY_Plane'), [''])
|
||||
draft.PullDirection = App.Vector(0, 0, 1)
|
||||
draft.Reversed = False
|
||||
```
|
||||
|
||||
### Thickness
|
||||
Creates a shell by hollowing out a solid, keeping selected faces open.
|
||||
|
||||
```python
|
||||
thickness = body.newObject('PartDesign::Thickness', 'Thickness')
|
||||
thickness.Base = (pad, ['Face6']) # Faces to remove (open)
|
||||
thickness.Value = 2.0 # Wall thickness
|
||||
thickness.Mode = 0 # 0=Skin, 1=Pipe, 2=RectoVerso
|
||||
thickness.Join = 0 # 0=Arc, 1=Intersection
|
||||
thickness.Reversed = False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Boolean Operations
|
||||
|
||||
### Boolean
|
||||
Imports bodies and applies boolean operations.
|
||||
|
||||
```python
|
||||
boolean = body.newObject('PartDesign::Boolean', 'Boolean')
|
||||
boolean.Type = 0 # 0=Fuse, 1=Cut, 2=Common (intersection)
|
||||
boolean.Bodies = [other_body1, other_body2]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Menu Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **Set Tip** | Sets selected feature as the body's current state (tip) |
|
||||
| **Move object to other body** | Transfers feature to a different body |
|
||||
| **Move object after other object** | Reorders features in the tree |
|
||||
| **Appearance** | Sets color and transparency |
|
||||
| **Color per face** | Assigns different colors to individual faces |
|
||||
|
||||
```python
|
||||
# Set tip programmatically
|
||||
body.Tip = pocket
|
||||
|
||||
# Move feature order
|
||||
doc.moveObject(feature, body, after_feature)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Tools
|
||||
|
||||
### Sprocket
|
||||
Creates a sprocket profile for chain drives.
|
||||
|
||||
```python
|
||||
# Available via Gui.runCommand('PartDesign_Sprocket')
|
||||
```
|
||||
|
||||
### Involute Gear
|
||||
Creates an involute gear profile.
|
||||
|
||||
```python
|
||||
# Available via Gui.runCommand('PartDesign_InvoluteGear')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Properties (All Features)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Label` | String | User-visible name |
|
||||
| `Placement` | Placement | Position and orientation |
|
||||
| `BaseFeature` | Link | Feature this builds upon |
|
||||
| `Shape` | Shape | Resulting geometry |
|
||||
|
||||
---
|
||||
|
||||
## Expression Binding
|
||||
|
||||
All dimensional properties can be driven by expressions:
|
||||
|
||||
```python
|
||||
pad.setExpression('Length', 'Spreadsheet.plate_height')
|
||||
fillet.setExpression('Radius', 'Spreadsheet.fillet_r * 0.5')
|
||||
hole.setExpression('Diameter', '<<Parameters>>.hole_dia')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always work within a Body** — PartDesign features require a body container
|
||||
2. **Use fully constrained sketches** — Prevents unexpected behavior when parameters change
|
||||
3. **Leverage datum geometry** — Creates stable references that survive TNP issues
|
||||
4. **Name constraints** — Enables expression-based parametric design
|
||||
5. **Use spreadsheets** — Centralizes parameters for easy modification
|
||||
6. **Set meaningful Labels** — Internal Names are auto-generated; Labels are user-friendly
|
||||
7. **Check isSolid()** — Before subtractive operations, verify the body has solid geometry
|
||||
|
||||
```python
|
||||
if not body.isSolid():
|
||||
raise ValueError("Body must contain solid geometry for subtractive features")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FreeCAD 1.0 Changes
|
||||
|
||||
| Change | Description |
|
||||
|--------|-------------|
|
||||
| **TNP Mitigation** | Topological naming more stable |
|
||||
| **UpToShape** | New Pad/Pocket type extending to arbitrary shapes |
|
||||
| **Draft Angle** | Taper angles on Pad/Pocket |
|
||||
| **Improved Hole** | More thread types, better UI |
|
||||
| **Assembly Integration** | Native assembly workbench |
|
||||
| **Arch → BIM** | Workbench rename |
|
||||
| **Path → CAM** | Workbench rename |
|
||||
|
||||
---
|
||||
|
||||
## Python Module Access
|
||||
|
||||
```python
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
import Sketcher
|
||||
import PartDesign
|
||||
import PartDesignGui
|
||||
|
||||
# Access feature classes
|
||||
print(dir(PartDesign))
|
||||
# ['Additive', 'AdditiveBox', 'AdditiveCone', 'AdditiveCylinder', ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document version: FreeCAD 1.0.2 / January 2026*
|
||||
*Reference: FreeCAD Wiki, GitHub FreeCAD-documentation, FreeCAD Forum*
|
||||
12
reference/ztools/ztools/Init.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# ztools Addon Initialization
|
||||
# This file runs at FreeCAD startup (before GUI)
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
# The Catppuccin Mocha theme is now provided as a Preference Pack.
|
||||
# It will be automatically available in:
|
||||
# Edit > Preferences > General > Preference packs > CatppuccinMocha
|
||||
#
|
||||
# No manual installation is required - FreeCAD's addon system handles it.
|
||||
|
||||
App.Console.PrintLog("ztools addon loaded\n")
|
||||
367
reference/ztools/ztools/InitGui.py
Normal file
@@ -0,0 +1,367 @@
|
||||
# ztools Workbench for FreeCAD 1.0+
|
||||
# Extended PartDesign replacement with velocity-focused tools
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
|
||||
|
||||
class ZToolsWorkbench(Gui.Workbench):
|
||||
"""Extended PartDesign workbench with velocity-focused tools."""
|
||||
|
||||
MenuText = "ztools"
|
||||
ToolTip = "Extended PartDesign replacement for faster CAD workflows"
|
||||
|
||||
# Catppuccin Mocha themed icon
|
||||
Icon = """
|
||||
/* XPM */
|
||||
static char * ztools_xpm[] = {
|
||||
"16 16 5 1",
|
||||
" c None",
|
||||
". c #313244",
|
||||
"+ c #cba6f7",
|
||||
"@ c #94e2d5",
|
||||
"# c #45475a",
|
||||
" ",
|
||||
" ############ ",
|
||||
" #..........# ",
|
||||
" #.++++++++.# ",
|
||||
" #.+......+.# ",
|
||||
" #.....+++..# ",
|
||||
" #....++....# ",
|
||||
" #...++.....# ",
|
||||
" #..++......# ",
|
||||
" #.++.......# ",
|
||||
" #.++++++++@# ",
|
||||
" #..........# ",
|
||||
" ############ ",
|
||||
" ",
|
||||
" ",
|
||||
" "};
|
||||
"""
|
||||
|
||||
def Initialize(self):
|
||||
"""Called on workbench first activation."""
|
||||
# Load PartDesign and Sketcher workbenches to register their commands
|
||||
# We need to actually activate them briefly to ensure commands are registered
|
||||
# Activate dependent workbenches so their commands are registered.
|
||||
# Use activateWorkbench() instead of calling Initialize() directly,
|
||||
# since direct calls skip the C++ __Workbench__ injection step.
|
||||
# Wrap each individually so one failure doesn't block the rest.
|
||||
wb_list = Gui.listWorkbenches()
|
||||
current_wb = Gui.activeWorkbench()
|
||||
for wb_name in (
|
||||
"PartDesignWorkbench",
|
||||
"SketcherWorkbench",
|
||||
"AssemblyWorkbench",
|
||||
"SpreadsheetWorkbench",
|
||||
):
|
||||
if wb_name in wb_list:
|
||||
try:
|
||||
Gui.activateWorkbench(wb_name)
|
||||
except Exception as e:
|
||||
App.Console.PrintWarning(f"Could not initialize {wb_name}: {e}\n")
|
||||
# Restore ztools as the active workbench
|
||||
try:
|
||||
Gui.activateWorkbench("ZToolsWorkbench")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Command imports moved to module scope (after Gui.addWorkbench) so they
|
||||
# are available before Initialize() runs. See end of file.
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Structure Tools
|
||||
# =====================================================================
|
||||
self.structure_tools = [
|
||||
"PartDesign_Body",
|
||||
"PartDesign_NewSketch",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Reference Geometry (Datums)
|
||||
# =====================================================================
|
||||
self.partdesign_datum_tools = [
|
||||
"PartDesign_Plane",
|
||||
"PartDesign_Line",
|
||||
"PartDesign_Point",
|
||||
"PartDesign_CoordinateSystem",
|
||||
"PartDesign_ShapeBinder",
|
||||
"PartDesign_SubShapeBinder",
|
||||
"PartDesign_Clone",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Additive Features
|
||||
# =====================================================================
|
||||
self.additive_tools = [
|
||||
"PartDesign_Pad",
|
||||
"PartDesign_Revolution",
|
||||
"PartDesign_AdditiveLoft",
|
||||
"PartDesign_AdditivePipe",
|
||||
"PartDesign_AdditiveHelix",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Additive Primitives (compound command with dropdown)
|
||||
# =====================================================================
|
||||
self.additive_primitives = [
|
||||
"PartDesign_CompPrimitiveAdditive",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Subtractive Features
|
||||
# =====================================================================
|
||||
self.subtractive_tools = [
|
||||
"PartDesign_Pocket",
|
||||
"PartDesign_Hole",
|
||||
"PartDesign_Groove",
|
||||
"PartDesign_SubtractiveLoft",
|
||||
"PartDesign_SubtractivePipe",
|
||||
"PartDesign_SubtractiveHelix",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Subtractive Primitives (compound command with dropdown)
|
||||
# =====================================================================
|
||||
self.subtractive_primitives = [
|
||||
"PartDesign_CompPrimitiveSubtractive",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Transformation Features (Patterns)
|
||||
# =====================================================================
|
||||
self.transformation_tools = [
|
||||
"PartDesign_Mirrored",
|
||||
"PartDesign_LinearPattern",
|
||||
"PartDesign_PolarPattern",
|
||||
"PartDesign_MultiTransform",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Dress-Up Features
|
||||
# =====================================================================
|
||||
self.dressup_tools = [
|
||||
"PartDesign_Fillet",
|
||||
"PartDesign_Chamfer",
|
||||
"PartDesign_Draft",
|
||||
"PartDesign_Thickness",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Boolean Operations
|
||||
# =====================================================================
|
||||
self.boolean_tools = [
|
||||
"PartDesign_Boolean",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Sketcher Tools (commonly used with PartDesign)
|
||||
# =====================================================================
|
||||
self.sketcher_tools = [
|
||||
"Sketcher_NewSketch",
|
||||
"Sketcher_EditSketch",
|
||||
"Sketcher_MapSketch",
|
||||
"Sketcher_ValidateSketch",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# ZTools Custom Tools
|
||||
# =====================================================================
|
||||
self.ztools_datum_tools = [
|
||||
"ZTools_DatumCreator",
|
||||
"ZTools_DatumManager",
|
||||
]
|
||||
|
||||
self.ztools_pattern_tools = [
|
||||
"ZTools_RotatedLinearPattern",
|
||||
]
|
||||
|
||||
self.ztools_pocket_tools = [
|
||||
"ZTools_EnhancedPocket",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Assembly Workbench Tools (FreeCAD 1.0+)
|
||||
# =====================================================================
|
||||
self.assembly_structure_tools = [
|
||||
"Assembly_CreateAssembly",
|
||||
"Assembly_InsertLink",
|
||||
"Assembly_InsertNewPart",
|
||||
]
|
||||
|
||||
self.assembly_joint_tools = [
|
||||
"Assembly_CreateJointFixed",
|
||||
"Assembly_CreateJointRevolute",
|
||||
"Assembly_CreateJointCylindrical",
|
||||
"Assembly_CreateJointSlider",
|
||||
"Assembly_CreateJointBall",
|
||||
"Assembly_CreateJointDistance",
|
||||
"Assembly_CreateJointParallel",
|
||||
"Assembly_CreateJointPerpendicular",
|
||||
"Assembly_CreateJointAngle",
|
||||
"Assembly_CreateJointRackPinion",
|
||||
"Assembly_CreateJointScrew",
|
||||
"Assembly_CreateJointGears",
|
||||
"Assembly_CreateJointBelt",
|
||||
]
|
||||
|
||||
self.assembly_management_tools = [
|
||||
"Assembly_ToggleGrounded",
|
||||
"Assembly_SolveAssembly",
|
||||
"Assembly_CreateView",
|
||||
"Assembly_CreateBom",
|
||||
"Assembly_ExportASMT",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# ZTools Assembly Pattern Tools
|
||||
# =====================================================================
|
||||
self.ztools_assembly_tools = [
|
||||
"ZTools_AssemblyLinearPattern",
|
||||
"ZTools_AssemblyPolarPattern",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Spreadsheet Workbench Tools
|
||||
# =====================================================================
|
||||
self.spreadsheet_tools = [
|
||||
"Spreadsheet_CreateSheet",
|
||||
"Spreadsheet_Import",
|
||||
"Spreadsheet_Export",
|
||||
"Spreadsheet_SetAlias",
|
||||
"Spreadsheet_MergeCells",
|
||||
"Spreadsheet_SplitCell",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# ZTools Spreadsheet Formatting Tools
|
||||
# =====================================================================
|
||||
self.ztools_spreadsheet_tools = [
|
||||
"ZTools_SpreadsheetStyleBold",
|
||||
"ZTools_SpreadsheetStyleItalic",
|
||||
"ZTools_SpreadsheetStyleUnderline",
|
||||
"ZTools_SpreadsheetAlignLeft",
|
||||
"ZTools_SpreadsheetAlignCenter",
|
||||
"ZTools_SpreadsheetAlignRight",
|
||||
"ZTools_SpreadsheetBgColor",
|
||||
"ZTools_SpreadsheetTextColor",
|
||||
"ZTools_SpreadsheetQuickAlias",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Append Toolbars
|
||||
# =====================================================================
|
||||
self.appendToolbar("Structure", self.structure_tools)
|
||||
self.appendToolbar("Sketcher", self.sketcher_tools)
|
||||
self.appendToolbar("Datums", self.partdesign_datum_tools)
|
||||
self.appendToolbar("Additive", self.additive_tools + self.additive_primitives)
|
||||
self.appendToolbar(
|
||||
"Subtractive", self.subtractive_tools + self.subtractive_primitives
|
||||
)
|
||||
self.appendToolbar("Transformations", self.transformation_tools)
|
||||
self.appendToolbar("Dress-Up", self.dressup_tools)
|
||||
self.appendToolbar("Boolean", self.boolean_tools)
|
||||
self.appendToolbar("Assembly", self.assembly_structure_tools)
|
||||
self.appendToolbar("Assembly Joints", self.assembly_joint_tools)
|
||||
self.appendToolbar("Assembly Management", self.assembly_management_tools)
|
||||
self.appendToolbar("ztools Datums", self.ztools_datum_tools)
|
||||
self.appendToolbar("ztools Patterns", self.ztools_pattern_tools)
|
||||
self.appendToolbar("ztools Features", self.ztools_pocket_tools)
|
||||
self.appendToolbar("ztools Assembly", self.ztools_assembly_tools)
|
||||
self.appendToolbar("Spreadsheet", self.spreadsheet_tools)
|
||||
self.appendToolbar("ztools Spreadsheet", self.ztools_spreadsheet_tools)
|
||||
|
||||
# =====================================================================
|
||||
# Append Menus
|
||||
# =====================================================================
|
||||
self.appendMenu("Structure", self.structure_tools)
|
||||
self.appendMenu("Sketch", self.sketcher_tools)
|
||||
self.appendMenu(["PartDesign", "Datums"], self.partdesign_datum_tools)
|
||||
self.appendMenu(
|
||||
["PartDesign", "Additive"], self.additive_tools + self.additive_primitives
|
||||
)
|
||||
self.appendMenu(
|
||||
["PartDesign", "Subtractive"],
|
||||
self.subtractive_tools + self.subtractive_primitives,
|
||||
)
|
||||
self.appendMenu(["PartDesign", "Transformations"], self.transformation_tools)
|
||||
self.appendMenu(["PartDesign", "Dress-Up"], self.dressup_tools)
|
||||
self.appendMenu(["PartDesign", "Boolean"], self.boolean_tools)
|
||||
self.appendMenu(["Assembly", "Structure"], self.assembly_structure_tools)
|
||||
self.appendMenu(["Assembly", "Joints"], self.assembly_joint_tools)
|
||||
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(
|
||||
"ztools",
|
||||
self.ztools_datum_tools
|
||||
+ self.ztools_pattern_tools
|
||||
+ self.ztools_pocket_tools
|
||||
+ self.ztools_assembly_tools
|
||||
+ self.ztools_spreadsheet_tools,
|
||||
)
|
||||
|
||||
App.Console.PrintMessage("ztools workbench initialized\n")
|
||||
|
||||
def Activated(self):
|
||||
"""Called when workbench is activated."""
|
||||
App.Console.PrintMessage("ztools workbench activated\n")
|
||||
|
||||
def Deactivated(self):
|
||||
"""Called when workbench is deactivated."""
|
||||
pass
|
||||
|
||||
def GetClassName(self):
|
||||
return "Gui::PythonWorkbench"
|
||||
|
||||
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _ZToolsPartDesignManipulator:
|
||||
"""Adds ZTools commands to PartDesign toolbars and menus."""
|
||||
|
||||
def modifyToolBars(self):
|
||||
return [
|
||||
{"append": "ZTools_DatumCreator", "toolBar": "Part Design Helper Features"},
|
||||
{"append": "ZTools_DatumManager", "toolBar": "Part Design Helper Features"},
|
||||
{
|
||||
"append": "ZTools_EnhancedPocket",
|
||||
"toolBar": "Part Design Modeling Features",
|
||||
},
|
||||
{
|
||||
"append": "ZTools_RotatedLinearPattern",
|
||||
"toolBar": "Part Design Transformation Features",
|
||||
},
|
||||
]
|
||||
|
||||
def modifyMenuBar(self):
|
||||
return [
|
||||
{"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())
|
||||
123
reference/ztools/ztools/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ztools - Extended PartDesign for FreeCAD
|
||||
|
||||
Velocity-focused CAD tools extending FreeCAD 1.0+ PartDesign workbench.
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Copy the `ztools` folder to your FreeCAD Mod directory:
|
||||
- **Linux**: `~/.local/share/FreeCAD/Mod/ztools/`
|
||||
- **Windows**: `%APPDATA%\FreeCAD\Mod\ztools\`
|
||||
- **macOS**: `~/Library/Application Support/FreeCAD/Mod/ztools/`
|
||||
|
||||
2. Restart FreeCAD
|
||||
|
||||
3. Select **ztools** from the workbench dropdown
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ztools/
|
||||
├── InitGui.py # Workbench registration
|
||||
├── ztools/
|
||||
│ ├── __init__.py
|
||||
│ ├── datums/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── core.py # Datum creation functions
|
||||
│ └── commands/
|
||||
│ ├── __init__.py
|
||||
│ └── datum_commands.py # GUI commands
|
||||
├── setup.cfg
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Module 1: Datum Tools
|
||||
|
||||
### Datum Creator (GUI)
|
||||
|
||||
Unified task panel for creating:
|
||||
|
||||
**Planes**
|
||||
- Offset from Face
|
||||
- Midplane (2 parallel faces)
|
||||
- 3 Points
|
||||
- Normal to Edge (at parameter)
|
||||
- Angled from Face (about edge)
|
||||
- Tangent to Cylinder
|
||||
|
||||
**Axes**
|
||||
- 2 Points
|
||||
- From Edge
|
||||
- Cylinder Center
|
||||
- Plane Intersection
|
||||
|
||||
**Points**
|
||||
- At Vertex
|
||||
- XYZ Coordinates
|
||||
- On Edge (at parameter)
|
||||
- Face Center
|
||||
- Circle Center
|
||||
|
||||
### Options
|
||||
|
||||
- **Link to Spreadsheet**: Creates aliases in Spreadsheet for parametric control
|
||||
- **Add to Active Body**: Creates PartDesign datums vs Part geometry
|
||||
- **Custom Name**: Override auto-naming (e.g., `ZPlane_Offset_001`)
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from ztools.datums import (
|
||||
plane_offset_from_face,
|
||||
plane_midplane,
|
||||
axis_cylinder_center,
|
||||
point_at_coordinates,
|
||||
)
|
||||
|
||||
doc = App.ActiveDocument
|
||||
body = doc.getObject('Body')
|
||||
|
||||
# Offset plane from selected face
|
||||
face = body.Shape.Faces[0]
|
||||
plane = plane_offset_from_face(face, 15.0, body=body, link_spreadsheet=True)
|
||||
|
||||
# Midplane between two faces
|
||||
mid = plane_midplane(face1, face2, name="MidPlane_Custom")
|
||||
|
||||
# Axis at cylinder center
|
||||
cyl_face = body.Shape.Faces[2]
|
||||
axis = axis_cylinder_center(cyl_face, body=body)
|
||||
|
||||
# Point at coordinates with spreadsheet link
|
||||
pt = point_at_coordinates(50, 25, 0, link_spreadsheet=True)
|
||||
```
|
||||
|
||||
### Metadata
|
||||
|
||||
All ztools datums store creation metadata in custom properties:
|
||||
|
||||
- `ZTools_Type`: Creation method (e.g., "offset_from_face")
|
||||
- `ZTools_Params`: JSON-encoded parameters
|
||||
|
||||
Access via:
|
||||
```python
|
||||
plane = doc.getObject('ZPlane_Offset_001')
|
||||
print(plane.ZTools_Type) # "offset_from_face"
|
||||
print(plane.ZTools_Params) # {"distance": 15.0, ...}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] **Module 2**: Enhanced Pad/Pocket (multi-body, draft angles, lip/groove)
|
||||
- [ ] **Module 3**: Body operations (split, combine, shell improvements)
|
||||
- [ ] **Module 4**: Pattern tools (curve-driven, fill patterns)
|
||||
- [ ] **Datum Manager**: Panel to list/toggle/rename all datums
|
||||
|
||||
## License
|
||||
|
||||
LGPL-2.1 (same as FreeCAD)
|
||||
|
||||
## Contributing
|
||||
|
||||
Kindred Systems LLC - Kansas City
|
||||
11
reference/ztools/ztools/setup.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
[metadata]
|
||||
name = ztools
|
||||
version = 0.1.0
|
||||
description = Extended PartDesign workbench for FreeCAD with velocity-focused tools
|
||||
author = Kindred Systems LLC
|
||||
license = LGPL-2.1
|
||||
url = https://github.com/kindredsystems/ztools
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.8
|
||||
2
reference/ztools/ztools/ztools/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# ztools - Extended PartDesign for FreeCAD
|
||||
__version__ = "0.1.0"
|
||||
18
reference/ztools/ztools/ztools/commands/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# ztools/commands - GUI commands
|
||||
from . import (
|
||||
assembly_pattern_commands,
|
||||
datum_commands,
|
||||
datum_viewprovider,
|
||||
pattern_commands,
|
||||
pocket_commands,
|
||||
spreadsheet_commands,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"datum_commands",
|
||||
"datum_viewprovider",
|
||||
"pattern_commands",
|
||||
"pocket_commands",
|
||||
"assembly_pattern_commands",
|
||||
"spreadsheet_commands",
|
||||
]
|
||||
@@ -0,0 +1,787 @@
|
||||
# ztools/commands/assembly_pattern_commands.py
|
||||
# Assembly patterning tools for FreeCAD 1.0+ Assembly workbench
|
||||
# Creates linear and polar patterns of assembly components
|
||||
|
||||
import math
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
# =============================================================================
|
||||
# Assembly Linear Pattern
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AssemblyLinearPatternTaskPanel:
|
||||
"""Task panel for creating linear patterns of assembly components."""
|
||||
|
||||
def __init__(self):
|
||||
self.form = QtGui.QWidget()
|
||||
self.setup_ui()
|
||||
self.setup_selection_observer()
|
||||
self.selected_components = []
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Component selection section
|
||||
component_group = QtGui.QGroupBox("Components")
|
||||
component_layout = QtGui.QVBoxLayout(component_group)
|
||||
|
||||
# Selection table
|
||||
self.component_table = QtGui.QTableWidget()
|
||||
self.component_table.setColumnCount(2)
|
||||
self.component_table.setHorizontalHeaderLabels(["Component", "Remove"])
|
||||
self.component_table.horizontalHeader().setStretchLastSection(False)
|
||||
self.component_table.horizontalHeader().setSectionResizeMode(
|
||||
0, QtGui.QHeaderView.Stretch
|
||||
)
|
||||
self.component_table.horizontalHeader().setSectionResizeMode(
|
||||
1, QtGui.QHeaderView.Fixed
|
||||
)
|
||||
self.component_table.setColumnWidth(1, 60)
|
||||
self.component_table.setMaximumHeight(120)
|
||||
component_layout.addWidget(self.component_table)
|
||||
|
||||
# Selection hint
|
||||
hint_label = QtGui.QLabel("Select assembly components in the 3D view")
|
||||
hint_label.setStyleSheet("color: gray; font-style: italic;")
|
||||
component_layout.addWidget(hint_label)
|
||||
|
||||
layout.addWidget(component_group)
|
||||
|
||||
# Pattern parameters section
|
||||
params_group = QtGui.QGroupBox("Pattern Parameters")
|
||||
params_layout = QtGui.QFormLayout(params_group)
|
||||
|
||||
# Direction
|
||||
direction_layout = QtGui.QHBoxLayout()
|
||||
self.dir_x_spin = QtGui.QDoubleSpinBox()
|
||||
self.dir_x_spin.setRange(-1000, 1000)
|
||||
self.dir_x_spin.setValue(1)
|
||||
self.dir_x_spin.setDecimals(3)
|
||||
self.dir_y_spin = QtGui.QDoubleSpinBox()
|
||||
self.dir_y_spin.setRange(-1000, 1000)
|
||||
self.dir_y_spin.setValue(0)
|
||||
self.dir_y_spin.setDecimals(3)
|
||||
self.dir_z_spin = QtGui.QDoubleSpinBox()
|
||||
self.dir_z_spin.setRange(-1000, 1000)
|
||||
self.dir_z_spin.setValue(0)
|
||||
self.dir_z_spin.setDecimals(3)
|
||||
direction_layout.addWidget(QtGui.QLabel("X:"))
|
||||
direction_layout.addWidget(self.dir_x_spin)
|
||||
direction_layout.addWidget(QtGui.QLabel("Y:"))
|
||||
direction_layout.addWidget(self.dir_y_spin)
|
||||
direction_layout.addWidget(QtGui.QLabel("Z:"))
|
||||
direction_layout.addWidget(self.dir_z_spin)
|
||||
params_layout.addRow("Direction:", direction_layout)
|
||||
|
||||
# Occurrences
|
||||
self.occurrences_spin = QtGui.QSpinBox()
|
||||
self.occurrences_spin.setRange(2, 100)
|
||||
self.occurrences_spin.setValue(3)
|
||||
params_layout.addRow("Occurrences:", self.occurrences_spin)
|
||||
|
||||
# Spacing mode
|
||||
self.spacing_mode = QtGui.QComboBox()
|
||||
self.spacing_mode.addItems(["Total Length", "Spacing"])
|
||||
self.spacing_mode.currentIndexChanged.connect(self.on_spacing_mode_changed)
|
||||
params_layout.addRow("Mode:", self.spacing_mode)
|
||||
|
||||
# Length/Spacing value
|
||||
self.length_spin = QtGui.QDoubleSpinBox()
|
||||
self.length_spin.setRange(0.001, 100000)
|
||||
self.length_spin.setValue(100)
|
||||
self.length_spin.setDecimals(3)
|
||||
self.length_spin.setSuffix(" mm")
|
||||
self.length_label = QtGui.QLabel("Total Length:")
|
||||
params_layout.addRow(self.length_label, self.length_spin)
|
||||
|
||||
layout.addWidget(params_group)
|
||||
|
||||
# Options section
|
||||
options_group = QtGui.QGroupBox("Options")
|
||||
options_layout = QtGui.QVBoxLayout(options_group)
|
||||
|
||||
self.link_checkbox = QtGui.QCheckBox("Create as Links (recommended)")
|
||||
self.link_checkbox.setChecked(True)
|
||||
self.link_checkbox.setToolTip(
|
||||
"Links reference the original component, reducing file size"
|
||||
)
|
||||
options_layout.addWidget(self.link_checkbox)
|
||||
|
||||
self.hide_original_checkbox = QtGui.QCheckBox("Hide original components")
|
||||
self.hide_original_checkbox.setChecked(False)
|
||||
options_layout.addWidget(self.hide_original_checkbox)
|
||||
|
||||
layout.addWidget(options_group)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Set up observer to track selection changes."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_selection_added(doc, obj, sub)
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_selection_removed(doc, obj, sub)
|
||||
|
||||
def clearSelection(self, doc):
|
||||
pass
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_selection_added(self, doc_name, obj_name, sub):
|
||||
"""Handle new selection."""
|
||||
doc = App.getDocument(doc_name)
|
||||
if not doc:
|
||||
return
|
||||
|
||||
obj = doc.getObject(obj_name)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
# Check if this is an assembly component (App::Link or Part)
|
||||
if not self._is_valid_component(obj):
|
||||
return
|
||||
|
||||
# Avoid duplicates
|
||||
if obj in self.selected_components:
|
||||
return
|
||||
|
||||
self.selected_components.append(obj)
|
||||
self._update_table()
|
||||
|
||||
def on_selection_removed(self, doc_name, obj_name, sub):
|
||||
"""Handle selection removal."""
|
||||
doc = App.getDocument(doc_name)
|
||||
if not doc:
|
||||
return
|
||||
|
||||
obj = doc.getObject(obj_name)
|
||||
if obj in self.selected_components:
|
||||
self.selected_components.remove(obj)
|
||||
self._update_table()
|
||||
|
||||
def _is_valid_component(self, obj):
|
||||
"""Check if object is a valid assembly component."""
|
||||
valid_types = [
|
||||
"App::Link",
|
||||
"App::LinkGroup",
|
||||
"Part::Feature",
|
||||
"PartDesign::Body",
|
||||
"App::Part",
|
||||
]
|
||||
return obj.TypeId in valid_types
|
||||
|
||||
def _update_table(self):
|
||||
"""Update the component table."""
|
||||
self.component_table.setRowCount(len(self.selected_components))
|
||||
|
||||
for i, comp in enumerate(self.selected_components):
|
||||
# Component name
|
||||
name_item = QtGui.QTableWidgetItem(comp.Label)
|
||||
name_item.setFlags(name_item.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
self.component_table.setItem(i, 0, name_item)
|
||||
|
||||
# Remove button
|
||||
remove_btn = QtGui.QPushButton("X")
|
||||
remove_btn.setMaximumWidth(40)
|
||||
remove_btn.clicked.connect(
|
||||
lambda checked, idx=i: self._remove_component(idx)
|
||||
)
|
||||
self.component_table.setCellWidget(i, 1, remove_btn)
|
||||
|
||||
def _remove_component(self, index):
|
||||
"""Remove component from selection."""
|
||||
if 0 <= index < len(self.selected_components):
|
||||
self.selected_components.pop(index)
|
||||
self._update_table()
|
||||
|
||||
def on_spacing_mode_changed(self, index):
|
||||
"""Update label based on spacing mode."""
|
||||
if index == 0:
|
||||
self.length_label.setText("Total Length:")
|
||||
else:
|
||||
self.length_label.setText("Spacing:")
|
||||
|
||||
def accept(self):
|
||||
"""Create the linear pattern."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
|
||||
if not self.selected_components:
|
||||
App.Console.PrintError("No components selected for pattern\n")
|
||||
return False
|
||||
|
||||
doc = App.ActiveDocument
|
||||
if not doc:
|
||||
return False
|
||||
|
||||
# Get parameters
|
||||
direction = App.Vector(
|
||||
self.dir_x_spin.value(),
|
||||
self.dir_y_spin.value(),
|
||||
self.dir_z_spin.value(),
|
||||
)
|
||||
|
||||
if direction.Length < 1e-6:
|
||||
App.Console.PrintError("Direction vector cannot be zero\n")
|
||||
return False
|
||||
|
||||
direction.normalize()
|
||||
|
||||
occurrences = self.occurrences_spin.value()
|
||||
length_value = self.length_spin.value()
|
||||
|
||||
if self.spacing_mode.currentIndex() == 0:
|
||||
# Total length mode
|
||||
spacing = length_value / (occurrences - 1) if occurrences > 1 else 0
|
||||
else:
|
||||
# Spacing mode
|
||||
spacing = length_value
|
||||
|
||||
use_links = self.link_checkbox.isChecked()
|
||||
hide_original = self.hide_original_checkbox.isChecked()
|
||||
|
||||
# Find parent assembly
|
||||
assembly = self._find_parent_assembly(self.selected_components[0])
|
||||
|
||||
doc.openTransaction("Assembly Linear Pattern")
|
||||
|
||||
try:
|
||||
created_objects = []
|
||||
|
||||
for comp in self.selected_components:
|
||||
# Get base placement
|
||||
if hasattr(comp, "Placement"):
|
||||
base_placement = comp.Placement
|
||||
else:
|
||||
base_placement = App.Placement()
|
||||
|
||||
# Create pattern instances (skip i=0 as that's the original)
|
||||
for i in range(1, occurrences):
|
||||
offset = direction * spacing * i
|
||||
new_placement = App.Placement(
|
||||
base_placement.Base + offset,
|
||||
base_placement.Rotation,
|
||||
)
|
||||
|
||||
if use_links:
|
||||
# Create link to original
|
||||
if comp.TypeId == "App::Link":
|
||||
# Link to the linked object
|
||||
link_target = comp.LinkedObject
|
||||
else:
|
||||
link_target = comp
|
||||
|
||||
new_obj = doc.addObject("App::Link", f"{comp.Label}_Pattern{i}")
|
||||
new_obj.LinkedObject = link_target
|
||||
new_obj.Placement = new_placement
|
||||
else:
|
||||
# Create copy
|
||||
new_obj = doc.copyObject(comp, False)
|
||||
new_obj.Label = f"{comp.Label}_Pattern{i}"
|
||||
new_obj.Placement = new_placement
|
||||
|
||||
# Add to assembly if found
|
||||
if assembly and hasattr(assembly, "addObject"):
|
||||
assembly.addObject(new_obj)
|
||||
|
||||
created_objects.append(new_obj)
|
||||
|
||||
# Hide original if requested
|
||||
if hide_original and hasattr(comp, "ViewObject"):
|
||||
comp.ViewObject.Visibility = False
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
|
||||
App.Console.PrintMessage(
|
||||
f"Created {len(created_objects)} pattern instances\n"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to create pattern: {e}\n")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Cancel the operation."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def _find_parent_assembly(self, obj):
|
||||
"""Find the parent assembly of an object."""
|
||||
doc = obj.Document
|
||||
|
||||
for candidate in doc.Objects:
|
||||
if candidate.TypeId == "Assembly::AssemblyObject":
|
||||
# Check if obj is in this assembly's group
|
||||
if hasattr(candidate, "Group"):
|
||||
if obj in candidate.Group:
|
||||
return candidate
|
||||
# Check nested
|
||||
for member in candidate.Group:
|
||||
if hasattr(member, "Group") and obj in member.Group:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class AssemblyLinearPatternCommand:
|
||||
"""Command to create a linear pattern of assembly components."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("assembly_linear_pattern"),
|
||||
"MenuText": "Assembly Linear Pattern",
|
||||
"ToolTip": "Create a linear pattern of assembly components",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
def Activated(self):
|
||||
panel = AssemblyLinearPatternTaskPanel()
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Assembly Polar Pattern
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AssemblyPolarPatternTaskPanel:
|
||||
"""Task panel for creating polar patterns of assembly components."""
|
||||
|
||||
def __init__(self):
|
||||
self.form = QtGui.QWidget()
|
||||
self.setup_ui()
|
||||
self.setup_selection_observer()
|
||||
self.selected_components = []
|
||||
self.axis_selection = None
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Component selection section
|
||||
component_group = QtGui.QGroupBox("Components")
|
||||
component_layout = QtGui.QVBoxLayout(component_group)
|
||||
|
||||
# Selection table
|
||||
self.component_table = QtGui.QTableWidget()
|
||||
self.component_table.setColumnCount(2)
|
||||
self.component_table.setHorizontalHeaderLabels(["Component", "Remove"])
|
||||
self.component_table.horizontalHeader().setStretchLastSection(False)
|
||||
self.component_table.horizontalHeader().setSectionResizeMode(
|
||||
0, QtGui.QHeaderView.Stretch
|
||||
)
|
||||
self.component_table.horizontalHeader().setSectionResizeMode(
|
||||
1, QtGui.QHeaderView.Fixed
|
||||
)
|
||||
self.component_table.setColumnWidth(1, 60)
|
||||
self.component_table.setMaximumHeight(100)
|
||||
component_layout.addWidget(self.component_table)
|
||||
|
||||
hint_label = QtGui.QLabel("Select assembly components in the 3D view")
|
||||
hint_label.setStyleSheet("color: gray; font-style: italic;")
|
||||
component_layout.addWidget(hint_label)
|
||||
|
||||
layout.addWidget(component_group)
|
||||
|
||||
# Axis section
|
||||
axis_group = QtGui.QGroupBox("Rotation Axis")
|
||||
axis_layout = QtGui.QFormLayout(axis_group)
|
||||
|
||||
# Axis definition mode
|
||||
self.axis_mode = QtGui.QComboBox()
|
||||
self.axis_mode.addItems(["Custom Axis", "X Axis", "Y Axis", "Z Axis"])
|
||||
self.axis_mode.currentIndexChanged.connect(self.on_axis_mode_changed)
|
||||
axis_layout.addRow("Axis:", self.axis_mode)
|
||||
|
||||
# Custom axis direction
|
||||
self.axis_widget = QtGui.QWidget()
|
||||
axis_dir_layout = QtGui.QHBoxLayout(self.axis_widget)
|
||||
axis_dir_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.axis_x_spin = QtGui.QDoubleSpinBox()
|
||||
self.axis_x_spin.setRange(-1, 1)
|
||||
self.axis_x_spin.setValue(0)
|
||||
self.axis_x_spin.setDecimals(3)
|
||||
self.axis_x_spin.setSingleStep(0.1)
|
||||
self.axis_y_spin = QtGui.QDoubleSpinBox()
|
||||
self.axis_y_spin.setRange(-1, 1)
|
||||
self.axis_y_spin.setValue(0)
|
||||
self.axis_y_spin.setDecimals(3)
|
||||
self.axis_y_spin.setSingleStep(0.1)
|
||||
self.axis_z_spin = QtGui.QDoubleSpinBox()
|
||||
self.axis_z_spin.setRange(-1, 1)
|
||||
self.axis_z_spin.setValue(1)
|
||||
self.axis_z_spin.setDecimals(3)
|
||||
self.axis_z_spin.setSingleStep(0.1)
|
||||
axis_dir_layout.addWidget(QtGui.QLabel("X:"))
|
||||
axis_dir_layout.addWidget(self.axis_x_spin)
|
||||
axis_dir_layout.addWidget(QtGui.QLabel("Y:"))
|
||||
axis_dir_layout.addWidget(self.axis_y_spin)
|
||||
axis_dir_layout.addWidget(QtGui.QLabel("Z:"))
|
||||
axis_dir_layout.addWidget(self.axis_z_spin)
|
||||
axis_layout.addRow("Direction:", self.axis_widget)
|
||||
|
||||
# Center point
|
||||
center_layout = QtGui.QHBoxLayout()
|
||||
self.center_x_spin = QtGui.QDoubleSpinBox()
|
||||
self.center_x_spin.setRange(-100000, 100000)
|
||||
self.center_x_spin.setValue(0)
|
||||
self.center_x_spin.setDecimals(3)
|
||||
self.center_y_spin = QtGui.QDoubleSpinBox()
|
||||
self.center_y_spin.setRange(-100000, 100000)
|
||||
self.center_y_spin.setValue(0)
|
||||
self.center_y_spin.setDecimals(3)
|
||||
self.center_z_spin = QtGui.QDoubleSpinBox()
|
||||
self.center_z_spin.setRange(-100000, 100000)
|
||||
self.center_z_spin.setValue(0)
|
||||
self.center_z_spin.setDecimals(3)
|
||||
center_layout.addWidget(QtGui.QLabel("X:"))
|
||||
center_layout.addWidget(self.center_x_spin)
|
||||
center_layout.addWidget(QtGui.QLabel("Y:"))
|
||||
center_layout.addWidget(self.center_y_spin)
|
||||
center_layout.addWidget(QtGui.QLabel("Z:"))
|
||||
center_layout.addWidget(self.center_z_spin)
|
||||
axis_layout.addRow("Center:", center_layout)
|
||||
|
||||
layout.addWidget(axis_group)
|
||||
|
||||
# Pattern parameters section
|
||||
params_group = QtGui.QGroupBox("Pattern Parameters")
|
||||
params_layout = QtGui.QFormLayout(params_group)
|
||||
|
||||
# Occurrences
|
||||
self.occurrences_spin = QtGui.QSpinBox()
|
||||
self.occurrences_spin.setRange(2, 100)
|
||||
self.occurrences_spin.setValue(6)
|
||||
params_layout.addRow("Occurrences:", self.occurrences_spin)
|
||||
|
||||
# Angle mode
|
||||
self.angle_mode = QtGui.QComboBox()
|
||||
self.angle_mode.addItems(["Full Circle (360)", "Custom Angle"])
|
||||
self.angle_mode.currentIndexChanged.connect(self.on_angle_mode_changed)
|
||||
params_layout.addRow("Mode:", self.angle_mode)
|
||||
|
||||
# Angle value
|
||||
self.angle_spin = QtGui.QDoubleSpinBox()
|
||||
self.angle_spin.setRange(0.001, 360)
|
||||
self.angle_spin.setValue(360)
|
||||
self.angle_spin.setDecimals(2)
|
||||
self.angle_spin.setSuffix(" deg")
|
||||
self.angle_spin.setEnabled(False)
|
||||
params_layout.addRow("Total Angle:", self.angle_spin)
|
||||
|
||||
layout.addWidget(params_group)
|
||||
|
||||
# Options section
|
||||
options_group = QtGui.QGroupBox("Options")
|
||||
options_layout = QtGui.QVBoxLayout(options_group)
|
||||
|
||||
self.link_checkbox = QtGui.QCheckBox("Create as Links (recommended)")
|
||||
self.link_checkbox.setChecked(True)
|
||||
options_layout.addWidget(self.link_checkbox)
|
||||
|
||||
self.hide_original_checkbox = QtGui.QCheckBox("Hide original components")
|
||||
self.hide_original_checkbox.setChecked(False)
|
||||
options_layout.addWidget(self.hide_original_checkbox)
|
||||
|
||||
layout.addWidget(options_group)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Set up observer to track selection changes."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_selection_added(doc, obj, sub)
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_selection_removed(doc, obj, sub)
|
||||
|
||||
def clearSelection(self, doc):
|
||||
pass
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_selection_added(self, doc_name, obj_name, sub):
|
||||
"""Handle new selection."""
|
||||
doc = App.getDocument(doc_name)
|
||||
if not doc:
|
||||
return
|
||||
|
||||
obj = doc.getObject(obj_name)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
# Check if this is an assembly component
|
||||
if not self._is_valid_component(obj):
|
||||
return
|
||||
|
||||
if obj in self.selected_components:
|
||||
return
|
||||
|
||||
self.selected_components.append(obj)
|
||||
self._update_table()
|
||||
|
||||
def on_selection_removed(self, doc_name, obj_name, sub):
|
||||
"""Handle selection removal."""
|
||||
doc = App.getDocument(doc_name)
|
||||
if not doc:
|
||||
return
|
||||
|
||||
obj = doc.getObject(obj_name)
|
||||
if obj in self.selected_components:
|
||||
self.selected_components.remove(obj)
|
||||
self._update_table()
|
||||
|
||||
def _is_valid_component(self, obj):
|
||||
"""Check if object is a valid assembly component."""
|
||||
valid_types = [
|
||||
"App::Link",
|
||||
"App::LinkGroup",
|
||||
"Part::Feature",
|
||||
"PartDesign::Body",
|
||||
"App::Part",
|
||||
]
|
||||
return obj.TypeId in valid_types
|
||||
|
||||
def _update_table(self):
|
||||
"""Update the component table."""
|
||||
self.component_table.setRowCount(len(self.selected_components))
|
||||
|
||||
for i, comp in enumerate(self.selected_components):
|
||||
name_item = QtGui.QTableWidgetItem(comp.Label)
|
||||
name_item.setFlags(name_item.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
self.component_table.setItem(i, 0, name_item)
|
||||
|
||||
remove_btn = QtGui.QPushButton("X")
|
||||
remove_btn.setMaximumWidth(40)
|
||||
remove_btn.clicked.connect(
|
||||
lambda checked, idx=i: self._remove_component(idx)
|
||||
)
|
||||
self.component_table.setCellWidget(i, 1, remove_btn)
|
||||
|
||||
def _remove_component(self, index):
|
||||
"""Remove component from selection."""
|
||||
if 0 <= index < len(self.selected_components):
|
||||
self.selected_components.pop(index)
|
||||
self._update_table()
|
||||
|
||||
def on_axis_mode_changed(self, index):
|
||||
"""Update axis inputs based on mode."""
|
||||
if index == 0:
|
||||
# Custom axis
|
||||
self.axis_widget.setEnabled(True)
|
||||
else:
|
||||
# Preset axis
|
||||
self.axis_widget.setEnabled(False)
|
||||
if index == 1: # X
|
||||
self.axis_x_spin.setValue(1)
|
||||
self.axis_y_spin.setValue(0)
|
||||
self.axis_z_spin.setValue(0)
|
||||
elif index == 2: # Y
|
||||
self.axis_x_spin.setValue(0)
|
||||
self.axis_y_spin.setValue(1)
|
||||
self.axis_z_spin.setValue(0)
|
||||
elif index == 3: # Z
|
||||
self.axis_x_spin.setValue(0)
|
||||
self.axis_y_spin.setValue(0)
|
||||
self.axis_z_spin.setValue(1)
|
||||
|
||||
def on_angle_mode_changed(self, index):
|
||||
"""Update angle input based on mode."""
|
||||
if index == 0:
|
||||
# Full circle
|
||||
self.angle_spin.setValue(360)
|
||||
self.angle_spin.setEnabled(False)
|
||||
else:
|
||||
# Custom angle
|
||||
self.angle_spin.setEnabled(True)
|
||||
|
||||
def accept(self):
|
||||
"""Create the polar pattern."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
|
||||
if not self.selected_components:
|
||||
App.Console.PrintError("No components selected for pattern\n")
|
||||
return False
|
||||
|
||||
doc = App.ActiveDocument
|
||||
if not doc:
|
||||
return False
|
||||
|
||||
# Get axis
|
||||
axis = App.Vector(
|
||||
self.axis_x_spin.value(),
|
||||
self.axis_y_spin.value(),
|
||||
self.axis_z_spin.value(),
|
||||
)
|
||||
|
||||
if axis.Length < 1e-6:
|
||||
App.Console.PrintError("Axis vector cannot be zero\n")
|
||||
return False
|
||||
|
||||
axis.normalize()
|
||||
|
||||
# Get center
|
||||
center = App.Vector(
|
||||
self.center_x_spin.value(),
|
||||
self.center_y_spin.value(),
|
||||
self.center_z_spin.value(),
|
||||
)
|
||||
|
||||
occurrences = self.occurrences_spin.value()
|
||||
total_angle = self.angle_spin.value()
|
||||
|
||||
# Calculate angle increment
|
||||
if self.angle_mode.currentIndex() == 0:
|
||||
# Full circle - don't include last (would overlap first)
|
||||
angle_step = 360.0 / occurrences
|
||||
else:
|
||||
# Custom angle
|
||||
angle_step = total_angle / (occurrences - 1) if occurrences > 1 else 0
|
||||
|
||||
use_links = self.link_checkbox.isChecked()
|
||||
hide_original = self.hide_original_checkbox.isChecked()
|
||||
|
||||
# Find parent assembly
|
||||
assembly = self._find_parent_assembly(self.selected_components[0])
|
||||
|
||||
doc.openTransaction("Assembly Polar Pattern")
|
||||
|
||||
try:
|
||||
created_objects = []
|
||||
|
||||
for comp in self.selected_components:
|
||||
# Get base placement
|
||||
if hasattr(comp, "Placement"):
|
||||
base_placement = comp.Placement
|
||||
else:
|
||||
base_placement = App.Placement()
|
||||
|
||||
# Create pattern instances
|
||||
for i in range(1, occurrences):
|
||||
angle = angle_step * i
|
||||
|
||||
# Create rotation around axis through center
|
||||
rotation = App.Rotation(axis, angle)
|
||||
|
||||
# Calculate new position
|
||||
# Translate to origin, rotate, translate back
|
||||
base_pos = base_placement.Base
|
||||
relative_pos = base_pos - center
|
||||
rotated_pos = rotation.multVec(relative_pos)
|
||||
new_pos = rotated_pos + center
|
||||
|
||||
# Combine rotations
|
||||
new_rotation = rotation.multiply(base_placement.Rotation)
|
||||
|
||||
new_placement = App.Placement(new_pos, new_rotation)
|
||||
|
||||
if use_links:
|
||||
if comp.TypeId == "App::Link":
|
||||
link_target = comp.LinkedObject
|
||||
else:
|
||||
link_target = comp
|
||||
|
||||
new_obj = doc.addObject("App::Link", f"{comp.Label}_Polar{i}")
|
||||
new_obj.LinkedObject = link_target
|
||||
new_obj.Placement = new_placement
|
||||
else:
|
||||
new_obj = doc.copyObject(comp, False)
|
||||
new_obj.Label = f"{comp.Label}_Polar{i}"
|
||||
new_obj.Placement = new_placement
|
||||
|
||||
if assembly and hasattr(assembly, "addObject"):
|
||||
assembly.addObject(new_obj)
|
||||
|
||||
created_objects.append(new_obj)
|
||||
|
||||
if hide_original and hasattr(comp, "ViewObject"):
|
||||
comp.ViewObject.Visibility = False
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
|
||||
App.Console.PrintMessage(
|
||||
f"Created {len(created_objects)} polar pattern instances\n"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to create pattern: {e}\n")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Cancel the operation."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def _find_parent_assembly(self, obj):
|
||||
"""Find the parent assembly of an object."""
|
||||
doc = obj.Document
|
||||
|
||||
for candidate in doc.Objects:
|
||||
if candidate.TypeId == "Assembly::AssemblyObject":
|
||||
if hasattr(candidate, "Group"):
|
||||
if obj in candidate.Group:
|
||||
return candidate
|
||||
for member in candidate.Group:
|
||||
if hasattr(member, "Group") and obj in member.Group:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class AssemblyPolarPatternCommand:
|
||||
"""Command to create a polar pattern of assembly components."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("assembly_polar_pattern"),
|
||||
"MenuText": "Assembly Polar Pattern",
|
||||
"ToolTip": "Create a polar (circular) pattern of assembly components",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
def Activated(self):
|
||||
panel = AssemblyPolarPatternTaskPanel()
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Register Commands
|
||||
# =============================================================================
|
||||
|
||||
Gui.addCommand("ZTools_AssemblyLinearPattern", AssemblyLinearPatternCommand())
|
||||
Gui.addCommand("ZTools_AssemblyPolarPattern", AssemblyPolarPatternCommand())
|
||||
880
reference/ztools/ztools/ztools/commands/datum_commands.py
Normal file
@@ -0,0 +1,880 @@
|
||||
# ztools/commands/datum_commands.py
|
||||
# GUI commands and task panel for datum creation
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
class SelectionItem:
|
||||
"""Represents a selected geometry item."""
|
||||
|
||||
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:
|
||||
# Try to get shape from object
|
||||
if hasattr(self.obj, "Shape"):
|
||||
if self.subname and self.subname.startswith("Face"):
|
||||
try:
|
||||
self.shape = self.obj.Shape.getElement(self.subname)
|
||||
except Exception:
|
||||
pass
|
||||
elif self.subname and self.subname.startswith("Edge"):
|
||||
try:
|
||||
self.shape = self.obj.Shape.getElement(self.subname)
|
||||
except Exception:
|
||||
pass
|
||||
elif self.subname and self.subname.startswith("Vertex"):
|
||||
try:
|
||||
self.shape = self.obj.Shape.getElement(self.subname)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.shape is None:
|
||||
# Check if it's a datum plane object
|
||||
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):
|
||||
# Check if it's a cylindrical face
|
||||
if isinstance(self.shape.Surface, Part.Cylinder):
|
||||
return "cylinder"
|
||||
elif self.shape.Surface.isPlanar():
|
||||
return "face"
|
||||
return "face"
|
||||
elif isinstance(self.shape, Part.Edge):
|
||||
# Check if it's a circular edge
|
||||
if isinstance(self.shape.Curve, (Part.Circle, Part.ArcOfCircle)):
|
||||
return "circle"
|
||||
elif isinstance(self.shape.Curve, Part.Line):
|
||||
return "edge"
|
||||
return "edge"
|
||||
elif isinstance(self.shape, Part.Vertex):
|
||||
return "vertex"
|
||||
|
||||
return "unknown"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Get display name for UI."""
|
||||
if self.subname:
|
||||
return f"{self.obj.Label}.{self.subname}"
|
||||
return self.obj.Label
|
||||
|
||||
@property
|
||||
def type_icon(self):
|
||||
"""Get icon character for geometry type."""
|
||||
icons = {
|
||||
"face": "▢",
|
||||
"plane": "▣",
|
||||
"cylinder": "◎",
|
||||
"edge": "―",
|
||||
"circle": "○",
|
||||
"vertex": "•",
|
||||
"unknown": "?",
|
||||
}
|
||||
return icons.get(self.geo_type, "?")
|
||||
|
||||
|
||||
class DatumCreatorTaskPanel:
|
||||
"""Unified task panel for creating datum planes, axes, and points.
|
||||
|
||||
Features a selection table where users can add/remove geometry items.
|
||||
The datum type is automatically detected based on selection contents.
|
||||
"""
|
||||
|
||||
# Mode definitions: (display_name, mode_id, required_types, datum_category)
|
||||
# required_types is a tuple describing what selection is needed
|
||||
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"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle("ZTools Datum Creator")
|
||||
self.selection_list = [] # List of SelectionItem
|
||||
self.setup_ui()
|
||||
self.setup_selection_observer()
|
||||
self.update_mode_from_selection()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Selection table section
|
||||
sel_group = QtGui.QGroupBox("Selection")
|
||||
sel_layout = QtGui.QVBoxLayout(sel_group)
|
||||
|
||||
# Selection table
|
||||
self.sel_table = QtGui.QTableWidget()
|
||||
self.sel_table.setColumnCount(3)
|
||||
self.sel_table.setHorizontalHeaderLabels(["", "Element", ""])
|
||||
header = self.sel_table.horizontalHeader()
|
||||
header.setStretchLastSection(False)
|
||||
header.setSectionResizeMode(0, QtGui.QHeaderView.Fixed)
|
||||
header.setSectionResizeMode(1, QtGui.QHeaderView.Stretch)
|
||||
header.setSectionResizeMode(2, QtGui.QHeaderView.Fixed)
|
||||
header.resizeSection(0, 28)
|
||||
header.resizeSection(2, 28)
|
||||
self.sel_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
self.sel_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
self.sel_table.setMaximumHeight(150)
|
||||
self.sel_table.verticalHeader().setVisible(False)
|
||||
sel_layout.addWidget(self.sel_table)
|
||||
|
||||
# Selection buttons
|
||||
sel_btn_layout = QtGui.QHBoxLayout()
|
||||
self.add_sel_btn = QtGui.QPushButton("Add Selected")
|
||||
self.add_sel_btn.clicked.connect(self.add_current_selection)
|
||||
self.remove_sel_btn = QtGui.QPushButton("Remove")
|
||||
self.remove_sel_btn.clicked.connect(self.remove_selected_row)
|
||||
self.clear_sel_btn = QtGui.QPushButton("Clear All")
|
||||
self.clear_sel_btn.clicked.connect(self.clear_selection)
|
||||
sel_btn_layout.addWidget(self.add_sel_btn)
|
||||
sel_btn_layout.addWidget(self.remove_sel_btn)
|
||||
sel_btn_layout.addWidget(self.clear_sel_btn)
|
||||
sel_layout.addLayout(sel_btn_layout)
|
||||
|
||||
layout.addWidget(sel_group)
|
||||
|
||||
# Detected mode display
|
||||
mode_group = QtGui.QGroupBox("Datum Type")
|
||||
mode_layout = QtGui.QVBoxLayout(mode_group)
|
||||
|
||||
self.mode_label = QtGui.QLabel("Select geometry to auto-detect mode")
|
||||
self.mode_label.setStyleSheet("font-weight: bold; color: #888;")
|
||||
mode_layout.addWidget(self.mode_label)
|
||||
|
||||
# Manual mode override
|
||||
override_layout = QtGui.QHBoxLayout()
|
||||
override_layout.addWidget(QtGui.QLabel("Override:"))
|
||||
self.mode_combo = QtGui.QComboBox()
|
||||
self.mode_combo.addItem("(Auto-detect)", None)
|
||||
for display_name, mode_id, _, category in self.MODES:
|
||||
self.mode_combo.addItem(f"[{category[0].upper()}] {display_name}", mode_id)
|
||||
self.mode_combo.currentIndexChanged.connect(self.on_mode_override_changed)
|
||||
override_layout.addWidget(self.mode_combo)
|
||||
mode_layout.addLayout(override_layout)
|
||||
|
||||
layout.addWidget(mode_group)
|
||||
|
||||
# Parameters section
|
||||
self.params_group = QtGui.QGroupBox("Parameters")
|
||||
self.params_layout = QtGui.QFormLayout(self.params_group)
|
||||
|
||||
# Offset spinner
|
||||
self.offset_spin = QtGui.QDoubleSpinBox()
|
||||
self.offset_spin.setRange(-10000, 10000)
|
||||
self.offset_spin.setValue(10)
|
||||
self.offset_spin.setSuffix(" mm")
|
||||
|
||||
# Angle spinner
|
||||
self.angle_spin = QtGui.QDoubleSpinBox()
|
||||
self.angle_spin.setRange(-360, 360)
|
||||
self.angle_spin.setValue(45)
|
||||
self.angle_spin.setSuffix(" °")
|
||||
|
||||
# Parameter spinner (0-1)
|
||||
self.param_spin = QtGui.QDoubleSpinBox()
|
||||
self.param_spin.setRange(0, 1)
|
||||
self.param_spin.setValue(0.5)
|
||||
self.param_spin.setSingleStep(0.1)
|
||||
|
||||
# XYZ coordinates
|
||||
self.x_spin = QtGui.QDoubleSpinBox()
|
||||
self.x_spin.setRange(-10000, 10000)
|
||||
self.x_spin.setSuffix(" mm")
|
||||
self.y_spin = QtGui.QDoubleSpinBox()
|
||||
self.y_spin.setRange(-10000, 10000)
|
||||
self.y_spin.setSuffix(" mm")
|
||||
self.z_spin = QtGui.QDoubleSpinBox()
|
||||
self.z_spin.setRange(-10000, 10000)
|
||||
self.z_spin.setSuffix(" mm")
|
||||
|
||||
layout.addWidget(self.params_group)
|
||||
|
||||
# Options section
|
||||
options_group = QtGui.QGroupBox("Options")
|
||||
options_layout = QtGui.QVBoxLayout(options_group)
|
||||
|
||||
self.link_spreadsheet_cb = QtGui.QCheckBox("Link to Spreadsheet")
|
||||
options_layout.addWidget(self.link_spreadsheet_cb)
|
||||
|
||||
self.use_body_cb = QtGui.QCheckBox("Add to Active Body")
|
||||
self.use_body_cb.setChecked(True)
|
||||
options_layout.addWidget(self.use_body_cb)
|
||||
|
||||
# Custom name
|
||||
name_layout = QtGui.QHBoxLayout()
|
||||
self.custom_name_cb = QtGui.QCheckBox("Custom Name:")
|
||||
self.custom_name_edit = QtGui.QLineEdit()
|
||||
self.custom_name_edit.setEnabled(False)
|
||||
self.custom_name_cb.toggled.connect(self.custom_name_edit.setEnabled)
|
||||
name_layout.addWidget(self.custom_name_cb)
|
||||
name_layout.addWidget(self.custom_name_edit)
|
||||
options_layout.addLayout(name_layout)
|
||||
|
||||
layout.addWidget(options_group)
|
||||
|
||||
# Initial state
|
||||
self.update_params_ui(None)
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Setup selection observer to track user selections."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_freecad_selection_changed()
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_freecad_selection_changed()
|
||||
|
||||
def clearSelection(self, doc):
|
||||
self.panel.on_freecad_selection_changed()
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_freecad_selection_changed(self):
|
||||
"""Called when FreeCAD selection changes - update add button state."""
|
||||
sel = Gui.Selection.getSelectionEx()
|
||||
has_sel = len(sel) > 0
|
||||
self.add_sel_btn.setEnabled(has_sel)
|
||||
|
||||
def add_current_selection(self):
|
||||
"""Add current FreeCAD selection to the selection table."""
|
||||
sel = Gui.Selection.getSelectionEx()
|
||||
for s in sel:
|
||||
obj = s.Object
|
||||
if s.SubElementNames:
|
||||
for i, sub in enumerate(s.SubElementNames):
|
||||
# Get the shape if available
|
||||
shape = None
|
||||
if i < len(s.SubObjects):
|
||||
shape = s.SubObjects[i]
|
||||
item = SelectionItem(obj, sub, shape)
|
||||
self._add_selection_item(item)
|
||||
else:
|
||||
# Object selected without sub-element
|
||||
item = SelectionItem(obj, "", None)
|
||||
self._add_selection_item(item)
|
||||
|
||||
self.refresh_selection_table()
|
||||
self.update_mode_from_selection()
|
||||
|
||||
def _add_selection_item(self, item):
|
||||
"""Add item to selection list if not already present."""
|
||||
# Check for duplicates
|
||||
for existing in self.selection_list:
|
||||
if existing.obj == item.obj and existing.subname == item.subname:
|
||||
return # Already in list
|
||||
self.selection_list.append(item)
|
||||
|
||||
def remove_selected_row(self):
|
||||
"""Remove selected row from selection table."""
|
||||
rows = self.sel_table.selectionModel().selectedRows()
|
||||
if rows:
|
||||
# Remove in reverse order to maintain indices
|
||||
for row in sorted([r.row() for r in rows], reverse=True):
|
||||
if row < len(self.selection_list):
|
||||
del self.selection_list[row]
|
||||
self.refresh_selection_table()
|
||||
self.update_mode_from_selection()
|
||||
|
||||
def clear_selection(self):
|
||||
"""Clear all items from selection table."""
|
||||
self.selection_list.clear()
|
||||
self.refresh_selection_table()
|
||||
self.update_mode_from_selection()
|
||||
|
||||
def refresh_selection_table(self):
|
||||
"""Refresh the selection table display."""
|
||||
self.sel_table.setRowCount(len(self.selection_list))
|
||||
for i, item in enumerate(self.selection_list):
|
||||
# Type icon
|
||||
type_item = QtGui.QTableWidgetItem(item.type_icon)
|
||||
type_item.setTextAlignment(QtCore.Qt.AlignCenter)
|
||||
type_item.setToolTip(item.geo_type)
|
||||
self.sel_table.setItem(i, 0, type_item)
|
||||
|
||||
# Element name
|
||||
name_item = QtGui.QTableWidgetItem(item.display_name)
|
||||
self.sel_table.setItem(i, 1, name_item)
|
||||
|
||||
# Remove button
|
||||
remove_btn = QtGui.QPushButton("✕")
|
||||
remove_btn.setFixedSize(24, 24)
|
||||
remove_btn.clicked.connect(lambda checked, row=i: self._remove_row(row))
|
||||
self.sel_table.setCellWidget(i, 2, remove_btn)
|
||||
|
||||
def _remove_row(self, row):
|
||||
"""Remove a specific row."""
|
||||
if row < len(self.selection_list):
|
||||
del self.selection_list[row]
|
||||
self.refresh_selection_table()
|
||||
self.update_mode_from_selection()
|
||||
|
||||
def get_selection_types(self):
|
||||
"""Get tuple of geometry types in current selection."""
|
||||
return tuple(item.geo_type for item in self.selection_list)
|
||||
|
||||
def update_mode_from_selection(self):
|
||||
"""Auto-detect the best mode based on current selection."""
|
||||
if self.mode_combo.currentIndex() > 0:
|
||||
# Manual override is active
|
||||
mode_id = self.mode_combo.currentData()
|
||||
self._set_detected_mode(mode_id)
|
||||
return
|
||||
|
||||
sel_types = self.get_selection_types()
|
||||
|
||||
if not sel_types:
|
||||
self.mode_label.setText("Select geometry to auto-detect mode")
|
||||
self.mode_label.setStyleSheet("font-weight: bold; color: #888;")
|
||||
self.update_params_ui(None)
|
||||
return
|
||||
|
||||
# Find matching modes
|
||||
best_match = None
|
||||
best_score = -1
|
||||
|
||||
for display_name, mode_id, required_types, category in self.MODES:
|
||||
if not required_types:
|
||||
continue # Skip modes with no requirements (like XYZ point)
|
||||
|
||||
score = self._match_score(sel_types, required_types)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_match = (display_name, mode_id, category)
|
||||
|
||||
if best_match and best_score > 0:
|
||||
display_name, mode_id, category = best_match
|
||||
cat_colors = {"plane": "#cba6f7", "axis": "#94e2d5", "point": "#f9e2af"}
|
||||
color = cat_colors.get(category, "#cdd6f4")
|
||||
self.mode_label.setText(f"{display_name}")
|
||||
self.mode_label.setStyleSheet(f"font-weight: bold; color: {color};")
|
||||
self.update_params_ui(mode_id)
|
||||
else:
|
||||
self.mode_label.setText("No matching mode for selection")
|
||||
self.mode_label.setStyleSheet("font-weight: bold; color: #f38ba8;")
|
||||
self.update_params_ui(None)
|
||||
|
||||
def _match_score(self, sel_types, required_types):
|
||||
"""
|
||||
Calculate how well selection matches required types.
|
||||
Returns score >= 0, higher is better. 0 means no match.
|
||||
"""
|
||||
if len(sel_types) < len(required_types):
|
||||
return 0 # Not enough items
|
||||
|
||||
# Check if we can satisfy all requirements
|
||||
sel_list = list(sel_types)
|
||||
matched = 0
|
||||
for req in required_types:
|
||||
# Try to find a matching type in selection
|
||||
found = False
|
||||
for i, sel in enumerate(sel_list):
|
||||
if self._type_matches(sel, req):
|
||||
sel_list.pop(i)
|
||||
matched += 1
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
return 0 # Can't satisfy this requirement
|
||||
|
||||
# Score based on how exact the match is
|
||||
# Exact match (same count) scores higher
|
||||
if len(sel_types) == len(required_types):
|
||||
return 100 + matched
|
||||
else:
|
||||
return matched
|
||||
|
||||
def _type_matches(self, sel_type, req_type):
|
||||
"""Check if a selected type matches a required type."""
|
||||
if sel_type == req_type:
|
||||
return True
|
||||
# Face can match cylinder (cylinder is a special face)
|
||||
if req_type == "face" and sel_type in ("face", "cylinder"):
|
||||
return True
|
||||
# Edge can match circle (circle is a special edge)
|
||||
if req_type == "edge" and sel_type in ("edge", "circle"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_mode_override_changed(self, index):
|
||||
"""Handle manual mode override selection."""
|
||||
if index == 0:
|
||||
# Auto-detect
|
||||
self.update_mode_from_selection()
|
||||
else:
|
||||
mode_id = self.mode_combo.currentData()
|
||||
self._set_detected_mode(mode_id)
|
||||
|
||||
def _set_detected_mode(self, mode_id):
|
||||
"""Set the mode and update UI."""
|
||||
for display_name, mid, _, category in self.MODES:
|
||||
if mid == mode_id:
|
||||
cat_colors = {"plane": "#cba6f7", "axis": "#94e2d5", "point": "#f9e2af"}
|
||||
color = cat_colors.get(category, "#cdd6f4")
|
||||
self.mode_label.setText(f"{display_name}")
|
||||
self.mode_label.setStyleSheet(f"font-weight: bold; color: {color};")
|
||||
self.update_params_ui(mode_id)
|
||||
return
|
||||
|
||||
def _clear_params_layout(self):
|
||||
"""Remove all rows from params_layout without deleting the widgets.
|
||||
|
||||
QFormLayout.removeRow() destroys the widgets it owns. Instead we
|
||||
detach every item from the layout (which relinquishes ownership)
|
||||
and hide the widgets so they can be re-added later.
|
||||
"""
|
||||
while self.params_layout.count():
|
||||
item = self.params_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.hide()
|
||||
widget.setParent(None)
|
||||
|
||||
def update_params_ui(self, mode_id):
|
||||
"""Update parameters UI based on mode."""
|
||||
# Clear existing params without destroying widgets
|
||||
self._clear_params_layout()
|
||||
|
||||
if mode_id is None:
|
||||
self.params_group.setVisible(False)
|
||||
return
|
||||
|
||||
self.params_group.setVisible(True)
|
||||
|
||||
if mode_id in ("offset_face", "offset_plane"):
|
||||
self.offset_spin.show()
|
||||
self.params_layout.addRow("Offset:", self.offset_spin)
|
||||
elif mode_id == "angled":
|
||||
self.angle_spin.show()
|
||||
self.params_layout.addRow("Angle:", self.angle_spin)
|
||||
elif mode_id == "normal_edge":
|
||||
self.param_spin.show()
|
||||
self.params_layout.addRow("Position (0-1):", self.param_spin)
|
||||
elif mode_id == "tangent_cyl":
|
||||
self.angle_spin.show()
|
||||
self.params_layout.addRow("Angle:", self.angle_spin)
|
||||
elif mode_id == "point_xyz":
|
||||
self.x_spin.show()
|
||||
self.y_spin.show()
|
||||
self.z_spin.show()
|
||||
self.params_layout.addRow("X:", self.x_spin)
|
||||
self.params_layout.addRow("Y:", self.y_spin)
|
||||
self.params_layout.addRow("Z:", self.z_spin)
|
||||
elif mode_id == "point_edge":
|
||||
self.param_spin.show()
|
||||
self.params_layout.addRow("Position (0-1):", self.param_spin)
|
||||
else:
|
||||
# No parameters needed
|
||||
self.params_group.setVisible(False)
|
||||
|
||||
def get_current_mode(self):
|
||||
"""Get the currently active mode ID."""
|
||||
if self.mode_combo.currentIndex() > 0:
|
||||
return self.mode_combo.currentData()
|
||||
|
||||
# Auto-detected mode
|
||||
sel_types = self.get_selection_types()
|
||||
if not sel_types:
|
||||
return None
|
||||
|
||||
best_match = None
|
||||
best_score = -1
|
||||
for _, mode_id, required_types, _ in self.MODES:
|
||||
if not required_types:
|
||||
continue
|
||||
score = self._match_score(sel_types, required_types)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_match = mode_id
|
||||
|
||||
return best_match if best_score > 0 else None
|
||||
|
||||
def get_body(self):
|
||||
"""Get active body if checkbox is checked."""
|
||||
if not self.use_body_cb.isChecked():
|
||||
return None
|
||||
|
||||
if hasattr(Gui, "ActiveDocument") and Gui.ActiveDocument:
|
||||
active_view = Gui.ActiveDocument.ActiveView
|
||||
if hasattr(active_view, "getActiveObject"):
|
||||
body = active_view.getActiveObject("pdbody")
|
||||
if body:
|
||||
return body
|
||||
|
||||
doc = App.ActiveDocument
|
||||
for obj in doc.Objects:
|
||||
if obj.TypeId == "PartDesign::Body":
|
||||
return obj
|
||||
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
"""Get custom name or None for auto-naming."""
|
||||
if self.custom_name_cb.isChecked() and self.custom_name_edit.text():
|
||||
return self.custom_name_edit.text()
|
||||
return None
|
||||
|
||||
def get_items_by_type(self, *geo_types):
|
||||
"""Get selection items matching given geometry types."""
|
||||
results = []
|
||||
for item in self.selection_list:
|
||||
if item.geo_type in geo_types:
|
||||
results.append(item)
|
||||
return results
|
||||
|
||||
def create_datum(self):
|
||||
"""Create the datum based on current settings."""
|
||||
from ztools.datums import core
|
||||
|
||||
mode = self.get_current_mode()
|
||||
if mode is None:
|
||||
raise ValueError("No valid mode detected. Add geometry to the selection.")
|
||||
|
||||
body = self.get_body()
|
||||
name = self.get_name()
|
||||
link_ss = self.link_spreadsheet_cb.isChecked()
|
||||
|
||||
# Planes
|
||||
if mode == "offset_face":
|
||||
items = self.get_items_by_type("face", "cylinder")
|
||||
if not items:
|
||||
raise ValueError("Select a face")
|
||||
item = items[0]
|
||||
face = item.shape if item.shape else item.obj.Shape.Faces[0]
|
||||
core.plane_offset_from_face(
|
||||
face,
|
||||
self.offset_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_object=item.obj,
|
||||
source_subname=item.subname,
|
||||
)
|
||||
|
||||
elif mode == "offset_plane":
|
||||
items = self.get_items_by_type("plane")
|
||||
if not items:
|
||||
raise ValueError("Select a datum plane")
|
||||
core.plane_offset_from_plane(
|
||||
items[0].obj,
|
||||
self.offset_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
)
|
||||
|
||||
elif mode == "midplane":
|
||||
items = self.get_items_by_type("face", "cylinder")
|
||||
if len(items) < 2:
|
||||
raise ValueError("Select 2 faces")
|
||||
face1 = items[0].shape if items[0].shape else items[0].obj.Shape.Faces[0]
|
||||
face2 = items[1].shape if items[1].shape else items[1].obj.Shape.Faces[0]
|
||||
core.plane_midplane(
|
||||
face1,
|
||||
face2,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object1=items[0].obj,
|
||||
source_subname1=items[0].subname,
|
||||
source_object2=items[1].obj,
|
||||
source_subname2=items[1].subname,
|
||||
)
|
||||
|
||||
elif mode == "3_points":
|
||||
items = self.get_items_by_type("vertex")
|
||||
if len(items) < 3:
|
||||
raise ValueError("Select 3 vertices")
|
||||
v1 = items[0].shape if items[0].shape else items[0].obj.Shape.Vertexes[0]
|
||||
v2 = items[1].shape if items[1].shape else items[1].obj.Shape.Vertexes[0]
|
||||
v3 = items[2].shape if items[2].shape else items[2].obj.Shape.Vertexes[0]
|
||||
core.plane_from_3_points(
|
||||
v1.Point,
|
||||
v2.Point,
|
||||
v3.Point,
|
||||
name=name,
|
||||
body=body,
|
||||
source_refs=[
|
||||
(items[0].obj, items[0].subname),
|
||||
(items[1].obj, items[1].subname),
|
||||
(items[2].obj, items[2].subname),
|
||||
],
|
||||
)
|
||||
|
||||
elif mode == "normal_edge":
|
||||
items = self.get_items_by_type("edge", "circle")
|
||||
if not items:
|
||||
raise ValueError("Select an edge")
|
||||
edge = items[0].shape if items[0].shape else items[0].obj.Shape.Edges[0]
|
||||
core.plane_normal_to_edge(
|
||||
edge,
|
||||
parameter=self.param_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "angled":
|
||||
faces = self.get_items_by_type("face", "cylinder")
|
||||
edges = self.get_items_by_type("edge", "circle")
|
||||
if not faces or not edges:
|
||||
raise ValueError("Select a face and an edge")
|
||||
face = faces[0].shape if faces[0].shape else faces[0].obj.Shape.Faces[0]
|
||||
edge = edges[0].shape if edges[0].shape else edges[0].obj.Shape.Edges[0]
|
||||
core.plane_angled(
|
||||
face,
|
||||
edge,
|
||||
self.angle_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_face_obj=faces[0].obj,
|
||||
source_face_sub=faces[0].subname,
|
||||
source_edge_obj=edges[0].obj,
|
||||
source_edge_sub=edges[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "tangent_cyl":
|
||||
items = self.get_items_by_type("cylinder")
|
||||
if not items:
|
||||
raise ValueError("Select a cylindrical face")
|
||||
face = items[0].shape if items[0].shape else items[0].obj.Shape.Faces[0]
|
||||
core.plane_tangent_to_cylinder(
|
||||
face,
|
||||
angle=self.angle_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
# Axes
|
||||
elif mode == "axis_2pt":
|
||||
items = self.get_items_by_type("vertex")
|
||||
if len(items) < 2:
|
||||
raise ValueError("Select 2 vertices")
|
||||
v1 = items[0].shape if items[0].shape else items[0].obj.Shape.Vertexes[0]
|
||||
v2 = items[1].shape if items[1].shape else items[1].obj.Shape.Vertexes[0]
|
||||
core.axis_from_2_points(
|
||||
v1.Point,
|
||||
v2.Point,
|
||||
name=name,
|
||||
body=body,
|
||||
source_refs=[
|
||||
(items[0].obj, items[0].subname),
|
||||
(items[1].obj, items[1].subname),
|
||||
],
|
||||
)
|
||||
|
||||
elif mode == "axis_edge":
|
||||
items = self.get_items_by_type("edge")
|
||||
if not items:
|
||||
raise ValueError("Select a linear edge")
|
||||
edge = items[0].shape if items[0].shape else items[0].obj.Shape.Edges[0]
|
||||
core.axis_from_edge(
|
||||
edge,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "axis_cyl":
|
||||
items = self.get_items_by_type("cylinder")
|
||||
if not items:
|
||||
raise ValueError("Select a cylindrical face")
|
||||
face = items[0].shape if items[0].shape else items[0].obj.Shape.Faces[0]
|
||||
core.axis_cylinder_center(
|
||||
face,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "axis_intersect":
|
||||
items = self.get_items_by_type("plane")
|
||||
if len(items) < 2:
|
||||
raise ValueError("Select 2 datum planes")
|
||||
core.axis_intersection_planes(
|
||||
items[0].obj,
|
||||
items[1].obj,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object1=items[0].obj,
|
||||
source_subname1="",
|
||||
source_object2=items[1].obj,
|
||||
source_subname2="",
|
||||
)
|
||||
|
||||
# Points
|
||||
elif mode == "point_vertex":
|
||||
items = self.get_items_by_type("vertex")
|
||||
if not items:
|
||||
raise ValueError("Select a vertex")
|
||||
vert = items[0].shape if items[0].shape else items[0].obj.Shape.Vertexes[0]
|
||||
core.point_at_vertex(
|
||||
vert,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "point_xyz":
|
||||
core.point_at_coordinates(
|
||||
self.x_spin.value(),
|
||||
self.y_spin.value(),
|
||||
self.z_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
)
|
||||
|
||||
elif mode == "point_edge":
|
||||
items = self.get_items_by_type("edge", "circle")
|
||||
if not items:
|
||||
raise ValueError("Select an edge")
|
||||
edge = items[0].shape if items[0].shape else items[0].obj.Shape.Edges[0]
|
||||
core.point_on_edge(
|
||||
edge,
|
||||
parameter=self.param_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "point_face":
|
||||
items = self.get_items_by_type("face", "cylinder")
|
||||
if not items:
|
||||
raise ValueError("Select a face")
|
||||
face = items[0].shape if items[0].shape else items[0].obj.Shape.Faces[0]
|
||||
core.point_center_of_face(
|
||||
face,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
elif mode == "point_circle":
|
||||
items = self.get_items_by_type("circle")
|
||||
if not items:
|
||||
raise ValueError("Select a circular edge")
|
||||
edge = items[0].shape if items[0].shape else items[0].obj.Shape.Edges[0]
|
||||
core.point_center_of_circle(
|
||||
edge,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=items[0].obj,
|
||||
source_subname=items[0].subname,
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
def accept(self):
|
||||
"""Called when OK is clicked. Creates the datum."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
try:
|
||||
self.create_datum()
|
||||
App.Console.PrintMessage("ZTools: Datum created successfully\n")
|
||||
except Exception as e:
|
||||
App.Console.PrintError(f"ZTools: Failed to create datum: {e}\n")
|
||||
QtGui.QMessageBox.warning(self.form, "Error", str(e))
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Called when Cancel is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class ZTools_DatumCreator:
|
||||
"""Command to open datum creator task panel."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("datum_creator"),
|
||||
"MenuText": "Datum Creator",
|
||||
"ToolTip": "Create datum planes, axes, and points with advanced options",
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
panel = DatumCreatorTaskPanel()
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
class ZTools_DatumManager:
|
||||
"""Command to open datum manager panel."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("datum_manager"),
|
||||
"MenuText": "Datum Manager",
|
||||
"ToolTip": "List, toggle visibility, and rename datums in document",
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
# TODO: Implement datum manager panel
|
||||
App.Console.PrintMessage("ZTools: Datum Manager - Coming soon\n")
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
# Register commands
|
||||
Gui.addCommand("ZTools_DatumCreator", ZTools_DatumCreator())
|
||||
Gui.addCommand("ZTools_DatumManager", ZTools_DatumManager())
|
||||
487
reference/ztools/ztools/ztools/commands/datum_viewprovider.py
Normal file
@@ -0,0 +1,487 @@
|
||||
# ztools/commands/datum_viewprovider.py
|
||||
# Custom ViewProvider for ZTools datum objects
|
||||
|
||||
import json
|
||||
import math
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
class ZToolsDatumViewProvider:
|
||||
"""
|
||||
Custom ViewProvider for ZTools datum objects.
|
||||
|
||||
Features:
|
||||
- Overrides double-click to open ZTools editor instead of vanilla attachment
|
||||
- Hides attachment properties from property editor
|
||||
- Provides custom icons based on datum type
|
||||
"""
|
||||
|
||||
_is_ztools = True # Marker to identify ZTools ViewProviders
|
||||
|
||||
def __init__(self, vobj):
|
||||
"""Initialize and attach to ViewObject."""
|
||||
vobj.Proxy = self
|
||||
self.Object = vobj.Object if vobj else None
|
||||
|
||||
def attach(self, vobj):
|
||||
"""Called when ViewProvider is attached to object."""
|
||||
self.Object = vobj.Object
|
||||
self._hide_attachment_props(vobj)
|
||||
|
||||
def _hide_attachment_props(self, vobj):
|
||||
"""Hide FreeCAD attachment properties using persistent property status."""
|
||||
if not vobj or not vobj.Object:
|
||||
return
|
||||
|
||||
obj = vobj.Object
|
||||
attachment_props = [
|
||||
"MapMode",
|
||||
"MapPathParameter",
|
||||
"MapReversed",
|
||||
"AttachmentOffset",
|
||||
"AttachmentSupport",
|
||||
"Support",
|
||||
]
|
||||
|
||||
for prop in attachment_props:
|
||||
try:
|
||||
if hasattr(obj, prop):
|
||||
obj.setPropertyStatus(prop, "Hidden")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
"""Called when data properties change."""
|
||||
pass
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
"""Called when view properties change."""
|
||||
# Re-hide attachment properties if they become visible
|
||||
if prop in ("MapMode", "Support"):
|
||||
self._hide_attachment_props(vobj)
|
||||
|
||||
def doubleClicked(self, vobj):
|
||||
"""
|
||||
Handle double-click - open ZTools datum editor.
|
||||
Returns True if handled, False to let FreeCAD handle it.
|
||||
"""
|
||||
if Gui.Control.activeDialog():
|
||||
# Task panel already open
|
||||
return False
|
||||
|
||||
# Check if this is a ZTools datum
|
||||
obj = vobj.Object
|
||||
if not hasattr(obj, "ZTools_Type"):
|
||||
# Not a ZTools datum, let FreeCAD handle it
|
||||
return False
|
||||
|
||||
# Open ZTools editor
|
||||
panel = DatumEditTaskPanel(obj)
|
||||
Gui.Control.showDialog(panel)
|
||||
return True
|
||||
|
||||
def setEdit(self, vobj, mode=0):
|
||||
"""
|
||||
Called when entering edit mode.
|
||||
Mode 0 = default edit, Mode 1 = transform
|
||||
"""
|
||||
if mode == 0:
|
||||
obj = vobj.Object
|
||||
if hasattr(obj, "ZTools_Type"):
|
||||
panel = DatumEditTaskPanel(obj)
|
||||
Gui.Control.showDialog(panel)
|
||||
return True
|
||||
return False
|
||||
|
||||
def unsetEdit(self, vobj, mode=0):
|
||||
"""Called when exiting edit mode."""
|
||||
return False
|
||||
|
||||
def getIcon(self):
|
||||
"""Return icon for tree view based on datum type."""
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
if not self.Object:
|
||||
return get_icon("datum_creator")
|
||||
|
||||
ztools_type = getattr(self.Object, "ZTools_Type", "")
|
||||
|
||||
# Map ZTools type to icon
|
||||
icon_map = {
|
||||
"offset_from_face": "plane_offset",
|
||||
"offset_from_plane": "plane_offset",
|
||||
"midplane": "plane_midplane",
|
||||
"3_points": "plane_3pt",
|
||||
"normal_to_edge": "plane_normal",
|
||||
"angled": "plane_angled",
|
||||
"tangent_cylinder": "plane_tangent",
|
||||
"2_points": "axis_2pt",
|
||||
"from_edge": "axis_edge",
|
||||
"cylinder_center": "axis_cyl",
|
||||
"plane_intersection": "axis_intersect",
|
||||
"vertex": "point_vertex",
|
||||
"coordinates": "point_xyz",
|
||||
"on_edge": "point_edge",
|
||||
"face_center": "point_face",
|
||||
"circle_center": "point_circle",
|
||||
}
|
||||
|
||||
icon_name = icon_map.get(ztools_type, "datum_creator")
|
||||
return get_icon(icon_name)
|
||||
|
||||
def __getstate__(self):
|
||||
"""Serialization - don't save proxy state."""
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""Deserialization."""
|
||||
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.
|
||||
|
||||
Allows modification of:
|
||||
- Offset distance (for offset-type datums)
|
||||
- Angle (for angled/tangent datums)
|
||||
- Parameter position (for edge-based datums)
|
||||
- Source references (future)
|
||||
"""
|
||||
|
||||
def __init__(self, datum_obj):
|
||||
self.datum_obj = datum_obj
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle(f"Edit {datum_obj.Label}")
|
||||
self.original_placement = datum_obj.Placement.copy()
|
||||
self.setup_ui()
|
||||
self.load_current_values()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Create the edit panel UI."""
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Header with datum info
|
||||
info_group = QtGui.QGroupBox("Datum Info")
|
||||
info_layout = QtGui.QFormLayout(info_group)
|
||||
|
||||
self.name_edit = QtGui.QLineEdit(self.datum_obj.Label)
|
||||
info_layout.addRow("Name:", self.name_edit)
|
||||
|
||||
ztools_type = getattr(self.datum_obj, "ZTools_Type", "unknown")
|
||||
type_label = QtGui.QLabel(self._format_type_name(ztools_type))
|
||||
type_label.setStyleSheet("color: #cba6f7; font-weight: bold;")
|
||||
info_layout.addRow("Type:", type_label)
|
||||
|
||||
layout.addWidget(info_group)
|
||||
|
||||
# Parameters group
|
||||
self.params_group = QtGui.QGroupBox("Parameters")
|
||||
self.params_layout = QtGui.QFormLayout(self.params_group)
|
||||
|
||||
# Offset spinner
|
||||
self.offset_spin = QtGui.QDoubleSpinBox()
|
||||
self.offset_spin.setRange(-10000, 10000)
|
||||
self.offset_spin.setSuffix(" mm")
|
||||
self.offset_spin.valueChanged.connect(self.on_param_changed)
|
||||
|
||||
# Angle spinner
|
||||
self.angle_spin = QtGui.QDoubleSpinBox()
|
||||
self.angle_spin.setRange(-360, 360)
|
||||
self.angle_spin.setSuffix(" °")
|
||||
self.angle_spin.valueChanged.connect(self.on_param_changed)
|
||||
|
||||
# Parameter spinner (0-1)
|
||||
self.param_spin = QtGui.QDoubleSpinBox()
|
||||
self.param_spin.setRange(0, 1)
|
||||
self.param_spin.setSingleStep(0.1)
|
||||
self.param_spin.valueChanged.connect(self.on_param_changed)
|
||||
|
||||
# XYZ spinners for point coordinates
|
||||
self.x_spin = QtGui.QDoubleSpinBox()
|
||||
self.x_spin.setRange(-10000, 10000)
|
||||
self.x_spin.setSuffix(" mm")
|
||||
self.x_spin.valueChanged.connect(self.on_param_changed)
|
||||
|
||||
self.y_spin = QtGui.QDoubleSpinBox()
|
||||
self.y_spin.setRange(-10000, 10000)
|
||||
self.y_spin.setSuffix(" mm")
|
||||
self.y_spin.valueChanged.connect(self.on_param_changed)
|
||||
|
||||
self.z_spin = QtGui.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 for now)
|
||||
refs_group = QtGui.QGroupBox("Source References")
|
||||
refs_layout = QtGui.QVBoxLayout(refs_group)
|
||||
|
||||
self.refs_list = QtGui.QListWidget()
|
||||
self.refs_list.setMaximumHeight(80)
|
||||
refs_layout.addWidget(self.refs_list)
|
||||
|
||||
layout.addWidget(refs_group)
|
||||
|
||||
# Placement info (read-only)
|
||||
placement_group = QtGui.QGroupBox("Current Placement")
|
||||
placement_layout = QtGui.QFormLayout(placement_group)
|
||||
|
||||
pos = self.datum_obj.Placement.Base
|
||||
self.pos_label = QtGui.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 _format_type_name(self, ztools_type):
|
||||
"""Format ZTools type string for display."""
|
||||
type_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",
|
||||
}
|
||||
return type_names.get(ztools_type, ztools_type)
|
||||
|
||||
def load_current_values(self):
|
||||
"""Load current values from datum object."""
|
||||
ztools_type = getattr(self.datum_obj, "ZTools_Type", "")
|
||||
params_json = getattr(self.datum_obj, "ZTools_Params", "{}")
|
||||
refs_json = getattr(self.datum_obj, "ZTools_SourceRefs", "[]")
|
||||
|
||||
try:
|
||||
params = json.loads(params_json)
|
||||
except json.JSONDecodeError:
|
||||
params = {}
|
||||
|
||||
try:
|
||||
refs = json.loads(refs_json)
|
||||
except json.JSONDecodeError:
|
||||
refs = []
|
||||
|
||||
# Clear params layout
|
||||
while self.params_layout.rowCount() > 0:
|
||||
self.params_layout.removeRow(0)
|
||||
|
||||
# Add appropriate parameter controls based on type
|
||||
if ztools_type in ("offset_from_face", "offset_from_plane"):
|
||||
distance = params.get("distance", 10)
|
||||
self.offset_spin.setValue(distance)
|
||||
self.params_layout.addRow("Offset:", self.offset_spin)
|
||||
|
||||
elif ztools_type == "angled":
|
||||
angle = params.get("angle", 45)
|
||||
self.angle_spin.setValue(angle)
|
||||
self.params_layout.addRow("Angle:", self.angle_spin)
|
||||
|
||||
elif ztools_type == "tangent_cylinder":
|
||||
angle = params.get("angle", 0)
|
||||
self.angle_spin.setValue(angle)
|
||||
self.params_layout.addRow("Angle:", self.angle_spin)
|
||||
|
||||
elif ztools_type in ("normal_to_edge", "on_edge"):
|
||||
parameter = params.get("parameter", 0.5)
|
||||
self.param_spin.setValue(parameter)
|
||||
self.params_layout.addRow("Position (0-1):", self.param_spin)
|
||||
|
||||
elif ztools_type == "coordinates":
|
||||
x = params.get("x", 0)
|
||||
y = params.get("y", 0)
|
||||
z = params.get("z", 0)
|
||||
self.x_spin.setValue(x)
|
||||
self.y_spin.setValue(y)
|
||||
self.z_spin.setValue(z)
|
||||
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:
|
||||
# No editable parameters
|
||||
no_params_label = QtGui.QLabel("No editable parameters")
|
||||
no_params_label.setStyleSheet("color: #888;")
|
||||
self.params_layout.addRow(no_params_label)
|
||||
|
||||
# Load source references
|
||||
self.refs_list.clear()
|
||||
for ref in refs:
|
||||
obj_name = ref.get("object", "?")
|
||||
subname = ref.get("subname", "")
|
||||
if subname:
|
||||
self.refs_list.addItem(f"{obj_name}.{subname}")
|
||||
else:
|
||||
self.refs_list.addItem(obj_name)
|
||||
|
||||
if not refs:
|
||||
self.refs_list.addItem("(no references)")
|
||||
|
||||
def _has_attachment(self):
|
||||
"""Check if datum uses vanilla AttachExtension (MapMode != Deactivated)."""
|
||||
return (
|
||||
hasattr(self.datum_obj, "MapMode")
|
||||
and self.datum_obj.MapMode != "Deactivated"
|
||||
)
|
||||
|
||||
def on_param_changed(self):
|
||||
"""Handle parameter value changes - update datum in real-time.
|
||||
|
||||
For datums with active AttachExtension, writes to AttachmentOffset or
|
||||
MapPathParameter — the C++ engine recalculates placement automatically.
|
||||
For manual datums, updates Placement directly.
|
||||
"""
|
||||
ztools_type = getattr(self.datum_obj, "ZTools_Type", "")
|
||||
|
||||
# For coordinate-based points, update placement directly
|
||||
if ztools_type == "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._update_params({"x": new_pos.x, "y": new_pos.y, "z": new_pos.z})
|
||||
|
||||
elif ztools_type in ("offset_from_face", "offset_from_plane", "midplane"):
|
||||
distance = self.offset_spin.value()
|
||||
if self._has_attachment():
|
||||
new_offset = App.Placement(App.Vector(0, 0, distance), App.Rotation())
|
||||
self.datum_obj.AttachmentOffset = new_offset
|
||||
self._update_params({"distance": distance})
|
||||
|
||||
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()
|
||||
if self._has_attachment():
|
||||
params_json = getattr(self.datum_obj, "ZTools_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 = 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()
|
||||
if self._has_attachment() and hasattr(self.datum_obj, "MapPathParameter"):
|
||||
self.datum_obj.MapPathParameter = parameter
|
||||
self._update_params({"parameter": parameter})
|
||||
|
||||
# Update position display
|
||||
pos = self.datum_obj.Placement.Base
|
||||
self.pos_label.setText(f"({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f})")
|
||||
|
||||
App.ActiveDocument.recompute()
|
||||
|
||||
def _update_params(self, new_values):
|
||||
"""Update stored parameters with new values."""
|
||||
params_json = getattr(self.datum_obj, "ZTools_Params", "{}")
|
||||
try:
|
||||
params = json.loads(params_json)
|
||||
except json.JSONDecodeError:
|
||||
params = {}
|
||||
|
||||
params.update(new_values)
|
||||
|
||||
# Re-serialize (handle vectors)
|
||||
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
|
||||
|
||||
self.datum_obj.ZTools_Params = json.dumps(serializable)
|
||||
|
||||
def accept(self):
|
||||
"""Handle OK button - apply changes."""
|
||||
# Update label if changed
|
||||
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):
|
||||
"""Handle Cancel button - restore original placement."""
|
||||
self.datum_obj.Placement = self.original_placement
|
||||
App.ActiveDocument.recompute()
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
"""Return dialog buttons."""
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
206
reference/ztools/ztools/ztools/commands/pattern_commands.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# ztools/commands/pattern_commands.py
|
||||
# Rotated Linear Pattern command
|
||||
# Creates a linear pattern with incremental rotation for each instance
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
|
||||
class RotatedLinearPatternFeature:
|
||||
"""Feature object for rotated linear pattern."""
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.Proxy = self
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyLink", "Source", "Base", "Source object to pattern"
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"Direction",
|
||||
"Pattern",
|
||||
"Direction of the linear pattern",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance", "Length", "Pattern", "Total length of the pattern"
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"Occurrences",
|
||||
"Pattern",
|
||||
"Number of occurrences (including original)",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"RotationAxis",
|
||||
"Rotation",
|
||||
"Axis of rotation for each instance",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyAngle",
|
||||
"RotationAngle",
|
||||
"Rotation",
|
||||
"Rotation angle increment per instance",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"RotationCenter",
|
||||
"Rotation",
|
||||
"Center point for rotation (relative to each instance)",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"CumulativeRotation",
|
||||
"Rotation",
|
||||
"If true, rotation accumulates with each instance",
|
||||
)
|
||||
|
||||
# Set defaults
|
||||
obj.Direction = App.Vector(1, 0, 0)
|
||||
obj.Length = 100.0
|
||||
obj.Occurrences = 3
|
||||
obj.RotationAxis = App.Vector(0, 0, 1)
|
||||
obj.RotationAngle = 15.0
|
||||
obj.RotationCenter = App.Vector(0, 0, 0)
|
||||
obj.CumulativeRotation = True
|
||||
|
||||
# Store metadata for ztools tracking
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"ZTools_Type",
|
||||
"ZTools",
|
||||
"ZTools feature type",
|
||||
)
|
||||
obj.ZTools_Type = "RotatedLinearPattern"
|
||||
|
||||
def execute(self, obj):
|
||||
"""Recompute the feature."""
|
||||
if not obj.Source or not hasattr(obj.Source, "Shape"):
|
||||
return
|
||||
|
||||
source_shape = obj.Source.Shape
|
||||
if source_shape.isNull():
|
||||
return
|
||||
|
||||
occurrences = max(1, obj.Occurrences)
|
||||
if occurrences == 1:
|
||||
obj.Shape = source_shape.copy()
|
||||
return
|
||||
|
||||
# Calculate spacing
|
||||
direction = App.Vector(obj.Direction)
|
||||
if direction.Length < 1e-6:
|
||||
direction = App.Vector(1, 0, 0)
|
||||
direction.normalize()
|
||||
|
||||
spacing = float(obj.Length) / (occurrences - 1) if occurrences > 1 else 0
|
||||
|
||||
shapes = []
|
||||
for i in range(occurrences):
|
||||
# Create translation
|
||||
offset = direction * spacing * i
|
||||
translated = source_shape.copy()
|
||||
translated.translate(offset)
|
||||
|
||||
# Apply rotation
|
||||
if abs(float(obj.RotationAngle)) > 1e-6:
|
||||
if obj.CumulativeRotation:
|
||||
angle = float(obj.RotationAngle) * i
|
||||
else:
|
||||
angle = float(obj.RotationAngle)
|
||||
|
||||
# Rotation center is relative to the translated position
|
||||
center = App.Vector(obj.RotationCenter) + offset
|
||||
axis = App.Vector(obj.RotationAxis)
|
||||
if axis.Length < 1e-6:
|
||||
axis = App.Vector(0, 0, 1)
|
||||
axis.normalize()
|
||||
|
||||
translated.rotate(center, axis, angle)
|
||||
|
||||
shapes.append(translated)
|
||||
|
||||
if shapes:
|
||||
obj.Shape = Part.makeCompound(shapes)
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
"""Handle property changes."""
|
||||
pass
|
||||
|
||||
|
||||
class RotatedLinearPatternViewProvider:
|
||||
"""View provider for rotated linear pattern."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
|
||||
def attach(self, vobj):
|
||||
self.Object = vobj.Object
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
pass
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
pass
|
||||
|
||||
def getIcon(self):
|
||||
return get_icon("rotated_pattern")
|
||||
|
||||
def __getstate__(self):
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
return None
|
||||
|
||||
|
||||
class RotatedLinearPatternCommand:
|
||||
"""Command to create a rotated linear pattern."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("rotated_pattern"),
|
||||
"MenuText": "Rotated Linear Pattern",
|
||||
"ToolTip": "Create a linear pattern with rotation for each instance",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
"""Command is active when there's a document and selection."""
|
||||
if App.ActiveDocument is None:
|
||||
return False
|
||||
sel = Gui.Selection.getSelection()
|
||||
return len(sel) == 1
|
||||
|
||||
def Activated(self):
|
||||
"""Execute the command."""
|
||||
sel = Gui.Selection.getSelection()
|
||||
if not sel:
|
||||
App.Console.PrintError("Please select an object first\n")
|
||||
return
|
||||
|
||||
source = sel[0]
|
||||
|
||||
# Create the feature
|
||||
doc = App.ActiveDocument
|
||||
obj = doc.addObject("Part::FeaturePython", "RotatedLinearPattern")
|
||||
RotatedLinearPatternFeature(obj)
|
||||
RotatedLinearPatternViewProvider(obj.ViewObject)
|
||||
|
||||
obj.Source = source
|
||||
obj.Label = f"RotatedPattern_{source.Label}"
|
||||
|
||||
# Hide source object
|
||||
if hasattr(source, "ViewObject"):
|
||||
source.ViewObject.Visibility = False
|
||||
|
||||
doc.recompute()
|
||||
|
||||
App.Console.PrintMessage(
|
||||
f"Created rotated linear pattern from {source.Label}\n"
|
||||
)
|
||||
|
||||
|
||||
# Register the command
|
||||
Gui.addCommand("ZTools_RotatedLinearPattern", RotatedLinearPatternCommand())
|
||||
601
reference/ztools/ztools/ztools/commands/pocket_commands.py
Normal file
@@ -0,0 +1,601 @@
|
||||
# ztools/commands/pocket_commands.py
|
||||
# Enhanced Pocket feature with "Flip side to cut" option
|
||||
#
|
||||
# This provides an enhanced pocket workflow that includes the ability to
|
||||
# cut material OUTSIDE the sketch profile rather than inside (like SOLIDWORKS
|
||||
# "Flip side to cut" feature).
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
class EnhancedPocketTaskPanel:
|
||||
"""Task panel for creating enhanced pocket features with flip option."""
|
||||
|
||||
# Pocket type modes matching FreeCAD's PartDesign::Pocket
|
||||
POCKET_TYPES = [
|
||||
("Dimension", 0),
|
||||
("Through All", 1),
|
||||
("To First", 2),
|
||||
("Up To Face", 3),
|
||||
("Two Dimensions", 4),
|
||||
]
|
||||
|
||||
def __init__(self, sketch=None):
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle("ztools Enhanced Pocket")
|
||||
self.sketch = sketch
|
||||
self.selected_face = None
|
||||
self.setup_ui()
|
||||
self.setup_selection_observer()
|
||||
|
||||
# If sketch provided, show it in selection
|
||||
if self.sketch:
|
||||
self.update_sketch_display()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Sketch selection display
|
||||
sketch_group = QtGui.QGroupBox("Sketch")
|
||||
sketch_layout = QtGui.QVBoxLayout(sketch_group)
|
||||
self.sketch_label = QtGui.QLabel("No sketch selected")
|
||||
self.sketch_label.setWordWrap(True)
|
||||
sketch_layout.addWidget(self.sketch_label)
|
||||
layout.addWidget(sketch_group)
|
||||
|
||||
# Type selection
|
||||
type_group = QtGui.QGroupBox("Type")
|
||||
type_layout = QtGui.QFormLayout(type_group)
|
||||
|
||||
self.type_combo = QtGui.QComboBox()
|
||||
for label, _ in self.POCKET_TYPES:
|
||||
self.type_combo.addItem(label)
|
||||
self.type_combo.currentIndexChanged.connect(self.on_type_changed)
|
||||
type_layout.addRow("Type:", self.type_combo)
|
||||
|
||||
layout.addWidget(type_group)
|
||||
|
||||
# Dimensions group
|
||||
self.dim_group = QtGui.QGroupBox("Dimensions")
|
||||
self.dim_layout = QtGui.QFormLayout(self.dim_group)
|
||||
|
||||
# Length input
|
||||
self.length_spin = QtGui.QDoubleSpinBox()
|
||||
self.length_spin.setRange(0.001, 10000)
|
||||
self.length_spin.setValue(10.0)
|
||||
self.length_spin.setSuffix(" mm")
|
||||
self.length_spin.setDecimals(3)
|
||||
self.dim_layout.addRow("Length:", self.length_spin)
|
||||
|
||||
# Length2 input (for Two Dimensions mode)
|
||||
self.length2_spin = QtGui.QDoubleSpinBox()
|
||||
self.length2_spin.setRange(0.001, 10000)
|
||||
self.length2_spin.setValue(10.0)
|
||||
self.length2_spin.setSuffix(" mm")
|
||||
self.length2_spin.setDecimals(3)
|
||||
self.length2_label = QtGui.QLabel("Length 2:")
|
||||
# Hidden by default
|
||||
self.length2_spin.setVisible(False)
|
||||
self.length2_label.setVisible(False)
|
||||
self.dim_layout.addRow(self.length2_label, self.length2_spin)
|
||||
|
||||
layout.addWidget(self.dim_group)
|
||||
|
||||
# Up To Face selection (hidden by default)
|
||||
self.face_group = QtGui.QGroupBox("Up To Face")
|
||||
face_layout = QtGui.QVBoxLayout(self.face_group)
|
||||
self.face_label = QtGui.QLabel("Select a face...")
|
||||
self.face_label.setWordWrap(True)
|
||||
face_layout.addWidget(self.face_label)
|
||||
self.face_group.setVisible(False)
|
||||
layout.addWidget(self.face_group)
|
||||
|
||||
# Direction options
|
||||
dir_group = QtGui.QGroupBox("Direction")
|
||||
dir_layout = QtGui.QVBoxLayout(dir_group)
|
||||
|
||||
self.reversed_cb = QtGui.QCheckBox("Reversed")
|
||||
self.reversed_cb.setToolTip("Reverse the pocket direction")
|
||||
dir_layout.addWidget(self.reversed_cb)
|
||||
|
||||
self.symmetric_cb = QtGui.QCheckBox("Symmetric to plane")
|
||||
self.symmetric_cb.setToolTip(
|
||||
"Extend pocket equally on both sides of sketch plane"
|
||||
)
|
||||
self.symmetric_cb.toggled.connect(self.on_symmetric_changed)
|
||||
dir_layout.addWidget(self.symmetric_cb)
|
||||
|
||||
layout.addWidget(dir_group)
|
||||
|
||||
# FLIP SIDE TO CUT - The main new feature
|
||||
flip_group = QtGui.QGroupBox("Flip Side to Cut")
|
||||
flip_layout = QtGui.QVBoxLayout(flip_group)
|
||||
|
||||
self.flipped_cb = QtGui.QCheckBox("Cut outside profile (keep inside)")
|
||||
self.flipped_cb.setToolTip(
|
||||
"Instead of removing material inside the sketch profile,\n"
|
||||
"remove material OUTSIDE the profile.\n\n"
|
||||
"This keeps only the material covered by the sketch,\n"
|
||||
"similar to SOLIDWORKS 'Flip side to cut' option."
|
||||
)
|
||||
flip_layout.addWidget(self.flipped_cb)
|
||||
|
||||
# Info label
|
||||
flip_info = QtGui.QLabel(
|
||||
"<i>When enabled, material outside the sketch profile is removed,\n"
|
||||
"leaving only the material inside the sketch boundary.</i>"
|
||||
)
|
||||
flip_info.setWordWrap(True)
|
||||
flip_info.setStyleSheet("color: gray; font-size: 10px;")
|
||||
flip_layout.addWidget(flip_info)
|
||||
|
||||
layout.addWidget(flip_group)
|
||||
|
||||
# Taper angle (optional)
|
||||
taper_group = QtGui.QGroupBox("Taper")
|
||||
taper_layout = QtGui.QFormLayout(taper_group)
|
||||
|
||||
self.taper_spin = QtGui.QDoubleSpinBox()
|
||||
self.taper_spin.setRange(-89.99, 89.99)
|
||||
self.taper_spin.setValue(0.0)
|
||||
self.taper_spin.setSuffix(" °")
|
||||
self.taper_spin.setDecimals(2)
|
||||
taper_layout.addRow("Taper Angle:", self.taper_spin)
|
||||
|
||||
layout.addWidget(taper_group)
|
||||
|
||||
# Create button
|
||||
self.create_btn = QtGui.QPushButton("Create Pocket")
|
||||
self.create_btn.clicked.connect(self.on_create)
|
||||
layout.addWidget(self.create_btn)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Setup selection observer to track user selections."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def clearSelection(self, doc):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_selection_changed(self):
|
||||
"""Handle selection changes."""
|
||||
sel = Gui.Selection.getSelectionEx()
|
||||
|
||||
for s in sel:
|
||||
obj = s.Object
|
||||
|
||||
# Check if it's a sketch (for sketch selection)
|
||||
if obj.TypeId == "Sketcher::SketchObject" and not self.sketch:
|
||||
self.sketch = obj
|
||||
self.update_sketch_display()
|
||||
|
||||
# Check for face selection (for Up To Face mode)
|
||||
if s.SubElementNames:
|
||||
for sub in s.SubElementNames:
|
||||
if sub.startswith("Face"):
|
||||
shape = obj.Shape.getElement(sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
self.selected_face = (obj, sub)
|
||||
self.face_label.setText(f"Face: {obj.Name}.{sub}")
|
||||
|
||||
def update_sketch_display(self):
|
||||
"""Update sketch label."""
|
||||
if self.sketch:
|
||||
self.sketch_label.setText(f"Sketch: {self.sketch.Label}")
|
||||
else:
|
||||
self.sketch_label.setText("No sketch selected")
|
||||
|
||||
def on_type_changed(self, index):
|
||||
"""Update UI based on pocket type."""
|
||||
pocket_type = self.POCKET_TYPES[index][1]
|
||||
|
||||
# Show/hide dimension inputs based on type
|
||||
show_length = pocket_type in [0, 4] # Dimension or Two Dimensions
|
||||
self.dim_group.setVisible(show_length or pocket_type == 4)
|
||||
self.length_spin.setVisible(show_length)
|
||||
|
||||
# Two Dimensions mode
|
||||
show_length2 = pocket_type == 4
|
||||
self.length2_spin.setVisible(show_length2)
|
||||
self.length2_label.setVisible(show_length2)
|
||||
|
||||
# Up To Face mode
|
||||
self.face_group.setVisible(pocket_type == 3)
|
||||
|
||||
def on_symmetric_changed(self, checked):
|
||||
"""Handle symmetric checkbox change."""
|
||||
if checked:
|
||||
self.reversed_cb.setEnabled(False)
|
||||
self.reversed_cb.setChecked(False)
|
||||
else:
|
||||
self.reversed_cb.setEnabled(True)
|
||||
|
||||
def get_body(self):
|
||||
"""Get the active PartDesign body."""
|
||||
if hasattr(Gui, "ActiveDocument") and Gui.ActiveDocument:
|
||||
active_view = Gui.ActiveDocument.ActiveView
|
||||
if hasattr(active_view, "getActiveObject"):
|
||||
body = active_view.getActiveObject("pdbody")
|
||||
if body:
|
||||
return body
|
||||
|
||||
# Fallback: find body containing the sketch
|
||||
if self.sketch:
|
||||
for obj in App.ActiveDocument.Objects:
|
||||
if obj.TypeId == "PartDesign::Body":
|
||||
if self.sketch in obj.Group:
|
||||
return obj
|
||||
|
||||
return None
|
||||
|
||||
def on_create(self):
|
||||
"""Create the pocket feature."""
|
||||
if not self.sketch:
|
||||
QtGui.QMessageBox.warning(
|
||||
self.form, "Error", "Please select a sketch first."
|
||||
)
|
||||
return
|
||||
|
||||
body = self.get_body()
|
||||
if not body:
|
||||
QtGui.QMessageBox.warning(
|
||||
self.form, "Error", "No active body found. Please activate a body."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
App.ActiveDocument.openTransaction("Create Enhanced Pocket")
|
||||
|
||||
flipped = self.flipped_cb.isChecked()
|
||||
|
||||
if flipped:
|
||||
self.create_flipped_pocket(body)
|
||||
else:
|
||||
self.create_standard_pocket(body)
|
||||
|
||||
App.ActiveDocument.commitTransaction()
|
||||
App.ActiveDocument.recompute()
|
||||
|
||||
App.Console.PrintMessage("Enhanced Pocket created successfully\n")
|
||||
|
||||
except Exception as e:
|
||||
App.ActiveDocument.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to create pocket: {e}\n")
|
||||
QtGui.QMessageBox.critical(self.form, "Error", str(e))
|
||||
|
||||
def create_standard_pocket(self, body):
|
||||
"""Create a standard PartDesign Pocket."""
|
||||
pocket = body.newObject("PartDesign::Pocket", "Pocket")
|
||||
pocket.Profile = self.sketch
|
||||
|
||||
# Set type
|
||||
pocket_type = self.POCKET_TYPES[self.type_combo.currentIndex()][1]
|
||||
pocket.Type = pocket_type
|
||||
|
||||
# Set dimensions
|
||||
pocket.Length = self.length_spin.value()
|
||||
if pocket_type == 4: # Two Dimensions
|
||||
pocket.Length2 = self.length2_spin.value()
|
||||
|
||||
# Set direction options
|
||||
pocket.Reversed = self.reversed_cb.isChecked()
|
||||
pocket.Midplane = self.symmetric_cb.isChecked()
|
||||
|
||||
# Set taper
|
||||
if abs(self.taper_spin.value()) > 0.001:
|
||||
pocket.TaperAngle = self.taper_spin.value()
|
||||
|
||||
# Up To Face
|
||||
if pocket_type == 3 and self.selected_face:
|
||||
obj, sub = self.selected_face
|
||||
pocket.UpToFace = (obj, [sub])
|
||||
|
||||
# Hide sketch
|
||||
self.sketch.ViewObject.Visibility = False
|
||||
|
||||
# Add metadata
|
||||
pocket.addProperty(
|
||||
"App::PropertyString", "ZTools_Type", "ZTools", "ZTools feature type"
|
||||
)
|
||||
pocket.ZTools_Type = "EnhancedPocket"
|
||||
|
||||
def create_flipped_pocket(self, body):
|
||||
"""Create a flipped pocket (cut outside profile).
|
||||
|
||||
This uses Boolean Common operation: keeps only the intersection
|
||||
of the body with the extruded profile.
|
||||
"""
|
||||
# Get current body shape (the Tip)
|
||||
tip = body.Tip
|
||||
if not tip or not hasattr(tip, "Shape"):
|
||||
raise ValueError("Body has no valid tip shape")
|
||||
|
||||
base_shape = tip.Shape.copy()
|
||||
|
||||
# Get sketch profile
|
||||
sketch_shape = self.sketch.Shape
|
||||
if not sketch_shape.Wires:
|
||||
raise ValueError("Sketch has no closed profile")
|
||||
|
||||
# Create face from sketch wires
|
||||
wires = sketch_shape.Wires
|
||||
if len(wires) == 0:
|
||||
raise ValueError("Sketch has no wires")
|
||||
|
||||
# Create a face from the outer wire
|
||||
# For multiple wires, the first is outer, rest are holes
|
||||
face = Part.Face(wires[0])
|
||||
if len(wires) > 1:
|
||||
# Handle holes in the profile
|
||||
face = Part.Face(wires)
|
||||
|
||||
# Get extrusion direction (sketch normal)
|
||||
sketch_placement = self.sketch.Placement
|
||||
normal = sketch_placement.Rotation.multVec(App.Vector(0, 0, 1))
|
||||
|
||||
if self.reversed_cb.isChecked():
|
||||
normal = normal.negative()
|
||||
|
||||
# Calculate extrusion length/direction
|
||||
pocket_type = self.POCKET_TYPES[self.type_combo.currentIndex()][1]
|
||||
|
||||
if pocket_type == 0: # Dimension
|
||||
length = self.length_spin.value()
|
||||
if self.symmetric_cb.isChecked():
|
||||
# Symmetric: extrude half in each direction
|
||||
half_length = length / 2
|
||||
tool_solid = face.extrude(normal * half_length)
|
||||
tool_solid2 = face.extrude(normal.negative() * half_length)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
elif pocket_type == 1: # Through All
|
||||
# Use a large value based on bounding box
|
||||
bbox = base_shape.BoundBox
|
||||
diagonal = bbox.DiagonalLength
|
||||
length = diagonal * 2
|
||||
|
||||
if self.symmetric_cb.isChecked():
|
||||
tool_solid = face.extrude(normal * length)
|
||||
tool_solid2 = face.extrude(normal.negative() * length)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
elif pocket_type == 4: # Two Dimensions
|
||||
length1 = self.length_spin.value()
|
||||
length2 = self.length2_spin.value()
|
||||
tool_solid = face.extrude(normal * length1)
|
||||
tool_solid2 = face.extrude(normal.negative() * length2)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
|
||||
else:
|
||||
# For other types, fall back to Through All behavior
|
||||
bbox = base_shape.BoundBox
|
||||
length = bbox.DiagonalLength * 2
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
# Apply taper if specified
|
||||
# Note: Taper with flipped pocket is complex, skip for now
|
||||
if abs(self.taper_spin.value()) > 0.001:
|
||||
App.Console.PrintWarning(
|
||||
"Taper angle is not supported with Flip Side to Cut. Ignoring.\n"
|
||||
)
|
||||
|
||||
# Boolean Common: keep only intersection
|
||||
result_shape = base_shape.common(tool_solid)
|
||||
|
||||
if result_shape.isNull() or result_shape.Volume < 1e-6:
|
||||
raise ValueError(
|
||||
"Flip pocket resulted in empty shape. "
|
||||
"Make sure the sketch profile intersects with the body."
|
||||
)
|
||||
|
||||
# Create a FeaturePython object to hold the result
|
||||
feature = body.newObject("PartDesign::FeaturePython", "FlippedPocket")
|
||||
|
||||
# Set up the feature
|
||||
FlippedPocketFeature(feature, self.sketch, result_shape)
|
||||
FlippedPocketViewProvider(feature.ViewObject)
|
||||
|
||||
# Store parameters as properties
|
||||
feature.addProperty("App::PropertyDistance", "Length", "Pocket", "Pocket depth")
|
||||
feature.Length = self.length_spin.value()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyBool", "Reversed", "Pocket", "Reverse direction"
|
||||
)
|
||||
feature.Reversed = self.reversed_cb.isChecked()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyBool", "Symmetric", "Pocket", "Symmetric to plane"
|
||||
)
|
||||
feature.Symmetric = self.symmetric_cb.isChecked()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyInteger", "PocketType", "Pocket", "Pocket type"
|
||||
)
|
||||
feature.PocketType = pocket_type
|
||||
|
||||
# Hide sketch
|
||||
self.sketch.ViewObject.Visibility = False
|
||||
|
||||
def accept(self):
|
||||
"""Called when OK is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Called when Cancel is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class FlippedPocketFeature:
|
||||
"""Feature object for flipped pocket (cuts outside profile)."""
|
||||
|
||||
def __init__(self, obj, sketch, initial_shape):
|
||||
obj.Proxy = self
|
||||
self.sketch = sketch
|
||||
|
||||
# Store reference to sketch
|
||||
obj.addProperty("App::PropertyLink", "Profile", "Base", "Sketch profile")
|
||||
obj.Profile = sketch
|
||||
|
||||
# ZTools metadata
|
||||
obj.addProperty(
|
||||
"App::PropertyString", "ZTools_Type", "ZTools", "ZTools feature type"
|
||||
)
|
||||
obj.ZTools_Type = "FlippedPocket"
|
||||
|
||||
# Set initial shape
|
||||
obj.Shape = initial_shape
|
||||
|
||||
def execute(self, obj):
|
||||
"""Recompute the flipped pocket."""
|
||||
if not obj.Profile:
|
||||
return
|
||||
|
||||
# Get the base feature (previous feature in the body)
|
||||
body = obj.getParentGeoFeatureGroup()
|
||||
if not body:
|
||||
return
|
||||
|
||||
# Find the feature before this one
|
||||
base_feature = None
|
||||
group = body.Group
|
||||
for i, feat in enumerate(group):
|
||||
if feat == obj and i > 0:
|
||||
base_feature = group[i - 1]
|
||||
break
|
||||
|
||||
if not base_feature or not hasattr(base_feature, "Shape"):
|
||||
return
|
||||
|
||||
base_shape = base_feature.Shape.copy()
|
||||
sketch = obj.Profile
|
||||
|
||||
# Get sketch profile
|
||||
sketch_shape = sketch.Shape
|
||||
if not sketch_shape.Wires:
|
||||
return
|
||||
|
||||
wires = sketch_shape.Wires
|
||||
face = Part.Face(wires[0])
|
||||
if len(wires) > 1:
|
||||
face = Part.Face(wires)
|
||||
|
||||
# Get direction
|
||||
sketch_placement = sketch.Placement
|
||||
normal = sketch_placement.Rotation.multVec(App.Vector(0, 0, 1))
|
||||
|
||||
if hasattr(obj, "Reversed") and obj.Reversed:
|
||||
normal = normal.negative()
|
||||
|
||||
# Get length
|
||||
length = obj.Length.Value if hasattr(obj, "Length") else 10.0
|
||||
symmetric = obj.Symmetric if hasattr(obj, "Symmetric") else False
|
||||
pocket_type = obj.PocketType if hasattr(obj, "PocketType") else 0
|
||||
|
||||
# Create tool solid
|
||||
if pocket_type == 1: # Through All
|
||||
bbox = base_shape.BoundBox
|
||||
length = bbox.DiagonalLength * 2
|
||||
|
||||
if symmetric:
|
||||
half = length / 2
|
||||
tool_solid = face.extrude(normal * half)
|
||||
tool_solid2 = face.extrude(normal.negative() * half)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
# Boolean Common
|
||||
result_shape = base_shape.common(tool_solid)
|
||||
obj.Shape = result_shape
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
"""Handle property changes."""
|
||||
pass
|
||||
|
||||
|
||||
class FlippedPocketViewProvider:
|
||||
"""View provider for flipped pocket."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
|
||||
def attach(self, vobj):
|
||||
self.Object = vobj.Object
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
pass
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
pass
|
||||
|
||||
def getIcon(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return get_icon("pocket_flipped")
|
||||
|
||||
def __getstate__(self):
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
return None
|
||||
|
||||
|
||||
class ZTools_EnhancedPocket:
|
||||
"""Command to create enhanced pocket with flip option."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("pocket_enhanced"),
|
||||
"MenuText": "Enhanced Pocket",
|
||||
"ToolTip": (
|
||||
"Create a pocket with additional options including\n"
|
||||
"'Flip side to cut' - removes material outside the sketch profile"
|
||||
),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
# Check if a sketch is selected
|
||||
sketch = None
|
||||
sel = Gui.Selection.getSelection()
|
||||
for obj in sel:
|
||||
if obj.TypeId == "Sketcher::SketchObject":
|
||||
sketch = obj
|
||||
break
|
||||
|
||||
panel = EnhancedPocketTaskPanel(sketch)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
# Register the command
|
||||
Gui.addCommand("ZTools_EnhancedPocket", ZTools_EnhancedPocket())
|
||||
567
reference/ztools/ztools/ztools/commands/spreadsheet_commands.py
Normal file
@@ -0,0 +1,567 @@
|
||||
# ztools/commands/spreadsheet_commands.py
|
||||
# Enhanced spreadsheet formatting tools for FreeCAD
|
||||
# Provides quick formatting toolbar for cell styling
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_active_spreadsheet():
|
||||
"""Get the currently active spreadsheet object and its view.
|
||||
|
||||
Returns:
|
||||
tuple: (sheet_object, sheet_view) or (None, None)
|
||||
"""
|
||||
doc = App.ActiveDocument
|
||||
if not doc:
|
||||
return None, None
|
||||
|
||||
# Get MDI area and active subwindow
|
||||
main_window = Gui.getMainWindow()
|
||||
mdi = main_window.centralWidget()
|
||||
if not mdi:
|
||||
return None, None
|
||||
|
||||
subwindow = mdi.activeSubWindow()
|
||||
if not subwindow:
|
||||
return None, None
|
||||
|
||||
# Get widget from subwindow
|
||||
widget = subwindow.widget()
|
||||
if not widget:
|
||||
return None, None
|
||||
|
||||
# Check if it's a spreadsheet view by looking for the table view
|
||||
# FreeCAD's spreadsheet view contains a QTableView
|
||||
table_view = None
|
||||
if hasattr(widget, "findChild"):
|
||||
table_view = widget.findChild(QtGui.QTableView)
|
||||
|
||||
if not table_view:
|
||||
# Try if widget itself is the table view
|
||||
if isinstance(widget, QtGui.QTableView):
|
||||
table_view = widget
|
||||
else:
|
||||
return None, None
|
||||
|
||||
# Get the spreadsheet object from window title
|
||||
# Window title format varies: "Spreadsheet" or "Spreadsheet - DocName"
|
||||
title = subwindow.windowTitle()
|
||||
sheet_name = title.split(" - ")[0].split(" : ")[0].strip()
|
||||
|
||||
# Try to find the sheet object
|
||||
sheet = doc.getObject(sheet_name)
|
||||
if sheet and sheet.TypeId == "Spreadsheet::Sheet":
|
||||
return sheet, table_view
|
||||
|
||||
# Fallback: search for any spreadsheet object
|
||||
for obj in doc.Objects:
|
||||
if obj.TypeId == "Spreadsheet::Sheet":
|
||||
return obj, table_view
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def get_selected_cells():
|
||||
"""Get list of selected cell addresses from active spreadsheet.
|
||||
|
||||
Returns:
|
||||
tuple: (sheet_object, list_of_cell_addresses) or (None, [])
|
||||
"""
|
||||
sheet, table_view = get_active_spreadsheet()
|
||||
if not sheet or not table_view:
|
||||
return None, []
|
||||
|
||||
# Get selection model
|
||||
selection_model = table_view.selectionModel()
|
||||
if not selection_model:
|
||||
return sheet, []
|
||||
|
||||
indexes = selection_model.selectedIndexes()
|
||||
if not indexes:
|
||||
return sheet, []
|
||||
|
||||
cells = []
|
||||
for idx in indexes:
|
||||
col = idx.column()
|
||||
row = idx.row()
|
||||
|
||||
# Convert to cell address (A1 notation)
|
||||
# Handle columns beyond Z (AA, AB, etc.)
|
||||
col_str = ""
|
||||
temp_col = col
|
||||
while temp_col >= 0:
|
||||
col_str = chr(65 + (temp_col % 26)) + col_str
|
||||
temp_col = temp_col // 26 - 1
|
||||
|
||||
cell_addr = f"{col_str}{row + 1}"
|
||||
cells.append(cell_addr)
|
||||
|
||||
return sheet, cells
|
||||
|
||||
|
||||
def column_to_index(col_str):
|
||||
"""Convert column string (A, B, ..., Z, AA, AB, ...) to index."""
|
||||
result = 0
|
||||
for char in col_str:
|
||||
result = result * 26 + (ord(char) - ord("A") + 1)
|
||||
return result - 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Style Commands (Bold, Italic, Underline)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleBold:
|
||||
"""Toggle bold style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_bold"),
|
||||
"MenuText": "Bold",
|
||||
"ToolTip": "Toggle bold style on selected cells (Ctrl+B)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Bold")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "bold" in styles:
|
||||
styles.discard("bold")
|
||||
else:
|
||||
styles.add("bold")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle bold: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleItalic:
|
||||
"""Toggle italic style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_italic"),
|
||||
"MenuText": "Italic",
|
||||
"ToolTip": "Toggle italic style on selected cells (Ctrl+I)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Italic")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "italic" in styles:
|
||||
styles.discard("italic")
|
||||
else:
|
||||
styles.add("italic")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle italic: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleUnderline:
|
||||
"""Toggle underline style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_underline"),
|
||||
"MenuText": "Underline",
|
||||
"ToolTip": "Toggle underline style on selected cells (Ctrl+U)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Underline")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "underline" in styles:
|
||||
styles.discard("underline")
|
||||
else:
|
||||
styles.add("underline")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle underline: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Alignment Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignLeft:
|
||||
"""Align cell content to the left."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_left"),
|
||||
"MenuText": "Align Left",
|
||||
"ToolTip": "Align selected cells to the left",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Left")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "left")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align left: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignCenter:
|
||||
"""Align cell content to the center."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_center"),
|
||||
"MenuText": "Align Center",
|
||||
"ToolTip": "Align selected cells to the center",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Center")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "center")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align center: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignRight:
|
||||
"""Align cell content to the right."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_right"),
|
||||
"MenuText": "Align Right",
|
||||
"ToolTip": "Align selected cells to the right",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Right")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "right")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align right: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Color Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetBgColor:
|
||||
"""Set background color of selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_bg_color"),
|
||||
"MenuText": "Background Color",
|
||||
"ToolTip": "Set background color of selected cells",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
# Show color picker dialog
|
||||
color = QtGui.QColorDialog.getColor(
|
||||
QtCore.Qt.white, Gui.getMainWindow(), "Select Background Color"
|
||||
)
|
||||
|
||||
if not color.isValid():
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Set Background Color")
|
||||
|
||||
try:
|
||||
# FreeCAD expects RGB as tuple of floats 0-1
|
||||
rgb = (
|
||||
color.redF(),
|
||||
color.greenF(),
|
||||
color.blueF(),
|
||||
)
|
||||
|
||||
for cell in cells:
|
||||
sheet.setBackground(cell, rgb)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to set background color: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetTextColor:
|
||||
"""Set text color of selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_text_color"),
|
||||
"MenuText": "Text Color",
|
||||
"ToolTip": "Set text color of selected cells",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
# Show color picker dialog with default white for dark theme
|
||||
initial_color = QtGui.QColor(205, 214, 244) # Catppuccin Mocha text color
|
||||
color = QtGui.QColorDialog.getColor(
|
||||
initial_color, Gui.getMainWindow(), "Select Text Color"
|
||||
)
|
||||
|
||||
if not color.isValid():
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Set Text Color")
|
||||
|
||||
try:
|
||||
# FreeCAD expects RGB as tuple of floats 0-1
|
||||
rgb = (
|
||||
color.redF(),
|
||||
color.greenF(),
|
||||
color.blueF(),
|
||||
)
|
||||
|
||||
for cell in cells:
|
||||
sheet.setForeground(cell, rgb)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to set text color: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Utility Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetQuickAlias:
|
||||
"""Create alias from cell content."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_quick_alias"),
|
||||
"MenuText": "Quick Alias",
|
||||
"ToolTip": "Create alias for selected cell based on adjacent label cell",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Quick Alias")
|
||||
|
||||
try:
|
||||
aliases_created = 0
|
||||
|
||||
for cell in cells:
|
||||
# Parse cell address
|
||||
import re
|
||||
|
||||
match = re.match(r"([A-Z]+)(\d+)", cell)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
col_str = match.group(1)
|
||||
row = int(match.group(2))
|
||||
|
||||
# Get content of cell to the left (label cell)
|
||||
col_idx = column_to_index(col_str)
|
||||
if col_idx > 0:
|
||||
# Convert back to column string
|
||||
label_col_idx = col_idx - 1
|
||||
label_col_str = ""
|
||||
temp = label_col_idx
|
||||
while temp >= 0:
|
||||
label_col_str = chr(65 + (temp % 26)) + label_col_str
|
||||
temp = temp // 26 - 1
|
||||
|
||||
label_cell = f"{label_col_str}{row}"
|
||||
label_content = sheet.getContents(label_cell)
|
||||
|
||||
if label_content:
|
||||
# Clean the label to make a valid alias
|
||||
# Must be alphanumeric + underscore, start with letter
|
||||
alias = "".join(
|
||||
c if c.isalnum() or c == "_" else "_" for c in label_content
|
||||
)
|
||||
# Ensure it starts with a letter
|
||||
if alias and not alias[0].isalpha():
|
||||
alias = "var_" + alias
|
||||
# Truncate if too long
|
||||
alias = alias[:30]
|
||||
|
||||
if alias:
|
||||
try:
|
||||
sheet.setAlias(cell, alias)
|
||||
aliases_created += 1
|
||||
except Exception as alias_err:
|
||||
App.Console.PrintWarning(
|
||||
f"Could not set alias '{alias}' for {cell}: {alias_err}\n"
|
||||
)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
|
||||
if aliases_created > 0:
|
||||
App.Console.PrintMessage(f"Created {aliases_created} alias(es)\n")
|
||||
else:
|
||||
App.Console.PrintWarning(
|
||||
"No aliases created. Select value cells with labels to the left.\n"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to create aliases: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Register Commands
|
||||
# =============================================================================
|
||||
|
||||
Gui.addCommand("ZTools_SpreadsheetStyleBold", ZTools_SpreadsheetStyleBold())
|
||||
Gui.addCommand("ZTools_SpreadsheetStyleItalic", ZTools_SpreadsheetStyleItalic())
|
||||
Gui.addCommand("ZTools_SpreadsheetStyleUnderline", ZTools_SpreadsheetStyleUnderline())
|
||||
Gui.addCommand("ZTools_SpreadsheetAlignLeft", ZTools_SpreadsheetAlignLeft())
|
||||
Gui.addCommand("ZTools_SpreadsheetAlignCenter", ZTools_SpreadsheetAlignCenter())
|
||||
Gui.addCommand("ZTools_SpreadsheetAlignRight", ZTools_SpreadsheetAlignRight())
|
||||
Gui.addCommand("ZTools_SpreadsheetBgColor", ZTools_SpreadsheetBgColor())
|
||||
Gui.addCommand("ZTools_SpreadsheetTextColor", ZTools_SpreadsheetTextColor())
|
||||
Gui.addCommand("ZTools_SpreadsheetQuickAlias", ZTools_SpreadsheetQuickAlias())
|
||||
41
reference/ztools/ztools/ztools/datums/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# ztools/datums - Datum creation tools
|
||||
from .core import (
|
||||
axis_cylinder_center,
|
||||
# Axes
|
||||
axis_from_2_points,
|
||||
axis_from_edge,
|
||||
axis_intersection_planes,
|
||||
plane_angled,
|
||||
plane_from_3_points,
|
||||
plane_midplane,
|
||||
plane_normal_to_edge,
|
||||
# Planes
|
||||
plane_offset_from_face,
|
||||
plane_offset_from_plane,
|
||||
plane_tangent_to_cylinder,
|
||||
point_at_coordinates,
|
||||
# Points
|
||||
point_at_vertex,
|
||||
point_center_of_circle,
|
||||
point_center_of_face,
|
||||
point_on_edge,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"plane_offset_from_face",
|
||||
"plane_offset_from_plane",
|
||||
"plane_midplane",
|
||||
"plane_from_3_points",
|
||||
"plane_normal_to_edge",
|
||||
"plane_angled",
|
||||
"plane_tangent_to_cylinder",
|
||||
"axis_from_2_points",
|
||||
"axis_from_edge",
|
||||
"axis_cylinder_center",
|
||||
"axis_intersection_planes",
|
||||
"point_at_vertex",
|
||||
"point_at_coordinates",
|
||||
"point_on_edge",
|
||||
"point_center_of_face",
|
||||
"point_center_of_circle",
|
||||
]
|
||||
1588
reference/ztools/ztools/ztools/datums/core.py
Normal file
8
reference/ztools/ztools/ztools/resources/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# ztools/resources - Icons and assets
|
||||
from .icons import MOCHA, get_icon, save_icons_to_disk
|
||||
|
||||
__all__ = [
|
||||
"get_icon",
|
||||
"save_icons_to_disk",
|
||||
"MOCHA",
|
||||
]
|
||||
513
reference/ztools/ztools/ztools/resources/icons.py
Normal file
@@ -0,0 +1,513 @@
|
||||
# ztools/resources/icons.py
|
||||
# Catppuccin Mocha themed icons for ztools
|
||||
|
||||
# Catppuccin Mocha Palette — sourced from kindred-addon-sdk
|
||||
from kindred_sdk.theme import get_theme_tokens
|
||||
|
||||
MOCHA = get_theme_tokens()
|
||||
|
||||
|
||||
def _svg_to_base64(svg_content: str) -> str:
|
||||
"""Convert SVG string to base64 data URI for FreeCAD."""
|
||||
import base64
|
||||
|
||||
encoded = base64.b64encode(svg_content.encode("utf-8")).decode("utf-8")
|
||||
return f"data:image/svg+xml;base64,{encoded}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SVG Icon Definitions
|
||||
# =============================================================================
|
||||
|
||||
# Workbench main icon - stylized "Z"
|
||||
ICON_WORKBENCH_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"]}"/>
|
||||
<path d="M8 10 L24 10 L10 22 L24 22" stroke="{MOCHA["mauve"]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<circle cx="24" cy="10" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Datum Creator icon - plane with plus
|
||||
ICON_DATUM_CREATOR_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"]}"/>
|
||||
<!-- Plane representation -->
|
||||
<path d="M6 20 L16 8 L26 20 L16 26 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Plus sign -->
|
||||
<circle cx="24" cy="8" r="6" fill="{MOCHA["green"]}"/>
|
||||
<path d="M24 5 L24 11 M21 8 L27 8" stroke="{MOCHA["base"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Datum Manager icon - stacked planes with list
|
||||
ICON_DATUM_MANAGER_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"]}"/>
|
||||
<!-- Stacked planes -->
|
||||
<path d="M4 18 L12 12 L20 18 L12 22 Z" fill="{MOCHA["blue"]}" fill-opacity="0.5" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<path d="M4 14 L12 8 L20 14 L12 18 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.5" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
<!-- List lines -->
|
||||
<line x1="22" y1="10" x2="28" y2="10" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="16" x2="28" y2="16" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="22" x2="28" y2="22" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Offset icon
|
||||
ICON_PLANE_OFFSET_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"]}"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 14 L28 22 L16 28 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Offset plane -->
|
||||
<path d="M4 14 L16 6 L28 14 L16 20 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Offset arrow -->
|
||||
<path d="M16 24 L16 18" stroke="{MOCHA["peach"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 20 L16 17 L18 20" stroke="{MOCHA["peach"]}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Midplane icon
|
||||
ICON_PLANE_MIDPLANE_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"]}"/>
|
||||
<!-- Top plane -->
|
||||
<path d="M4 10 L16 4 L28 10 L16 14 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Middle plane (result) -->
|
||||
<path d="M4 16 L16 10 L28 16 L16 20 Z" fill="{MOCHA["green"]}" fill-opacity="0.7" stroke="{MOCHA["teal"]}" stroke-width="1.5"/>
|
||||
<!-- Bottom plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane 3 Points icon
|
||||
ICON_PLANE_3PT_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"]}"/>
|
||||
<!-- Plane -->
|
||||
<path d="M4 20 L16 8 L28 18 L14 28 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Three points -->
|
||||
<circle cx="8" cy="20" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="16" cy="10" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="24" cy="18" r="3" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Normal to Edge icon
|
||||
ICON_PLANE_NORMAL_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"]}"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 26 Q16 6 26 16" stroke="{MOCHA["yellow"]}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Plane perpendicular -->
|
||||
<path d="M12 8 L20 12 L20 24 L12 20 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Point on curve -->
|
||||
<circle cx="16" cy="16" r="2.5" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Angled icon
|
||||
ICON_PLANE_ANGLED_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"]}"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Angled plane -->
|
||||
<path d="M8 10 L20 6 L24 18 L12 22 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.7" stroke="{MOCHA["lavender"]}" stroke-width="1.5"/>
|
||||
<!-- Angle arc -->
|
||||
<path d="M14 20 Q18 18 18 14" stroke="{MOCHA["peach"]}" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Tangent icon
|
||||
ICON_PLANE_TANGENT_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"]}"/>
|
||||
<!-- Cylinder outline -->
|
||||
<ellipse cx="12" cy="16" rx="6" ry="10" fill="none" stroke="{MOCHA["yellow"]}" stroke-width="2"/>
|
||||
<!-- Tangent plane -->
|
||||
<path d="M18 6 L28 10 L28 26 L18 22 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Tangent point -->
|
||||
<circle cx="18" cy="16" r="2.5" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis 2 Points icon
|
||||
ICON_AXIS_2PT_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"]}"/>
|
||||
<!-- Axis line -->
|
||||
<line x1="6" y1="26" x2="26" y2="6" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<!-- End points -->
|
||||
<circle cx="6" cy="26" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="26" cy="6" r="3" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis from Edge icon
|
||||
ICON_AXIS_EDGE_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"]}"/>
|
||||
<!-- Box edge representation -->
|
||||
<path d="M8 24 L8 12 L20 8 L20 20 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Selected edge highlighted -->
|
||||
<line x1="8" y1="24" x2="8" y2="12" stroke="{MOCHA["yellow"]}" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Resulting axis -->
|
||||
<line x1="8" y1="28" x2="8" y2="4" stroke="{MOCHA["red"]}" stroke-width="2" stroke-dasharray="4,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis Cylinder Center icon
|
||||
ICON_AXIS_CYL_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"]}"/>
|
||||
<!-- Cylinder -->
|
||||
<ellipse cx="16" cy="8" rx="8" ry="3" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M8 8 L8 24" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M24 8 L24 24" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<ellipse cx="16" cy="24" rx="8" ry="3" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Center axis -->
|
||||
<line x1="16" y1="4" x2="16" y2="28" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="16" r="2" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis Intersection icon
|
||||
ICON_AXIS_INTERSECT_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"]}"/>
|
||||
<!-- First plane -->
|
||||
<path d="M4 12 L16 6 L28 12 L16 18 Z" fill="{MOCHA["blue"]}" fill-opacity="0.5" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<!-- Second plane -->
|
||||
<path d="M4 20 L16 14 L28 20 L16 26 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.5" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
<!-- Intersection axis -->
|
||||
<line x1="4" y1="16" x2="28" y2="16" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Point at Vertex icon
|
||||
ICON_POINT_VERTEX_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"]}"/>
|
||||
<!-- Wireframe box corner -->
|
||||
<path d="M10 20 L10 10 L20 6" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L20 16 L20 6" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L4 24" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<!-- Vertex point -->
|
||||
<circle cx="10" cy="20" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="10" cy="20" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Point XYZ icon
|
||||
ICON_POINT_XYZ_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"]}"/>
|
||||
<!-- Coordinate axes -->
|
||||
<line x1="6" y1="24" x2="26" y2="24" stroke="{MOCHA["red"]}" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="6" y2="6" stroke="{MOCHA["green"]}" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="16" y2="28" stroke="{MOCHA["blue"]}" stroke-width="1.5"/>
|
||||
<!-- Point -->
|
||||
<circle cx="18" cy="12" r="4" fill="{MOCHA["peach"]}"/>
|
||||
<!-- Projection lines -->
|
||||
<line x1="18" y1="12" x2="18" y2="24" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
<line x1="18" y1="12" x2="6" y2="12" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Point on Edge icon
|
||||
ICON_POINT_EDGE_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"]}"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 24 Q16 4 26 20" stroke="{MOCHA["yellow"]}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Point on edge -->
|
||||
<circle cx="14" cy="12" r="4" fill="{MOCHA["green"]}"/>
|
||||
<!-- Parameter indicator -->
|
||||
<text x="20" y="10" font-family="monospace" font-size="8" fill="{MOCHA["text"]}">t</text>
|
||||
</svg>'''
|
||||
|
||||
# Point Face Center icon
|
||||
ICON_POINT_FACE_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"]}"/>
|
||||
<!-- Face -->
|
||||
<path d="M6 20 L16 10 L26 20 L16 26 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="19" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="16" cy="19" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Point Circle Center icon
|
||||
ICON_POINT_CIRCLE_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"]}"/>
|
||||
<!-- Circle -->
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="{MOCHA["yellow"]}" stroke-width="2.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="16" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="16" cy="16" r="2" fill="{MOCHA["teal"]}"/>
|
||||
<!-- Radius line -->
|
||||
<line x1="16" y1="16" x2="26" y2="16" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Rotated Linear Pattern icon - objects along line with rotation
|
||||
ICON_ROTATED_PATTERN_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"]}"/>
|
||||
<!-- Direction line -->
|
||||
<line x1="4" y1="24" x2="28" y2="24" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- First cube (original) -->
|
||||
<rect x="4" y="16" width="6" height="6" fill="{MOCHA["blue"]}" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<!-- Second cube (rotated 15deg) -->
|
||||
<g transform="translate(14,19) rotate(-15)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="{MOCHA["mauve"]}" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Third cube (rotated 30deg) -->
|
||||
<g transform="translate(24,19) rotate(-30)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="{MOCHA["pink"]}" stroke="{MOCHA["flamingo"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M24 8 A5 5 0 0 1 19 13" stroke="{MOCHA["peach"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M18 11 L19 13 L21 12" stroke="{MOCHA["peach"]}" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Enhanced Pocket icon - pocket with plus/settings indicator
|
||||
ICON_POCKET_ENHANCED_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"]}"/>
|
||||
<!-- 3D block with pocket cutout -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="{MOCHA["overlay1"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Pocket depression -->
|
||||
<path d="M10 12 L16 10 L22 12 L22 18 L16 20 L10 18 Z" fill="{MOCHA["base"]}" stroke="{MOCHA["mauve"]}" stroke-width="1.5"/>
|
||||
<!-- Down arrow indicating cut -->
|
||||
<path d="M16 13 L16 17" stroke="{MOCHA["red"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 15 L16 18 L18 15" stroke="{MOCHA["red"]}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>'''
|
||||
|
||||
# Flipped Pocket icon - pocket cutting outside the profile
|
||||
ICON_POCKET_FLIPPED_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"]}"/>
|
||||
<!-- Outer material removed (dark) -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="{MOCHA["base"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="{MOCHA["overlay0"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="{MOCHA["overlay0"]}" stroke-width="1"/>
|
||||
<!-- Inner remaining material (raised) -->
|
||||
<path d="M10 20 L10 12 L16 10 L22 12 L22 20 L16 22 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["teal"]}" stroke-width="1.5"/>
|
||||
<path d="M10 12 L16 14 L22 12" stroke="{MOCHA["overlay1"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 22" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Flip arrows -->
|
||||
<path d="M4 16 L8 14 L8 18 Z" fill="{MOCHA["peach"]}"/>
|
||||
<path d="M28 16 L24 14 L24 18 Z" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Assembly Linear Pattern icon - components along a line
|
||||
ICON_ASSEMBLY_LINEAR_PATTERN_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"]}"/>
|
||||
<!-- Direction arrow -->
|
||||
<line x1="4" y1="16" x2="26" y2="16" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" stroke-dasharray="3,2"/>
|
||||
<path d="M24 13 L28 16 L24 19" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (original) -->
|
||||
<rect x="4" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
|
||||
<circle cx="7" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
|
||||
<!-- Component 2 -->
|
||||
<rect x="13" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
|
||||
<circle cx="16" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
|
||||
<!-- Component 3 -->
|
||||
<rect x="22" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
|
||||
<circle cx="25" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
|
||||
<!-- Count indicator -->
|
||||
<text x="16" y="26" font-family="monospace" font-size="7" fill="{MOCHA["text"]}" text-anchor="middle">1-2-3</text>
|
||||
</svg>'''
|
||||
|
||||
# Assembly Polar Pattern icon - components around a center
|
||||
ICON_ASSEMBLY_POLAR_PATTERN_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"]}"/>
|
||||
<!-- Center axis indicator -->
|
||||
<circle cx="16" cy="16" r="2" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M23 8 A9 9 0 0 1 26 14" stroke="{MOCHA["green"]}" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M25 11 L26 14 L23 14" stroke="{MOCHA["green"]}" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (top) -->
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
|
||||
<!-- Component 2 (right) -->
|
||||
<g transform="translate(16,16) rotate(72) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["mauve"]}" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 3 (bottom-right) -->
|
||||
<g transform="translate(16,16) rotate(144) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["pink"]}" stroke="{MOCHA["flamingo"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 4 (bottom-left) -->
|
||||
<g transform="translate(16,16) rotate(216) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["peach"]}" stroke="{MOCHA["maroon"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 5 (left) -->
|
||||
<g transform="translate(16,16) rotate(288) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["yellow"]}" stroke="{MOCHA["peach"]}" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>'''
|
||||
|
||||
# =============================================================================
|
||||
# Spreadsheet Icons
|
||||
# =============================================================================
|
||||
|
||||
# Bold text icon
|
||||
ICON_SPREADSHEET_BOLD_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"]}"/>
|
||||
<text x="16" y="23" font-family="sans-serif" font-size="18" font-weight="bold" fill="{MOCHA["text"]}" text-anchor="middle">B</text>
|
||||
</svg>'''
|
||||
|
||||
# Italic text icon
|
||||
ICON_SPREADSHEET_ITALIC_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"]}"/>
|
||||
<text x="16" y="23" font-family="serif" font-size="18" font-style="italic" fill="{MOCHA["text"]}" text-anchor="middle">I</text>
|
||||
</svg>'''
|
||||
|
||||
# Underline text icon
|
||||
ICON_SPREADSHEET_UNDERLINE_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"]}"/>
|
||||
<text x="16" y="20" font-family="sans-serif" font-size="16" fill="{MOCHA["text"]}" text-anchor="middle">U</text>
|
||||
<line x1="10" y1="24" x2="22" y2="24" stroke="{MOCHA["text"]}" stroke-width="2"/>
|
||||
</svg>'''
|
||||
|
||||
# Align left icon
|
||||
ICON_SPREADSHEET_ALIGN_LEFT_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"]}"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="14" x2="20" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="19" x2="24" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="24" x2="16" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Align center icon
|
||||
ICON_SPREADSHEET_ALIGN_CENTER_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"]}"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="10" y1="14" x2="22" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="8" y1="19" x2="24" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="24" x2="20" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Align right icon
|
||||
ICON_SPREADSHEET_ALIGN_RIGHT_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"]}"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="14" x2="26" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="8" y1="19" x2="26" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="16" y1="24" x2="26" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Background color icon (paint bucket)
|
||||
ICON_SPREADSHEET_BG_COLOR_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"]}"/>
|
||||
<!-- Paint bucket -->
|
||||
<path d="M10 12 L16 6 L22 12 L22 20 C22 22 20 24 16 24 C12 24 10 22 10 20 Z" fill="{MOCHA["yellow"]}" stroke="{MOCHA["peach"]}" stroke-width="1.5"/>
|
||||
<!-- Handle -->
|
||||
<path d="M16 6 L16 3" stroke="{MOCHA["overlay1"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Paint drip -->
|
||||
<ellipse cx="25" cy="22" rx="2" ry="3" fill="{MOCHA["yellow"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Text color icon (A with color bar)
|
||||
ICON_SPREADSHEET_TEXT_COLOR_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"]}"/>
|
||||
<!-- Letter A -->
|
||||
<text x="16" y="20" font-family="sans-serif" font-size="16" font-weight="bold" fill="{MOCHA["text"]}" text-anchor="middle">A</text>
|
||||
<!-- Color bar -->
|
||||
<rect x="8" y="24" width="16" height="3" rx="1" fill="{MOCHA["red"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Quick alias icon (tag/label)
|
||||
ICON_SPREADSHEET_QUICK_ALIAS_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="M6 10 L18 10 L24 16 L18 22 L6 22 Z" fill="{MOCHA["teal"]}" stroke="{MOCHA["green"]}" stroke-width="1.5"/>
|
||||
<!-- Tag hole -->
|
||||
<circle cx="10" cy="16" r="2" fill="{MOCHA["surface0"]}"/>
|
||||
<!-- Equals sign (alias) -->
|
||||
<line x1="20" y1="12" x2="26" y2="12" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="17" x2="26" y2="17" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Icon Registry - Base64 encoded for FreeCAD
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_icon(name: str) -> str:
|
||||
"""Get icon file path by name.
|
||||
|
||||
Returns the path to an SVG icon file. If the file doesn't exist,
|
||||
it will be created from the embedded SVG definitions.
|
||||
"""
|
||||
import os
|
||||
|
||||
# Map of short names to SVG content
|
||||
icons = {
|
||||
"workbench": ICON_WORKBENCH_SVG,
|
||||
"datum_creator": ICON_DATUM_CREATOR_SVG,
|
||||
"datum_manager": ICON_DATUM_MANAGER_SVG,
|
||||
"plane_offset": ICON_PLANE_OFFSET_SVG,
|
||||
"plane_midplane": ICON_PLANE_MIDPLANE_SVG,
|
||||
"plane_3pt": ICON_PLANE_3PT_SVG,
|
||||
"plane_normal": ICON_PLANE_NORMAL_SVG,
|
||||
"plane_angled": ICON_PLANE_ANGLED_SVG,
|
||||
"plane_tangent": ICON_PLANE_TANGENT_SVG,
|
||||
"axis_2pt": ICON_AXIS_2PT_SVG,
|
||||
"axis_edge": ICON_AXIS_EDGE_SVG,
|
||||
"axis_cyl": ICON_AXIS_CYL_SVG,
|
||||
"axis_intersect": ICON_AXIS_INTERSECT_SVG,
|
||||
"point_vertex": ICON_POINT_VERTEX_SVG,
|
||||
"point_xyz": ICON_POINT_XYZ_SVG,
|
||||
"point_edge": ICON_POINT_EDGE_SVG,
|
||||
"point_face": ICON_POINT_FACE_SVG,
|
||||
"point_circle": ICON_POINT_CIRCLE_SVG,
|
||||
"rotated_pattern": ICON_ROTATED_PATTERN_SVG,
|
||||
"pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
|
||||
"pocket_flipped": ICON_POCKET_FLIPPED_SVG,
|
||||
"assembly_linear_pattern": ICON_ASSEMBLY_LINEAR_PATTERN_SVG,
|
||||
"assembly_polar_pattern": ICON_ASSEMBLY_POLAR_PATTERN_SVG,
|
||||
"spreadsheet_bold": ICON_SPREADSHEET_BOLD_SVG,
|
||||
"spreadsheet_italic": ICON_SPREADSHEET_ITALIC_SVG,
|
||||
"spreadsheet_underline": ICON_SPREADSHEET_UNDERLINE_SVG,
|
||||
"spreadsheet_align_left": ICON_SPREADSHEET_ALIGN_LEFT_SVG,
|
||||
"spreadsheet_align_center": ICON_SPREADSHEET_ALIGN_CENTER_SVG,
|
||||
"spreadsheet_align_right": ICON_SPREADSHEET_ALIGN_RIGHT_SVG,
|
||||
"spreadsheet_bg_color": ICON_SPREADSHEET_BG_COLOR_SVG,
|
||||
"spreadsheet_text_color": ICON_SPREADSHEET_TEXT_COLOR_SVG,
|
||||
"spreadsheet_quick_alias": ICON_SPREADSHEET_QUICK_ALIAS_SVG,
|
||||
}
|
||||
|
||||
if name not in icons:
|
||||
return ""
|
||||
|
||||
# Get the icons directory path (relative to this file)
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||
icon_path = os.path.join(icons_dir, f"ztools_{name}.svg")
|
||||
|
||||
# If the icon file doesn't exist, create it
|
||||
if not os.path.exists(icon_path):
|
||||
os.makedirs(icons_dir, exist_ok=True)
|
||||
with open(icon_path, "w") as f:
|
||||
f.write(icons[name])
|
||||
|
||||
return icon_path
|
||||
|
||||
|
||||
def save_icons_to_disk(directory: str):
|
||||
"""Save all icons as SVG files to a directory."""
|
||||
import os
|
||||
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
icons = {
|
||||
"ztools_workbench": ICON_WORKBENCH_SVG,
|
||||
"ztools_datum_creator": ICON_DATUM_CREATOR_SVG,
|
||||
"ztools_datum_manager": ICON_DATUM_MANAGER_SVG,
|
||||
"ztools_plane_offset": ICON_PLANE_OFFSET_SVG,
|
||||
"ztools_plane_midplane": ICON_PLANE_MIDPLANE_SVG,
|
||||
"ztools_plane_3pt": ICON_PLANE_3PT_SVG,
|
||||
"ztools_plane_normal": ICON_PLANE_NORMAL_SVG,
|
||||
"ztools_plane_angled": ICON_PLANE_ANGLED_SVG,
|
||||
"ztools_plane_tangent": ICON_PLANE_TANGENT_SVG,
|
||||
"ztools_axis_2pt": ICON_AXIS_2PT_SVG,
|
||||
"ztools_axis_edge": ICON_AXIS_EDGE_SVG,
|
||||
"ztools_axis_cyl": ICON_AXIS_CYL_SVG,
|
||||
"ztools_axis_intersect": ICON_AXIS_INTERSECT_SVG,
|
||||
"ztools_point_vertex": ICON_POINT_VERTEX_SVG,
|
||||
"ztools_point_xyz": ICON_POINT_XYZ_SVG,
|
||||
"ztools_point_edge": ICON_POINT_EDGE_SVG,
|
||||
"ztools_point_face": ICON_POINT_FACE_SVG,
|
||||
"ztools_point_circle": ICON_POINT_CIRCLE_SVG,
|
||||
"ztools_rotated_pattern": ICON_ROTATED_PATTERN_SVG,
|
||||
"ztools_pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
|
||||
"ztools_pocket_flipped": ICON_POCKET_FLIPPED_SVG,
|
||||
"ztools_assembly_linear_pattern": ICON_ASSEMBLY_LINEAR_PATTERN_SVG,
|
||||
"ztools_assembly_polar_pattern": ICON_ASSEMBLY_POLAR_PATTERN_SVG,
|
||||
"ztools_spreadsheet_bold": ICON_SPREADSHEET_BOLD_SVG,
|
||||
"ztools_spreadsheet_italic": ICON_SPREADSHEET_ITALIC_SVG,
|
||||
"ztools_spreadsheet_underline": ICON_SPREADSHEET_UNDERLINE_SVG,
|
||||
"ztools_spreadsheet_align_left": ICON_SPREADSHEET_ALIGN_LEFT_SVG,
|
||||
"ztools_spreadsheet_align_center": ICON_SPREADSHEET_ALIGN_CENTER_SVG,
|
||||
"ztools_spreadsheet_align_right": ICON_SPREADSHEET_ALIGN_RIGHT_SVG,
|
||||
"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,
|
||||
}
|
||||
|
||||
for name, svg in icons.items():
|
||||
filepath = os.path.join(directory, f"{name}.svg")
|
||||
with open(filepath, "w") as f:
|
||||
f.write(svg)
|
||||
print(f"Saved: {filepath}")
|
||||
@@ -0,0 +1,17 @@
|
||||
<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="#313244"/>
|
||||
<!-- Direction arrow -->
|
||||
<line x1="4" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1.5" stroke-dasharray="3,2"/>
|
||||
<path d="M24 13 L28 16 L24 19" stroke="#7f849c" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (original) -->
|
||||
<rect x="4" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="7" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Component 2 -->
|
||||
<rect x="13" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="16" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Component 3 -->
|
||||
<rect x="22" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="25" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Count indicator -->
|
||||
<text x="16" y="26" font-family="monospace" font-size="7" fill="#cdd6f4" text-anchor="middle">1-2-3</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,27 @@
|
||||
<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="#313244"/>
|
||||
<!-- Center axis indicator -->
|
||||
<circle cx="16" cy="16" r="2" fill="#fab387"/>
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="#7f849c" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M23 8 A9 9 0 0 1 26 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M25 11 L26 14 L23 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (top) -->
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<!-- Component 2 (right) -->
|
||||
<g transform="translate(16,16) rotate(72) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#cba6f7" stroke="#b4befe" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 3 (bottom-right) -->
|
||||
<g transform="translate(16,16) rotate(144) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f5c2e7" stroke="#f2cdcd" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 4 (bottom-left) -->
|
||||
<g transform="translate(16,16) rotate(216) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#fab387" stroke="#eba0ac" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 5 (left) -->
|
||||
<g transform="translate(16,16) rotate(288) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f9e2af" stroke="#fab387" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,8 @@
|
||||
<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="#313244"/>
|
||||
<!-- Axis line -->
|
||||
<line x1="6" y1="26" x2="26" y2="6" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<!-- End points -->
|
||||
<circle cx="6" cy="26" r="3" fill="#fab387"/>
|
||||
<circle cx="26" cy="6" r="3" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1,11 @@
|
||||
<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="#313244"/>
|
||||
<!-- Cylinder -->
|
||||
<ellipse cx="16" cy="8" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M8 8 L8 24" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M24 8 L24 24" stroke="#7f849c" stroke-width="1"/>
|
||||
<ellipse cx="16" cy="24" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Center axis -->
|
||||
<line x1="16" y1="4" x2="16" y2="28" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="16" r="2" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Box edge representation -->
|
||||
<path d="M8 24 L8 12 L20 8 L20 20 Z" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Selected edge highlighted -->
|
||||
<line x1="8" y1="24" x2="8" y2="12" stroke="#f9e2af" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Resulting axis -->
|
||||
<line x1="8" y1="28" x2="8" y2="4" stroke="#f38ba8" stroke-width="2" stroke-dasharray="4,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- First plane -->
|
||||
<path d="M4 12 L16 6 L28 12 L16 18 Z" fill="#89b4fa" fill-opacity="0.5" stroke="#74c7ec" stroke-width="1"/>
|
||||
<!-- Second plane -->
|
||||
<path d="M4 20 L16 14 L28 20 L16 26 Z" fill="#cba6f7" fill-opacity="0.5" stroke="#b4befe" stroke-width="1"/>
|
||||
<!-- Intersection axis -->
|
||||
<line x1="4" y1="16" x2="28" y2="16" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 531 B |
@@ -0,0 +1,8 @@
|
||||
<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="#313244"/>
|
||||
<!-- Plane representation -->
|
||||
<path d="M6 20 L16 8 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Plus sign -->
|
||||
<circle cx="24" cy="8" r="6" fill="#a6e3a1"/>
|
||||
<path d="M24 5 L24 11 M21 8 L27 8" stroke="#1e1e2e" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 443 B |
@@ -0,0 +1,10 @@
|
||||
<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="#313244"/>
|
||||
<!-- Stacked planes -->
|
||||
<path d="M4 18 L12 12 L20 18 L12 22 Z" fill="#89b4fa" fill-opacity="0.5" stroke="#74c7ec" stroke-width="1"/>
|
||||
<path d="M4 14 L12 8 L20 14 L12 18 Z" fill="#cba6f7" fill-opacity="0.5" stroke="#b4befe" stroke-width="1"/>
|
||||
<!-- List lines -->
|
||||
<line x1="22" y1="10" x2="28" y2="10" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="16" x2="28" y2="16" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="22" x2="28" y2="22" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Plane -->
|
||||
<path d="M4 20 L16 8 L28 18 L14 28 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Three points -->
|
||||
<circle cx="8" cy="20" r="3" fill="#fab387"/>
|
||||
<circle cx="16" cy="10" r="3" fill="#fab387"/>
|
||||
<circle cx="24" cy="18" r="3" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Angled plane -->
|
||||
<path d="M8 10 L20 6 L24 18 L12 22 Z" fill="#cba6f7" fill-opacity="0.7" stroke="#b4befe" stroke-width="1.5"/>
|
||||
<!-- Angle arc -->
|
||||
<path d="M14 20 Q18 18 18 14" stroke="#fab387" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Top plane -->
|
||||
<path d="M4 10 L16 4 L28 10 L16 14 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Middle plane (result) -->
|
||||
<path d="M4 16 L16 10 L28 16 L16 20 Z" fill="#a6e3a1" fill-opacity="0.7" stroke="#94e2d5" stroke-width="1.5"/>
|
||||
<!-- Bottom plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 26 Q16 6 26 16" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Plane perpendicular -->
|
||||
<path d="M12 8 L20 12 L20 24 L12 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Point on curve -->
|
||||
<circle cx="16" cy="16" r="2.5" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 480 B |
@@ -0,0 +1,10 @@
|
||||
<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="#313244"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 14 L28 22 L16 28 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Offset plane -->
|
||||
<path d="M4 14 L16 6 L28 14 L16 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Offset arrow -->
|
||||
<path d="M16 24 L16 18" stroke="#fab387" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 20 L16 17 L18 20" stroke="#fab387" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 621 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Cylinder outline -->
|
||||
<ellipse cx="12" cy="16" rx="6" ry="10" fill="none" stroke="#f9e2af" stroke-width="2"/>
|
||||
<!-- Tangent plane -->
|
||||
<path d="M18 6 L28 10 L28 26 L18 22 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Tangent point -->
|
||||
<circle cx="18" cy="16" r="2.5" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
@@ -0,0 +1,12 @@
|
||||
<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="#313244"/>
|
||||
<!-- 3D block with pocket cutout -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="#7f849c" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Pocket depression -->
|
||||
<path d="M10 12 L16 10 L22 12 L22 18 L16 20 L10 18 Z" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Down arrow indicating cut -->
|
||||
<path d="M16 13 L16 17" stroke="#f38ba8" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 15 L16 18 L18 15" stroke="#f38ba8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 807 B |
@@ -0,0 +1,14 @@
|
||||
<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="#313244"/>
|
||||
<!-- Outer material removed (dark) -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="#1e1e2e" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="#6c7086" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="#6c7086" stroke-width="1"/>
|
||||
<!-- Inner remaining material (raised) -->
|
||||
<path d="M10 20 L10 12 L16 10 L22 12 L22 20 L16 22 Z" fill="#585b70" stroke="#94e2d5" stroke-width="1.5"/>
|
||||
<path d="M10 12 L16 14 L22 12" stroke="#7f849c" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 22" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Flip arrows -->
|
||||
<path d="M4 16 L8 14 L8 18 Z" fill="#fab387"/>
|
||||
<path d="M28 16 L24 14 L24 18 Z" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 842 B |
@@ -0,0 +1,10 @@
|
||||
<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="#313244"/>
|
||||
<!-- Circle -->
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="#f9e2af" stroke-width="2.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="16" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="16" cy="16" r="2" fill="#94e2d5"/>
|
||||
<!-- Radius line -->
|
||||
<line x1="16" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 479 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 24 Q16 4 26 20" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Point on edge -->
|
||||
<circle cx="14" cy="12" r="4" fill="#a6e3a1"/>
|
||||
<!-- Parameter indicator -->
|
||||
<text x="20" y="10" font-family="monospace" font-size="8" fill="#cdd6f4">t</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
@@ -0,0 +1,8 @@
|
||||
<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="#313244"/>
|
||||
<!-- Face -->
|
||||
<path d="M6 20 L16 10 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="19" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="16" cy="19" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,10 @@
|
||||
<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="#313244"/>
|
||||
<!-- Wireframe box corner -->
|
||||
<path d="M10 20 L10 10 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L20 16 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L4 24" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<!-- Vertex point -->
|
||||
<circle cx="10" cy="20" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="10" cy="20" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
@@ -0,0 +1,12 @@
|
||||
<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="#313244"/>
|
||||
<!-- Coordinate axes -->
|
||||
<line x1="6" y1="24" x2="26" y2="24" stroke="#f38ba8" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="6" y2="6" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="16" y2="28" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<!-- Point -->
|
||||
<circle cx="18" cy="12" r="4" fill="#fab387"/>
|
||||
<!-- Projection lines -->
|
||||
<line x1="18" y1="12" x2="18" y2="24" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
<line x1="18" y1="12" x2="6" y2="12" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@@ -0,0 +1,18 @@
|
||||
<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="#313244"/>
|
||||
<!-- Direction line -->
|
||||
<line x1="4" y1="24" x2="28" y2="24" stroke="#7f849c" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- First cube (original) -->
|
||||
<rect x="4" y="16" width="6" height="6" fill="#89b4fa" stroke="#74c7ec" stroke-width="1"/>
|
||||
<!-- Second cube (rotated 15deg) -->
|
||||
<g transform="translate(14,19) rotate(-15)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="#cba6f7" stroke="#b4befe" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Third cube (rotated 30deg) -->
|
||||
<g transform="translate(24,19) rotate(-30)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="#f5c2e7" stroke="#f2cdcd" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M24 8 A5 5 0 0 1 19 13" stroke="#fab387" stroke-width="1.5" fill="none"/>
|
||||
<path d="M18 11 L19 13 L21 12" stroke="#fab387" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 980 B |
@@ -0,0 +1,7 @@
|
||||
<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="#313244"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="10" y1="14" x2="22" y2="14" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="8" y1="19" x2="24" y2="19" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="24" x2="20" y2="24" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 526 B |
@@ -0,0 +1,7 @@
|
||||
<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="#313244"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="14" x2="20" y2="14" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="19" x2="24" y2="19" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="6" y1="24" x2="16" y2="24" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 524 B |
@@ -0,0 +1,7 @@
|
||||
<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="#313244"/>
|
||||
<line x1="6" y1="9" x2="26" y2="9" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="14" x2="26" y2="14" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="8" y1="19" x2="26" y2="19" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="16" y1="24" x2="26" y2="24" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 526 B |
@@ -0,0 +1,9 @@
|
||||
<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="#313244"/>
|
||||
<!-- Paint bucket -->
|
||||
<path d="M10 12 L16 6 L22 12 L22 20 C22 22 20 24 16 24 C12 24 10 22 10 20 Z" fill="#f9e2af" stroke="#fab387" stroke-width="1.5"/>
|
||||
<!-- Handle -->
|
||||
<path d="M16 6 L16 3" stroke="#7f849c" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Paint drip -->
|
||||
<ellipse cx="25" cy="22" rx="2" ry="3" fill="#f9e2af"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 471 B |
@@ -0,0 +1,4 @@
|
||||
<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="#313244"/>
|
||||
<text x="16" y="23" font-family="sans-serif" font-size="18" font-weight="bold" fill="#cdd6f4" text-anchor="middle">B</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,4 @@
|
||||
<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="#313244"/>
|
||||
<text x="16" y="23" font-family="serif" font-size="18" font-style="italic" fill="#cdd6f4" text-anchor="middle">I</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,10 @@
|
||||
<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="#313244"/>
|
||||
<!-- Tag shape -->
|
||||
<path d="M6 10 L18 10 L24 16 L18 22 L6 22 Z" fill="#94e2d5" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||
<!-- Tag hole -->
|
||||
<circle cx="10" cy="16" r="2" fill="#313244"/>
|
||||
<!-- Equals sign (alias) -->
|
||||
<line x1="20" y1="12" x2="26" y2="12" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="17" x2="26" y2="17" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
@@ -0,0 +1,7 @@
|
||||
<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="#313244"/>
|
||||
<!-- Letter A -->
|
||||
<text x="16" y="20" font-family="sans-serif" font-size="16" font-weight="bold" fill="#cdd6f4" text-anchor="middle">A</text>
|
||||
<!-- Color bar -->
|
||||
<rect x="8" y="24" width="16" height="3" rx="1" fill="#f38ba8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -0,0 +1,5 @@
|
||||
<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="#313244"/>
|
||||
<text x="16" y="20" font-family="sans-serif" font-size="16" fill="#cdd6f4" text-anchor="middle">U</text>
|
||||
<line x1="10" y1="24" x2="22" y2="24" stroke="#cdd6f4" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,15 @@
|
||||
<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="#313244"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots on palette -->
|
||||
<circle cx="9" cy="16" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Check mark -->
|
||||
<circle cx="24" cy="8" r="5" fill="#a6e3a1"/>
|
||||
<path d="M21.5 8 L23 9.5 L26.5 6" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 767 B |
@@ -0,0 +1,15 @@
|
||||
<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="#313244"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="16" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots -->
|
||||
<circle cx="9" cy="14" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="11" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="14" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="19" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="18" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Download arrow -->
|
||||
<rect x="21" y="20" width="8" height="8" rx="2" fill="#fab387"/>
|
||||
<path d="M25 22 L25 26 M23 24.5 L25 26.5 L27 24.5" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
@@ -0,0 +1,15 @@
|
||||
<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="#313244"/>
|
||||
<!-- Paint palette (dimmed) -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#6c7086" stroke-width="1.5"/>
|
||||
<!-- Color dots (dimmed) -->
|
||||
<circle cx="9" cy="16" r="2" fill="#7f849c"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#7f849c"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#7f849c"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#7f849c"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- X mark -->
|
||||
<circle cx="24" cy="8" r="5" fill="#f38ba8"/>
|
||||
<path d="M22 6 L26 10 M26 6 L22 10" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
@@ -0,0 +1,17 @@
|
||||
<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="#313244"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots -->
|
||||
<circle cx="9" cy="16" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Toggle arrows -->
|
||||
<path d="M22 5 A4 4 0 0 1 26 9" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<path d="M25 5 L22 5 L22 8" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M28 11 A4 4 0 0 1 24 7" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<path d="M25 11 L28 11 L28 8" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<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="#313244"/>
|
||||
<path d="M8 10 L24 10 L10 22 L24 22" stroke="#cba6f7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<circle cx="24" cy="10" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |