From 981b15804e4571d0c6db1697d7f72e34cc606c46 Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Sat, 24 Jan 2026 15:16:09 -0600 Subject: [PATCH] first commit --- CatppuccinMocha/CatppuccinMocha.cfg | 109 ++ CatppuccinMocha/CatppuccinMocha.qss | 1249 ++++++++++++++++ Makefile | 89 ++ TODO_ATTACHMENT_WORK.md | 58 + package.xml | 32 + partdesign.md | 591 ++++++++ ztools/Init.py | 12 + ztools/InitGui.py | 233 +++ ztools/README.md | 123 ++ ztools/__pycache__/Init.cpython-313.pyc | Bin 0 -> 249 bytes ztools/__pycache__/InitGui.cpython-313.pyc | Bin 0 -> 6891 bytes ztools/setup.cfg | 11 + ztools/ztools/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 216 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 141 bytes ztools/ztools/commands/__init__.py | 4 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 319 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 244 bytes .../datum_commands.cpython-312.pyc | Bin 0 -> 30027 bytes .../datum_commands.cpython-313.pyc | Bin 0 -> 30691 bytes .../pattern_commands.cpython-312.pyc | Bin 0 -> 8299 bytes .../pocket_commands.cpython-312.pyc | Bin 0 -> 28305 bytes .../pocket_commands.cpython-313.pyc | Bin 0 -> 28916 bytes .../theme_commands.cpython-312.pyc | Bin 0 -> 4704 bytes .../theme_commands.cpython-313.pyc | Bin 0 -> 4759 bytes ztools/ztools/commands/datum_commands.py | 650 ++++++++ ztools/ztools/commands/pattern_commands.py | 206 +++ ztools/ztools/commands/pocket_commands.py | 601 ++++++++ ztools/ztools/datums/__init__.py | 39 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 707 bytes .../datums/__pycache__/core.cpython-312.pyc | Bin 0 -> 41705 bytes .../datums/__pycache__/core.cpython-313.pyc | Bin 0 -> 41683 bytes ztools/ztools/datums/core.py | 1138 ++++++++++++++ ztools/ztools/resources/__init__.py | 10 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 380 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 305 bytes .../__pycache__/icons.cpython-312.pyc | Bin 0 -> 17584 bytes .../__pycache__/icons.cpython-313.pyc | Bin 0 -> 17519 bytes .../__pycache__/theme.cpython-312.pyc | Bin 0 -> 38238 bytes .../__pycache__/theme.cpython-313.pyc | Bin 0 -> 38184 bytes ztools/ztools/resources/icons.py | 386 +++++ .../resources/icons/ztools_axis_2pt.svg | 8 + .../resources/icons/ztools_axis_cyl.svg | 11 + .../resources/icons/ztools_axis_edge.svg | 9 + .../resources/icons/ztools_axis_intersect.svg | 9 + .../resources/icons/ztools_datum_creator.svg | 8 + .../resources/icons/ztools_datum_manager.svg | 10 + .../resources/icons/ztools_plane_3pt.svg | 9 + .../resources/icons/ztools_plane_angled.svg | 9 + .../resources/icons/ztools_plane_midplane.svg | 9 + .../resources/icons/ztools_plane_normal.svg | 9 + .../resources/icons/ztools_plane_offset.svg | 10 + .../resources/icons/ztools_plane_tangent.svg | 9 + .../icons/ztools_pocket_enhanced.svg | 12 + .../resources/icons/ztools_pocket_flipped.svg | 14 + .../resources/icons/ztools_point_circle.svg | 10 + .../resources/icons/ztools_point_edge.svg | 9 + .../resources/icons/ztools_point_face.svg | 8 + .../resources/icons/ztools_point_vertex.svg | 10 + .../resources/icons/ztools_point_xyz.svg | 12 + .../icons/ztools_rotated_pattern.svg | 18 + .../resources/icons/ztools_theme_apply.svg | 15 + .../resources/icons/ztools_theme_export.svg | 15 + .../resources/icons/ztools_theme_remove.svg | 15 + .../resources/icons/ztools_theme_toggle.svg | 17 + .../resources/icons/ztools_workbench.svg | 5 + ztools/ztools/resources/theme.py | 1306 +++++++++++++++++ 67 files changed, 7119 insertions(+) create mode 100644 CatppuccinMocha/CatppuccinMocha.cfg create mode 100644 CatppuccinMocha/CatppuccinMocha.qss create mode 100644 Makefile create mode 100644 TODO_ATTACHMENT_WORK.md create mode 100644 package.xml create mode 100644 partdesign.md create mode 100644 ztools/Init.py create mode 100644 ztools/InitGui.py create mode 100644 ztools/README.md create mode 100644 ztools/__pycache__/Init.cpython-313.pyc create mode 100644 ztools/__pycache__/InitGui.cpython-313.pyc create mode 100644 ztools/setup.cfg create mode 100644 ztools/ztools/__init__.py create mode 100644 ztools/ztools/__pycache__/__init__.cpython-312.pyc create mode 100644 ztools/ztools/__pycache__/__init__.cpython-313.pyc create mode 100644 ztools/ztools/commands/__init__.py create mode 100644 ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc create mode 100644 ztools/ztools/commands/__pycache__/__init__.cpython-313.pyc create mode 100644 ztools/ztools/commands/__pycache__/datum_commands.cpython-312.pyc create mode 100644 ztools/ztools/commands/__pycache__/datum_commands.cpython-313.pyc create mode 100644 ztools/ztools/commands/__pycache__/pattern_commands.cpython-312.pyc create mode 100644 ztools/ztools/commands/__pycache__/pocket_commands.cpython-312.pyc create mode 100644 ztools/ztools/commands/__pycache__/pocket_commands.cpython-313.pyc create mode 100644 ztools/ztools/commands/__pycache__/theme_commands.cpython-312.pyc create mode 100644 ztools/ztools/commands/__pycache__/theme_commands.cpython-313.pyc create mode 100644 ztools/ztools/commands/datum_commands.py create mode 100644 ztools/ztools/commands/pattern_commands.py create mode 100644 ztools/ztools/commands/pocket_commands.py create mode 100644 ztools/ztools/datums/__init__.py create mode 100644 ztools/ztools/datums/__pycache__/__init__.cpython-312.pyc create mode 100644 ztools/ztools/datums/__pycache__/core.cpython-312.pyc create mode 100644 ztools/ztools/datums/__pycache__/core.cpython-313.pyc create mode 100644 ztools/ztools/datums/core.py create mode 100644 ztools/ztools/resources/__init__.py create mode 100644 ztools/ztools/resources/__pycache__/__init__.cpython-312.pyc create mode 100644 ztools/ztools/resources/__pycache__/__init__.cpython-313.pyc create mode 100644 ztools/ztools/resources/__pycache__/icons.cpython-312.pyc create mode 100644 ztools/ztools/resources/__pycache__/icons.cpython-313.pyc create mode 100644 ztools/ztools/resources/__pycache__/theme.cpython-312.pyc create mode 100644 ztools/ztools/resources/__pycache__/theme.cpython-313.pyc create mode 100644 ztools/ztools/resources/icons.py create mode 100644 ztools/ztools/resources/icons/ztools_axis_2pt.svg create mode 100644 ztools/ztools/resources/icons/ztools_axis_cyl.svg create mode 100644 ztools/ztools/resources/icons/ztools_axis_edge.svg create mode 100644 ztools/ztools/resources/icons/ztools_axis_intersect.svg create mode 100644 ztools/ztools/resources/icons/ztools_datum_creator.svg create mode 100644 ztools/ztools/resources/icons/ztools_datum_manager.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_3pt.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_angled.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_midplane.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_normal.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_offset.svg create mode 100644 ztools/ztools/resources/icons/ztools_plane_tangent.svg create mode 100644 ztools/ztools/resources/icons/ztools_pocket_enhanced.svg create mode 100644 ztools/ztools/resources/icons/ztools_pocket_flipped.svg create mode 100644 ztools/ztools/resources/icons/ztools_point_circle.svg create mode 100644 ztools/ztools/resources/icons/ztools_point_edge.svg create mode 100644 ztools/ztools/resources/icons/ztools_point_face.svg create mode 100644 ztools/ztools/resources/icons/ztools_point_vertex.svg create mode 100644 ztools/ztools/resources/icons/ztools_point_xyz.svg create mode 100644 ztools/ztools/resources/icons/ztools_rotated_pattern.svg create mode 100644 ztools/ztools/resources/icons/ztools_theme_apply.svg create mode 100644 ztools/ztools/resources/icons/ztools_theme_export.svg create mode 100644 ztools/ztools/resources/icons/ztools_theme_remove.svg create mode 100644 ztools/ztools/resources/icons/ztools_theme_toggle.svg create mode 100644 ztools/ztools/resources/icons/ztools_workbench.svg create mode 100644 ztools/ztools/resources/theme.py diff --git a/CatppuccinMocha/CatppuccinMocha.cfg b/CatppuccinMocha/CatppuccinMocha.cfg new file mode 100644 index 0000000..fc726cc --- /dev/null +++ b/CatppuccinMocha/CatppuccinMocha.cfg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CatppuccinMocha.qss + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CatppuccinMocha/CatppuccinMocha.qss b/CatppuccinMocha/CatppuccinMocha.qss new file mode 100644 index 0000000..aaf927b --- /dev/null +++ b/CatppuccinMocha/CatppuccinMocha.qss @@ -0,0 +1,1249 @@ + +/* ============================================================================= + Catppuccin Mocha Theme for FreeCAD + Bundled with ztools addon + https://catppuccin.com/ + ============================================================================= */ + +/* ============================================================================= + Global Defaults + ============================================================================= */ + +* { + color: #cdd6f4; + font-family: "Segoe UI", "Ubuntu", "Noto Sans", sans-serif; +} + +QWidget { + background-color: #1e1e2e; + color: #cdd6f4; + selection-background-color: #585b70; + selection-color: #cdd6f4; +} + +/* ============================================================================= + Main Window and MDI Area + ============================================================================= */ + +QMainWindow { + background-color: #181825; +} + +QMainWindow::separator { + background-color: #313244; + width: 4px; + height: 4px; +} + +QMainWindow::separator:hover { + background-color: #cba6f7; +} + +QMdiArea { + background-color: #11111b; +} + +QMdiSubWindow { + background-color: #1e1e2e; + border: 1px solid #45475a; +} + +QMdiSubWindow > QWidget { + background-color: #1e1e2e; +} + +/* ============================================================================= + Menu Bar + ============================================================================= */ + +QMenuBar { + background-color: #181825; + color: #cdd6f4; + border-bottom: 1px solid #313244; + padding: 2px; +} + +QMenuBar::item { + background-color: transparent; + padding: 4px 8px; + border-radius: 4px; +} + +QMenuBar::item:selected { + background-color: #313244; +} + +QMenuBar::item:pressed { + background-color: #45475a; +} + +/* ============================================================================= + Menus + ============================================================================= */ + +QMenu { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 6px; + padding: 4px; +} + +QMenu::item { + padding: 6px 24px 6px 8px; + border-radius: 4px; +} + +QMenu::item:selected { + background-color: #45475a; + color: #cdd6f4; +} + +QMenu::item:disabled { + color: #6c7086; +} + +QMenu::separator { + height: 1px; + background-color: #45475a; + margin: 4px 8px; +} + +QMenu::icon { + margin-left: 8px; +} + +QMenu::indicator { + width: 16px; + height: 16px; + margin-left: 4px; +} + +/* ============================================================================= + Toolbars + ============================================================================= */ + +QToolBar { + background-color: #181825; + border: none; + spacing: 2px; + padding: 2px; +} + +QToolBar::handle { + background-color: #45475a; + width: 8px; + margin: 2px; + border-radius: 2px; +} + +QToolBar::handle:horizontal { + width: 8px; +} + +QToolBar::handle:vertical { + height: 8px; +} + +QToolBar::separator { + background-color: #45475a; + width: 1px; + margin: 4px 2px; +} + +/* ============================================================================= + Tool Buttons (Toolbar icons) + ============================================================================= */ + +QToolButton { + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; + padding: 4px; + margin: 1px; +} + +QToolButton:hover { + background-color: #313244; + border: 1px solid #45475a; +} + +QToolButton:pressed { + background-color: #45475a; +} + +QToolButton:checked { + background-color: #45475a; + border: 1px solid #cba6f7; +} + +QToolButton:disabled { + color: #6c7086; +} + +QToolButton[popupMode="1"] { + padding-right: 16px; +} + +QToolButton::menu-button { + border: none; + width: 14px; +} + +QToolButton::menu-arrow { + width: 10px; + height: 10px; +} + +/* ============================================================================= + Push Buttons + ============================================================================= */ + +QPushButton { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 6px; + padding: 6px 16px; + min-height: 20px; +} + +QPushButton:hover { + background-color: #45475a; + border-color: #585b70; +} + +QPushButton:pressed { + background-color: #585b70; +} + +QPushButton:checked { + background-color: #cba6f7; + color: #11111b; + border-color: #cba6f7; +} + +QPushButton:disabled { + background-color: #313244; + color: #6c7086; + border-color: #313244; +} + +QPushButton:default { + border: 2px solid #cba6f7; +} + +/* ============================================================================= + Dock Widgets + ============================================================================= */ + +QDockWidget { + background-color: #1e1e2e; + color: #cdd6f4; + titlebar-close-icon: none; + titlebar-normal-icon: none; +} + +QDockWidget::title { + background-color: #181825; + color: #cdd6f4; + padding: 6px; + border-bottom: 1px solid #313244; +} + +QDockWidget::close-button, +QDockWidget::float-button { + background-color: transparent; + border: none; + padding: 2px; +} + +QDockWidget::close-button:hover, +QDockWidget::float-button:hover { + background-color: #313244; + border-radius: 4px; +} + +/* ============================================================================= + Tab Widgets + ============================================================================= */ + +QTabWidget::pane { + background-color: #1e1e2e; + border: 1px solid #45475a; + border-radius: 4px; + top: -1px; +} + +QTabBar { + background-color: transparent; +} + +QTabBar::tab { + background-color: #313244; + color: #bac2de; + border: 1px solid #45475a; + padding: 6px 12px; + margin-right: 2px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +QTabBar::tab:selected { + background-color: #1e1e2e; + color: #cdd6f4; + border-bottom-color: #1e1e2e; +} + +QTabBar::tab:hover:!selected { + background-color: #45475a; + color: #cdd6f4; +} + +QTabBar::tab:disabled { + color: #6c7086; +} + +QTabBar::close-button { + margin-left: 4px; +} + +QTabBar::close-button:hover { + background-color: #f38ba8; + border-radius: 2px; +} + +/* ============================================================================= + Scroll Bars + ============================================================================= */ + +QScrollBar:horizontal { + background-color: #181825; + height: 12px; + margin: 0 12px 0 12px; + border-radius: 6px; +} + +QScrollBar:vertical { + background-color: #181825; + width: 12px; + margin: 12px 0 12px 0; + border-radius: 6px; +} + +QScrollBar::handle:horizontal { + background-color: #45475a; + min-width: 20px; + border-radius: 5px; + margin: 1px; +} + +QScrollBar::handle:vertical { + background-color: #45475a; + min-height: 20px; + border-radius: 5px; + margin: 1px; +} + +QScrollBar::handle:horizontal:hover, +QScrollBar::handle:vertical:hover { + background-color: #585b70; +} + +QScrollBar::add-line:horizontal, +QScrollBar::sub-line:horizontal, +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical { + width: 12px; + height: 12px; + background-color: #313244; + border-radius: 6px; +} + +QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, +QScrollBar::sub-line:vertical:hover { + background-color: #45475a; +} + +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal, +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background-color: transparent; +} + +/* ============================================================================= + Input Fields + ============================================================================= */ + +QLineEdit { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; + selection-background-color: #cba6f7; + selection-color: #11111b; +} + +QLineEdit:focus { + border-color: #cba6f7; +} + +QLineEdit:disabled { + background-color: #181825; + color: #6c7086; +} + +QLineEdit:read-only { + background-color: #181825; +} + +QTextEdit, QPlainTextEdit { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + selection-background-color: #cba6f7; + selection-color: #11111b; +} + +QTextEdit:focus, QPlainTextEdit:focus { + border-color: #cba6f7; +} + +/* ============================================================================= + Spin Boxes + ============================================================================= */ + +QSpinBox, QDoubleSpinBox { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px; + padding-right: 20px; +} + +QSpinBox:focus, QDoubleSpinBox:focus { + border-color: #cba6f7; +} + +QSpinBox:disabled, QDoubleSpinBox:disabled { + background-color: #181825; + color: #6c7086; +} + +QSpinBox::up-button, QDoubleSpinBox::up-button { + subcontrol-origin: border; + subcontrol-position: top right; + width: 16px; + border-left: 1px solid #45475a; + border-top-right-radius: 4px; + background-color: #45475a; +} + +QSpinBox::down-button, QDoubleSpinBox::down-button { + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 16px; + border-left: 1px solid #45475a; + border-bottom-right-radius: 4px; + background-color: #45475a; +} + +QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, +QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover { + background-color: #585b70; +} + +QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed, +QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed { + background-color: #cba6f7; +} + +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { + width: 8px; + height: 8px; +} + +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { + width: 8px; + height: 8px; +} + +/* ============================================================================= + Combo Boxes + ============================================================================= */ + +QComboBox { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; + padding-right: 24px; + min-height: 20px; +} + +QComboBox:hover { + border-color: #585b70; +} + +QComboBox:focus { + border-color: #cba6f7; +} + +QComboBox:disabled { + background-color: #181825; + color: #6c7086; +} + +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 20px; + border-left: 1px solid #45475a; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background-color: #45475a; +} + +QComboBox::drop-down:hover { + background-color: #585b70; +} + +QComboBox::down-arrow { + width: 10px; + height: 10px; +} + +QComboBox QAbstractItemView { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + selection-background-color: #45475a; + selection-color: #cdd6f4; + outline: none; +} + +QComboBox QAbstractItemView::item { + padding: 4px 8px; + min-height: 24px; +} + +QComboBox QAbstractItemView::item:hover { + background-color: #45475a; +} + +QComboBox QAbstractItemView::item:selected { + background-color: #585b70; +} + +/* ============================================================================= + Check Boxes + ============================================================================= */ + +QCheckBox { + spacing: 8px; + color: #cdd6f4; +} + +QCheckBox:disabled { + color: #6c7086; +} + +QCheckBox::indicator { + width: 18px; + height: 18px; + border: 2px solid #585b70; + border-radius: 4px; + background-color: #313244; +} + +QCheckBox::indicator:hover { + border-color: #cba6f7; +} + +QCheckBox::indicator:checked { + background-color: #cba6f7; + border-color: #cba6f7; +} + +QCheckBox::indicator:checked:disabled { + background-color: #6c7086; + border-color: #6c7086; +} + +QCheckBox::indicator:disabled { + background-color: #181825; + border-color: #45475a; +} + +/* ============================================================================= + Radio Buttons + ============================================================================= */ + +QRadioButton { + spacing: 8px; + color: #cdd6f4; +} + +QRadioButton:disabled { + color: #6c7086; +} + +QRadioButton::indicator { + width: 18px; + height: 18px; + border: 2px solid #585b70; + border-radius: 9px; + background-color: #313244; +} + +QRadioButton::indicator:hover { + border-color: #cba6f7; +} + +QRadioButton::indicator:checked { + background-color: #cba6f7; + border-color: #cba6f7; +} + +QRadioButton::indicator:checked:disabled { + background-color: #6c7086; + border-color: #6c7086; +} + +QRadioButton::indicator:disabled { + background-color: #181825; + border-color: #45475a; +} + +/* ============================================================================= + Sliders + ============================================================================= */ + +QSlider::groove:horizontal { + height: 6px; + background-color: #45475a; + border-radius: 3px; +} + +QSlider::groove:vertical { + width: 6px; + background-color: #45475a; + border-radius: 3px; +} + +QSlider::handle:horizontal { + width: 16px; + height: 16px; + margin: -5px 0; + background-color: #cba6f7; + border-radius: 8px; +} + +QSlider::handle:vertical { + width: 16px; + height: 16px; + margin: 0 -5px; + background-color: #cba6f7; + border-radius: 8px; +} + +QSlider::handle:horizontal:hover, +QSlider::handle:vertical:hover { + background-color: #b4befe; +} + +QSlider::handle:horizontal:pressed, +QSlider::handle:vertical:pressed { + background-color: #f5c2e7; +} + +QSlider::sub-page:horizontal { + background-color: #cba6f7; + border-radius: 3px; +} + +QSlider::add-page:vertical { + background-color: #cba6f7; + border-radius: 3px; +} + +/* ============================================================================= + Progress Bars + ============================================================================= */ + +QProgressBar { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + text-align: center; + height: 20px; +} + +QProgressBar::chunk { + background-color: #cba6f7; + border-radius: 3px; +} + +/* ============================================================================= + Group Boxes + ============================================================================= */ + +QGroupBox { + background-color: #1e1e2e; + border: 1px solid #45475a; + border-radius: 6px; + margin-top: 12px; + padding-top: 8px; +} + +QGroupBox::title { + subcontrol-origin: margin; + subcontrol-position: top left; + left: 12px; + padding: 0 4px; + color: #bac2de; + background-color: #1e1e2e; +} + +/* ============================================================================= + Tree View + ============================================================================= */ + +QTreeView { + background-color: #1e1e2e; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + outline: none; +} + +QTreeView::item { + padding: 4px; + border-radius: 2px; +} + +QTreeView::item:hover { + background-color: #313244; +} + +QTreeView::item:selected { + background-color: #45475a; + color: #cdd6f4; +} + +QTreeView::item:selected:active { + background-color: #585b70; +} + +/* Branch indicators (collapse/expand arrows) - uses FreeCAD built-in light images for dark theme */ +QTreeView::branch { + background: transparent; +} + +QTreeView::branch:has-siblings:!adjoins-item { + border-image: url(qss:images_dark-light/branch_vline_light.svg) 0; +} + +QTreeView::branch:has-siblings:adjoins-item { + border-image: url(qss:images_dark-light/branch_more_light.svg) 0; +} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item { + border-image: url(qss:images_dark-light/branch_end_light.svg) 0; +} + +QTreeView::branch:closed:has-children:has-siblings { + border-image: url(qss:images_dark-light/branch_more_closed_light.svg) 0; +} + +QTreeView::branch:has-children:!has-siblings:closed { + border-image: url(qss:images_dark-light/branch_end_closed_light.svg) 0; +} + +QTreeView::branch:open:has-children:has-siblings { + border-image: url(qss:images_dark-light/branch_more_open_light.svg) 0; +} + +QTreeView::branch:open:has-children:!has-siblings { + border-image: url(qss:images_dark-light/branch_end_open_light.svg) 0; +} + +/* ============================================================================= + List View + ============================================================================= */ + +QListView { + background-color: #1e1e2e; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + outline: none; +} + +QListView::item { + padding: 4px; + border-radius: 2px; +} + +QListView::item:hover { + background-color: #313244; +} + +QListView::item:selected { + background-color: #45475a; + color: #cdd6f4; +} + +/* ============================================================================= + Table View + ============================================================================= */ + +QTableView { + background-color: #1e1e2e; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + gridline-color: #313244; + outline: none; +} + +QTableView::item { + padding: 4px; +} + +QTableView::item:hover { + background-color: #313244; +} + +QTableView::item:selected { + background-color: #45475a; + color: #cdd6f4; +} + +QTableView QTableCornerButton::section { + background-color: #313244; + border: 1px solid #45475a; +} + +/* ============================================================================= + Header Views (for Tables/Trees) + ============================================================================= */ + +QHeaderView { + background-color: #313244; + border: none; +} + +QHeaderView::section { + background-color: #313244; + color: #bac2de; + border: none; + border-right: 1px solid #45475a; + border-bottom: 1px solid #45475a; + padding: 6px 8px; +} + +QHeaderView::section:hover { + background-color: #45475a; + color: #cdd6f4; +} + +QHeaderView::section:checked { + background-color: #45475a; +} + +QHeaderView::down-arrow { + width: 10px; + height: 10px; +} + +QHeaderView::up-arrow { + width: 10px; + height: 10px; +} + +/* ============================================================================= + Splitters + ============================================================================= */ + +QSplitter::handle { + background-color: #313244; +} + +QSplitter::handle:horizontal { + width: 4px; +} + +QSplitter::handle:vertical { + height: 4px; +} + +QSplitter::handle:hover { + background-color: #cba6f7; +} + +/* ============================================================================= + Status Bar + ============================================================================= */ + +QStatusBar { + background-color: #181825; + color: #bac2de; + border-top: 1px solid #313244; +} + +QStatusBar::item { + border: none; +} + +QStatusBar QLabel { + padding: 2px 8px; +} + +/* ============================================================================= + Tooltips + ============================================================================= */ + +QToolTip { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; +} + +/* ============================================================================= + Labels + ============================================================================= */ + +QLabel { + color: #cdd6f4; + background-color: transparent; +} + +QLabel:disabled { + color: #6c7086; +} + +/* ============================================================================= + Frames + ============================================================================= */ + +QFrame { + border: none; +} + +QFrame[frameShape="4"] { + /* HLine */ + background-color: #45475a; + max-height: 1px; +} + +QFrame[frameShape="5"] { + /* VLine */ + background-color: #45475a; + max-width: 1px; +} + +/* ============================================================================= + Tool Box (Collapsible sections) + ============================================================================= */ + +QToolBox { + background-color: #1e1e2e; + border: 1px solid #45475a; + border-radius: 4px; +} + +QToolBox::tab { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 8px; +} + +QToolBox::tab:selected { + background-color: #45475a; + border-color: #cba6f7; +} + +QToolBox::tab:hover { + background-color: #45475a; +} + +/* ============================================================================= + Dialog Buttons + ============================================================================= */ + +QDialogButtonBox { + button-layout: 0; +} + +/* ============================================================================= + Date/Time Edits + ============================================================================= */ + +QDateEdit, QTimeEdit, QDateTimeEdit { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px; +} + +QDateEdit:focus, QTimeEdit:focus, QDateTimeEdit:focus { + border-color: #cba6f7; +} + +QDateEdit::drop-down, QTimeEdit::drop-down, QDateTimeEdit::drop-down { + subcontrol-origin: padding; + subcontrol-position: top right; + width: 20px; + border-left: 1px solid #45475a; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background-color: #45475a; +} + +QCalendarWidget { + background-color: #1e1e2e; +} + +QCalendarWidget QToolButton { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + margin: 2px; +} + +QCalendarWidget QToolButton:hover { + background-color: #45475a; +} + +QCalendarWidget QMenu { + background-color: #313244; +} + +QCalendarWidget QSpinBox { + background-color: #313244; +} + +QCalendarWidget QAbstractItemView { + background-color: #1e1e2e; + selection-background-color: #cba6f7; + selection-color: #11111b; +} + +/* ============================================================================= + Wizard + ============================================================================= */ + +QWizard { + background-color: #1e1e2e; +} + +QWizard QLabel { + color: #cdd6f4; +} + +/* ============================================================================= + FreeCAD Specific Widgets + ============================================================================= */ + +/* Property Editor */ +Gui--PropertyEditor--PropertyEditor { + background-color: #1e1e2e; + color: #cdd6f4; + border: 1px solid #45475a; + qproperty-groupBackground: #313244; + qproperty-groupTextColor: #bac2de; + qproperty-itemBackground: #1e1e2e; +} + +Gui--PropertyEditor--PropertyEditor QLineEdit { + background-color: #313244; + border: 1px solid #45475a; +} + +Gui--PropertyEditor--PropertyEditor QComboBox { + background-color: #313244; +} + +/* Color Button */ +Gui--ColorButton { + background-color: #313244; + border: 1px solid #45475a; + border-radius: 4px; + padding: 2px; +} + +Gui--ColorButton:hover { + border-color: #cba6f7; +} + +/* Workbench Selector */ +Gui--WorkbenchComboBox { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; + padding: 4px 8px; +} + +Gui--WorkbenchComboBox:hover { + border-color: #585b70; +} + +Gui--WorkbenchComboBox::drop-down { + background-color: #45475a; + border-left: 1px solid #45475a; + border-radius: 0 4px 4px 0; +} + +/* Task Panel */ +QSint--ActionGroup { + background-color: #313244; + border: 1px solid #45475a; + border-radius: 6px; +} + +QSint--ActionGroup QToolButton { + background-color: #313244; + color: #cdd6f4; + border: none; + border-radius: 4px; + padding: 6px; +} + +QSint--ActionGroup QToolButton:hover { + background-color: #45475a; +} + +QSint--ActionGroup QFrame { + background-color: #1e1e2e; + border: none; + border-radius: 4px; +} + +/* Input Field */ +Gui--InputField { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 4px; +} + +Gui--InputField:focus { + border-color: #cba6f7; +} + +/* Expression Completer */ +Gui--ExpressionCompleter { + background-color: #313244; + color: #cdd6f4; + border: 1px solid #45475a; +} + +/* Spreadsheet */ +SpreadsheetGui--SheetTableView { + background-color: #1e1e2e; + color: #cdd6f4; + gridline-color: #45475a; + selection-background-color: #45475a; + selection-color: #cdd6f4; +} + +SpreadsheetGui--SheetTableView QHeaderView::section { + background-color: #313244; + color: #bac2de; + border: 1px solid #45475a; + padding: 4px; +} + +/* Python Console */ +Gui--PythonConsole { + background-color: #11111b; + color: #cdd6f4; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; + selection-background-color: #45475a; +} + +/* Python Editor */ +Gui--PythonEditor { + background-color: #11111b; + color: #cdd6f4; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; + selection-background-color: #cba6f7; + selection-color: #11111b; +} + +/* Report View */ +Gui--DockWnd--ReportOutput { + background-color: #11111b; + color: #cdd6f4; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; +} + +/* DAG View */ +Gui--DAG--Model { + background-color: #1e1e2e; +} + +/* ============================================================================= + Sketcher Specific Styles + ============================================================================= */ + +/* Sketcher constraint colors are handled via preferences, not QSS */ + +/* ============================================================================= + Syntax Highlighting Colors (Python Editor) + Note: These are typically set via FreeCAD preferences, but we define them here + for reference and any widgets that support them. + ============================================================================= */ + +/* + Python Editor Syntax Colors (Catppuccin Mocha): + - Comment: #7f849c + - Number: #fab387 + - String: #a6e3a1 + - Keyword: #cba6f7 + - Class/Def name: #89b4fa + - Operator: #89dceb + - Output: #cdd6f4 + - Error: #f38ba8 +*/ + +/* ============================================================================= + Custom Color Accents by Context + ============================================================================= */ + +/* Success states */ +*[state="success"] { + color: #a6e3a1; +} + +/* Warning states */ +*[state="warning"] { + color: #f9e2af; +} + +/* Error states */ +*[state="error"] { + color: #f38ba8; +} + +/* Info states */ +*[state="info"] { + color: #89b4fa; +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b2d881a --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +# Makefile for ZTools FreeCAD Workbench +# Installs to local flatpak FreeCAD instance + +WORKBENCH_NAME := ZTools +FLATPAK_APP := org.freecad.FreeCAD + +# FreeCAD Mod directory for flatpak +FLATPAK_MOD_DIR := $(HOME)/.var/app/$(FLATPAK_APP)/data/FreeCAD/Mod +INSTALL_DIR := $(FLATPAK_MOD_DIR)/$(WORKBENCH_NAME) + +.PHONY: all install uninstall clean check-flatpak list dev-install dev-uninstall run help + +all: install + +# Check if flatpak FreeCAD is installed +check-flatpak: + @if ! flatpak list | grep -q $(FLATPAK_APP); then \ + echo "Error: FreeCAD flatpak ($(FLATPAK_APP)) is not installed"; \ + exit 1; \ + fi + +# Install the workbench +install: check-flatpak + @echo "Installing $(WORKBENCH_NAME) workbench to flatpak FreeCAD..." + @mkdir -p $(INSTALL_DIR)/ztools/commands + @mkdir -p $(INSTALL_DIR)/ztools/datums + @mkdir -p $(INSTALL_DIR)/ztools/resources/icons + @cp -v package.xml $(INSTALL_DIR)/ + @cp -v ztools/InitGui.py $(INSTALL_DIR)/ + @cp -v ztools/ztools/*.py $(INSTALL_DIR)/ztools/ + @cp -v ztools/ztools/commands/*.py $(INSTALL_DIR)/ztools/commands/ + @cp -v ztools/ztools/datums/*.py $(INSTALL_DIR)/ztools/datums/ + @cp -v ztools/ztools/resources/*.py $(INSTALL_DIR)/ztools/resources/ + @cp -v ztools/ztools/resources/icons/*.svg $(INSTALL_DIR)/ztools/resources/icons/ + @echo "Installation complete!" + @echo "Restart FreeCAD to load the workbench." + +# Uninstall the workbench +uninstall: + @echo "Uninstalling $(WORKBENCH_NAME) workbench..." + @rm -rf $(INSTALL_DIR) + @echo "Uninstallation complete!" + +# Clean build artifacts (if any) +clean: + @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + @find . -type f -name "*.pyc" -delete 2>/dev/null || true + @echo "Cleaned build artifacts." + +# List installed files +list: + @echo "Installed files in $(INSTALL_DIR):" + @find $(INSTALL_DIR) -type f 2>/dev/null || echo "Workbench not installed." + +# Development: install with symlink for live editing +dev-install: check-flatpak + @echo "Installing $(WORKBENCH_NAME) workbench (dev mode with symlink)..." + @mkdir -p $(FLATPAK_MOD_DIR) + @rm -rf $(INSTALL_DIR) + @ln -sfv $(CURDIR) $(INSTALL_DIR) + @echo "Dev installation complete (symlinked)!" + @echo "Changes to source files will be reflected immediately (restart FreeCAD to reload)." + +# Remove dev symlink +dev-uninstall: + @echo "Removing dev symlink..." + @rm -f $(INSTALL_DIR) + @echo "Dev uninstallation complete!" + +# Run FreeCAD flatpak +run: + @echo "Starting FreeCAD flatpak..." + @flatpak run $(FLATPAK_APP) + +# Show help +help: + @echo "ZTools Workbench Makefile" + @echo "" + @echo "Targets:" + @echo " install - Install workbench to flatpak FreeCAD" + @echo " uninstall - Remove workbench from flatpak FreeCAD" + @echo " dev-install - Install as symlink for development" + @echo " dev-uninstall- Remove development symlink" + @echo " clean - Remove Python cache files" + @echo " list - List installed files" + @echo " run - Start FreeCAD flatpak" + @echo " help - Show this help message" + @echo "" + @echo "Install directory: $(INSTALL_DIR)" diff --git a/TODO_ATTACHMENT_WORK.md b/TODO_ATTACHMENT_WORK.md new file mode 100644 index 0000000..0c13a7f --- /dev/null +++ b/TODO_ATTACHMENT_WORK.md @@ -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. diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..4f29cd2 --- /dev/null +++ b/package.xml @@ -0,0 +1,32 @@ + + + + ZTools + + Extended PartDesign workbench with velocity-focused tools, advanced datum creation, and Catppuccin Mocha theme. + + 0.1.0 + + 2026-01-24 + + LGPL-3.0-or-later + + + + ZTools + ZToolsWorkbench + ./ztools + + + CatppuccinMocha + Catppuccin Mocha dark theme - soothing pastel colors for the high-spirited + ./CatppuccinMocha + color + dark + catppuccin + mocha + theme + + + + diff --git a/partdesign.md b/partdesign.md new file mode 100644 index 0000000..333dbf9 --- /dev/null +++ b/partdesign.md @@ -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', '<>.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* diff --git a/ztools/Init.py b/ztools/Init.py new file mode 100644 index 0000000..79204b3 --- /dev/null +++ b/ztools/Init.py @@ -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") diff --git a/ztools/InitGui.py b/ztools/InitGui.py new file mode 100644 index 0000000..2507c31 --- /dev/null +++ b/ztools/InitGui.py @@ -0,0 +1,233 @@ +# 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 + try: + # Get list of available workbenches + wb_list = Gui.listWorkbenches() + + # Initialize PartDesign workbench if available + if "PartDesignWorkbench" in wb_list: + pd_wb = Gui.getWorkbench("PartDesignWorkbench") + # Call Initialize if not already done + if hasattr(pd_wb, "Initialize"): + pd_wb.Initialize() + + # Initialize Sketcher workbench if available + if "SketcherWorkbench" in wb_list: + sketcher_wb = Gui.getWorkbench("SketcherWorkbench") + if hasattr(sketcher_wb, "Initialize"): + sketcher_wb.Initialize() + + except Exception as e: + App.Console.PrintWarning(f"Could not initialize PartDesign/Sketcher: {e}\n") + + from ztools.commands import datum_commands, pattern_commands, pocket_commands + + # ===================================================================== + # 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", + ] + + # ===================================================================== + # 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("ztools Datums", self.ztools_datum_tools) + self.appendToolbar("ztools Patterns", self.ztools_pattern_tools) + self.appendToolbar("ztools Features", self.ztools_pocket_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( + "ztools", + self.ztools_datum_tools + + self.ztools_pattern_tools + + self.ztools_pocket_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()) diff --git a/ztools/README.md b/ztools/README.md new file mode 100644 index 0000000..b1d9e4a --- /dev/null +++ b/ztools/README.md @@ -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 diff --git a/ztools/__pycache__/Init.cpython-313.pyc b/ztools/__pycache__/Init.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db5e049b59084d1df03a1120c0b235a40141c1e3 GIT binary patch literal 249 zcmey&%ge<81f6*$nNt`U7#@Q-Fu)9De0BmdrZNOG1T%Uw6fwpy2C=}DAm|`=1RKT* zW-5Q7pFF{PdDv_#^{QR6^g~XJU{5*x6{KS;h6fRAcTkLK{sj1G6F1MH+ z3kq(rJLl&W=jWu};s_|p%q#K9PhZLK8DzjMcF(-b61{@TTO2mI`6;D2sdhymPk^i{ rmIo3am>C%vKXWiJihU4ZU}S0MZsfksAaapGQ}K?6u~orH7TA(=Wfd$4U zx$j)S%;ONqSr@l+DfD3|g>qIg=QA@Ly5q|PzbA#cf+eFbOO9Thh2kUhA+-eic^?k& zejMZjIK&5W6Cc81zNr&oh7Us;;TcGyd<4>FK8o2p%^B86{CrVSO6pAof4nTph1Ktg z`RijB?`xu56pP%vfVF8+mF~#gJzIynCuytPU9qGTB<;cR6{S#7K~*CaWsRnFnpr>7 zs17WaOF}_h6J?ECQ82e6sG5kmsflS?{Ypu>rwYIGBNXMVXLyLaH9yM@4LgXc37S;k z3abL6LfA@C5>oj)*6VIWL?h@JMdt-JIZs?a#2^+hu9b!*M%gS?5XbmL0X8Y$KUT zJlaS`L_p2HK}IBRkeQ-}JWJF_W{Voj%#XOx;2Ve;^w}-@KR&a)Y`(Yq^C!rDGfo4h zU=K7h>YCt>4C7ZX!(`BPquT_f&jQ5j&c_|jvXA#I`Nly(co%tD!JQmLrV*b#{AK@b zXvut*{JcMBwK8a7LJLAm{)LFGW8{w1u0c!0*2Wg|6cBg!hmOJe75?ZibZ$t3r z+X3(N09MxaDu>>{>j3=NTktx8cjzs6UBK(!%>$I%jUESR&o4Lx+|+I^puM}%A64UF z;P&t4?rBLk@a%!(q3_A-0p5^@U}g8@alp&I1@8#(-gygN3V0{?;KA&(^Z8M+*@%~h z_V$X+M!a6WlZ>f72|h`H_K__nj%U2x`jHwqU<-MpRS`|WiIHo#3s&~3UcC(<=>g`%duBtKu+ied=SzYt8VL{bfds%d6-AOYqsyZ*r6{;4$$}>G4`55b zX0&;P{HjnECnY#gV57r}SC+jZZwpf;*raDdM%<%5FBFXfp7MftS1DCUKNtr+@Ij*xQi7iM z=*(b6xhoo-9*D0jYnX~k#|DaXmzlQ(EDb=qc|&+E$pD9cmoC~3@OnPw3;X3 z{4^P5Q&13m>Gt(Hna(t<5ge`BZ$ISCDXV$cyu!>eMst#}oz6Tmt zjV9}Jqi(lga<*ANY*F1~zB@S;qnX1-V5TA&iISvh&Qu2Vv3EqbY=l<@6#@ujax5_- z7w;FuG993Si85F{rN}B78Z3+{S-UAvS--b&=52Vqh)@wi|OeXHE+l*{%QR*ikCJMirKypTsV2?Nv&ZQ22rcC1Kxp;ESs z%VuC&#kgwC_f~nobwS~^GihP=1a4a2YNO^PZh|qPT!x#NMQCGLz(&+8&Wduya^fu5 z0#TV^jG!u(R*dkyhR?A z?y1k4UjS$PDO+#N>8-hqWKTUgq$h`J$z%VDqR0)O&Ma-RDAv8frZz(XF8WEZ#vIye zVFqX}z17*&Lvvl5?IY7a4H2vn^)Hlrxk>$bWXN0C^a>C_qWQNjA)S$*(qjXAf$ zuogP@M5_;s=>uamX52;3JXx&wUC{e3)R+kub#%X8te;!b;Ww~UV{SMK8*Inpk$Tq& zz3aqR)Af;SI{dn>y$<*zQE<3zNVV(O(_%e4qi1Jo%w>oCnASe34~*8BF&7HnCURle0Bu&Jt|IIv$Tz*?!{IaeZ+7>tg-A8#??3 zZ@8aKY9NZ|s%&P1ZK<;uh=>zP}zH(BlI?1bnf`C|jGL!n6YHN*+~S@WmOb%xU!u6ktRTW0d#uO9?aOUEnZgE9AlO?-Cs)2ns1 zS7&>x1J|nyeD(Tbb@4{E?`EC7^^Cps-^~C~{Etd*Eq*(hn9HGWa?xwY1K*raUmFbk zX)px&Z5LhSl8at+O+x08_ri+oLw7t2_RY8lcdxKu1MklS*3;Hj`rfMO-AqfWy^|J; z+4Zhn%a66q7K_ojOaM*=d=d|UB{z^#X z(&r)ij%oJ#_WZZZg;yXz+nhn_r_KmM<`KD&_d6$p-Jeqw9hn{QSOo4aL)aNnn<@#aIw!1& z#-W|=@=E{VnzE7Tqo!nH_$sOO3Q6Wka*ZVKljJ%{E|X-IBp;AujwA~t;YqRx$y_F8 zF!{U;&FAw*G@oBnij@+{v-$k5DniMUB=UKB<|@J7R8~M7&gY9t0Wu6(0~f{nnh_?8 zD4eX;f$k^JUl82N&A)bb3qe9`lKU+P=3.8 diff --git a/ztools/ztools/__init__.py b/ztools/ztools/__init__.py new file mode 100644 index 0000000..d0c14a3 --- /dev/null +++ b/ztools/ztools/__init__.py @@ -0,0 +1,2 @@ +# ztools - Extended PartDesign for FreeCAD +__version__ = "0.1.0" diff --git a/ztools/ztools/__pycache__/__init__.cpython-312.pyc b/ztools/ztools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ece48324c375c975dada7d02813672841465036 GIT binary patch literal 216 zcmX@j%ge<81cCu2nT|mEF^B^Lj8MjB4j^MXLkdF_LnWgoQx&U$o}r$BpC;oi?)dn! z)S}|d{Ji-1l?VL)RXXNLm>Zj!wC8ZYY>y;%I=_eKx=;s%u>!lT?rY0w*=(z!D zXGa(Pl*E!meHh0#KSe((BtJg~s7b%71k8X^@$s2?nI-Y@dIgoYIBatBQ%ZAE?TXld Zwt<{j4D!JTW=2NF2YjN9+(j%vE&yeQIgkJV literal 0 HcmV?d00001 diff --git a/ztools/ztools/__pycache__/__init__.cpython-313.pyc b/ztools/ztools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae1cf3fdb22877d2d33a61e360387c9ac077e44f GIT binary patch literal 141 zcmey&%ge<81cCu2nT|mEF^B^Lj8MjB4j^MHLoh=TLpq}-Qx&U$o}r$BpC;oi?)dn! z)S}|d{Ji-1l?Td~EmE`B=6zj*wXXa&=#K-FuRNmsS$<0qG%}KQ@Vgo7xnOh99 T_5(8`BjW=;(MIkf79bY@&X^x6 literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__init__.py b/ztools/ztools/commands/__init__.py new file mode 100644 index 0000000..6bd2534 --- /dev/null +++ b/ztools/ztools/commands/__init__.py @@ -0,0 +1,4 @@ +# ztools/commands - GUI commands +from . import datum_commands, pattern_commands, pocket_commands + +__all__ = ["datum_commands", "pattern_commands", "pocket_commands"] diff --git a/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc b/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eccf2d48db1b1c6ab6752b68911eeaadc8d8857 GIT binary patch literal 319 zcmYLEy-ve05I)DP3PpuJ0&5217eG{~hz%wtFP4d&5@E-d<06%fr(kD5;#rtjJ;G42 zA$4Qof*QEt`_rB7?sYO51J+T9^)vEEXZp&vVsnV%5fmt5z!FYu#xqMfC9uHkiSWTH z%jqYtg>m-#S3pg0}|9SjlDNC;_+5HGMn z3*QyHaaOa+g=^ii;!Xx8y=N{giYjPbN>!XA&ri=79w1q7E}UZg$~l8W>^}aAE%hfd rA?l`%LKJ?Lc8;x6OQTQ19$Mc6#*v0S%9i&=v literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__pycache__/__init__.cpython-313.pyc b/ztools/ztools/commands/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2210b1b3e12bd34bc6ce25c6f49549363acc263b GIT binary patch literal 244 zcmey&%ge<81iD2fnahCmV-N=hn4pZ$3P8qGhG2$ZMsEf$#v(>9rXnUU<|1YV27RDd zAY&0rI+G^zOGcnFP3BvCDTyVex$(*Qxw(mXDaE%03KC07Qj78sBK!sU$=RtTZ~;F} z)>{lk%s}l$EFgjvNZewNk5A0WiH~2&@EOQuxFuaxlAoVbtPe9+KR!M)FS8^*Uaz3? s7Kcr4eoARhs$CH`P%X$I#R5R$12ZEd<8uc4iwyRUxSSi=i#UOT0Et~cJ^%m! literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__pycache__/datum_commands.cpython-312.pyc b/ztools/ztools/commands/__pycache__/datum_commands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..365ca6669d5305dd078f424a44915b034a90cdef GIT binary patch literal 30027 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 literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__pycache__/pocket_commands.cpython-312.pyc b/ztools/ztools/commands/__pycache__/pocket_commands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..881c2fb1615f7e1215114647f8bb6dd2bf22ed93 GIT binary patch literal 28305 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 TE50pa_lw!?{r4>|u{QocsD80{ literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__pycache__/pocket_commands.cpython-313.pyc b/ztools/ztools/commands/__pycache__/pocket_commands.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..217827b1505ee4897dc4d2ebc57afb79ffb6c8b1 GIT binary patch literal 28916 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 literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/__pycache__/theme_commands.cpython-312.pyc b/ztools/ztools/commands/__pycache__/theme_commands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63a888235b409e90f1ef45a5edde00fd1a37ae05 GIT binary patch literal 4704 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 literal 0 HcmV?d00001 diff --git a/ztools/ztools/commands/datum_commands.py b/ztools/ztools/commands/datum_commands.py new file mode 100644 index 0000000..9eecbd2 --- /dev/null +++ b/ztools/ztools/commands/datum_commands.py @@ -0,0 +1,650 @@ +# 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 DatumCreatorTaskPanel: + """Unified task panel for creating datum planes, axes, and points.""" + + PLANE_MODES = [ + ("Offset from Face", "offset_face"), + ("Midplane (2 Faces)", "midplane"), + ("3 Points", "3_points"), + ("Normal to Edge", "normal_edge"), + ("Angled from Face", "angled"), + ("Tangent to Cylinder", "tangent_cyl"), + ] + + AXIS_MODES = [ + ("2 Points", "axis_2pt"), + ("From Edge", "axis_edge"), + ("Cylinder Center", "axis_cyl"), + ("Plane Intersection", "axis_intersect"), + ] + + POINT_MODES = [ + ("At Vertex", "point_vertex"), + ("XYZ Coordinates", "point_xyz"), + ("On Edge", "point_edge"), + ("Face Center", "point_face"), + ("Circle Center", "point_circle"), + ] + + def __init__(self): + self.form = QtGui.QWidget() + self.form.setWindowTitle("ztools Datum Creator") + self.setup_ui() + self.selection_callback = None + self.selected_items = [] + self.setup_selection_observer() + + def setup_ui(self): + layout = QtGui.QVBoxLayout(self.form) + + # Datum type tabs + self.tabs = QtGui.QTabWidget() + layout.addWidget(self.tabs) + + # Plane tab + plane_widget = QtGui.QWidget() + plane_layout = QtGui.QVBoxLayout(plane_widget) + + self.plane_mode = QtGui.QComboBox() + for label, _ in self.PLANE_MODES: + self.plane_mode.addItem(label) + self.plane_mode.currentIndexChanged.connect(self.on_plane_mode_changed) + plane_layout.addWidget(QtGui.QLabel("Mode:")) + plane_layout.addWidget(self.plane_mode) + + # Plane parameters group + self.plane_params = QtGui.QGroupBox("Parameters") + self.plane_params_layout = QtGui.QFormLayout(self.plane_params) + + self.plane_offset_spin = QtGui.QDoubleSpinBox() + self.plane_offset_spin.setRange(-10000, 10000) + self.plane_offset_spin.setValue(10) + self.plane_offset_spin.setSuffix(" mm") + + self.plane_angle_spin = QtGui.QDoubleSpinBox() + self.plane_angle_spin.setRange(-360, 360) + self.plane_angle_spin.setValue(45) + self.plane_angle_spin.setSuffix(" °") + + self.plane_param_spin = QtGui.QDoubleSpinBox() + self.plane_param_spin.setRange(0, 1) + self.plane_param_spin.setValue(0.5) + self.plane_param_spin.setSingleStep(0.1) + + plane_layout.addWidget(self.plane_params) + + # Plane selection display + self.plane_selection_label = QtGui.QLabel("Selection: None") + self.plane_selection_label.setWordWrap(True) + plane_layout.addWidget(self.plane_selection_label) + + self.tabs.addTab(plane_widget, "Planes") + + # Axis tab + axis_widget = QtGui.QWidget() + axis_layout = QtGui.QVBoxLayout(axis_widget) + + self.axis_mode = QtGui.QComboBox() + for label, _ in self.AXIS_MODES: + self.axis_mode.addItem(label) + self.axis_mode.currentIndexChanged.connect(self.on_axis_mode_changed) + axis_layout.addWidget(QtGui.QLabel("Mode:")) + axis_layout.addWidget(self.axis_mode) + + self.axis_selection_label = QtGui.QLabel("Selection: None") + self.axis_selection_label.setWordWrap(True) + axis_layout.addWidget(self.axis_selection_label) + + axis_layout.addStretch() + self.tabs.addTab(axis_widget, "Axes") + + # Point tab + point_widget = QtGui.QWidget() + point_layout = QtGui.QVBoxLayout(point_widget) + + self.point_mode = QtGui.QComboBox() + for label, _ in self.POINT_MODES: + self.point_mode.addItem(label) + self.point_mode.currentIndexChanged.connect(self.on_point_mode_changed) + point_layout.addWidget(QtGui.QLabel("Mode:")) + point_layout.addWidget(self.point_mode) + + # Point XYZ inputs + self.point_xyz_group = QtGui.QGroupBox("Coordinates") + xyz_layout = QtGui.QFormLayout(self.point_xyz_group) + + self.point_x_spin = QtGui.QDoubleSpinBox() + self.point_x_spin.setRange(-10000, 10000) + self.point_x_spin.setSuffix(" mm") + + self.point_y_spin = QtGui.QDoubleSpinBox() + self.point_y_spin.setRange(-10000, 10000) + self.point_y_spin.setSuffix(" mm") + + self.point_z_spin = QtGui.QDoubleSpinBox() + self.point_z_spin.setRange(-10000, 10000) + self.point_z_spin.setSuffix(" mm") + + xyz_layout.addRow("X:", self.point_x_spin) + xyz_layout.addRow("Y:", self.point_y_spin) + xyz_layout.addRow("Z:", self.point_z_spin) + + self.point_xyz_group.setVisible(False) + point_layout.addWidget(self.point_xyz_group) + + # Point parameter (for edge) + self.point_param_spin = QtGui.QDoubleSpinBox() + self.point_param_spin.setRange(0, 1) + self.point_param_spin.setValue(0.5) + self.point_param_spin.setSingleStep(0.1) + + self.point_selection_label = QtGui.QLabel("Selection: None") + self.point_selection_label.setWordWrap(True) + point_layout.addWidget(self.point_selection_label) + + point_layout.addStretch() + self.tabs.addTab(point_widget, "Points") + + # Common options + 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) + + # Create button + self.create_btn = QtGui.QPushButton("Create Datum") + self.create_btn.clicked.connect(self.on_create) + layout.addWidget(self.create_btn) + + # Initialize UI state + self.on_plane_mode_changed(0) + self.tabs.currentChanged.connect(self.on_tab_changed) + + 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): + """Update UI when selection changes.""" + sel = Gui.Selection.getSelectionEx() + self.selected_items = sel + + # Build selection description + desc = [] + for s in sel: + if s.SubElementNames: + for sub in s.SubElementNames: + desc.append(f"{s.ObjectName}.{sub}") + else: + desc.append(s.ObjectName) + + text = ", ".join(desc) if desc else "None" + + # Update appropriate label + tab = self.tabs.currentIndex() + if tab == 0: + self.plane_selection_label.setText(f"Selection: {text}") + elif tab == 1: + self.axis_selection_label.setText(f"Selection: {text}") + elif tab == 2: + self.point_selection_label.setText(f"Selection: {text}") + + def on_tab_changed(self, index): + self.on_selection_changed() + + def on_plane_mode_changed(self, index): + """Update plane parameter UI based on mode.""" + # Clear existing params + while self.plane_params_layout.rowCount() > 0: + self.plane_params_layout.removeRow(0) + + mode = self.PLANE_MODES[index][1] + + if mode == "offset_face": + self.plane_params_layout.addRow("Offset:", self.plane_offset_spin) + elif mode == "angled": + self.plane_params_layout.addRow("Angle:", self.plane_angle_spin) + elif mode == "normal_edge": + self.plane_params_layout.addRow("Position (0-1):", self.plane_param_spin) + elif mode == "tangent_cyl": + self.plane_params_layout.addRow("Angle:", self.plane_angle_spin) + + def on_axis_mode_changed(self, index): + pass # Axes don't have extra parameters currently + + def on_point_mode_changed(self, index): + mode = self.POINT_MODES[index][1] + self.point_xyz_group.setVisible(mode == "point_xyz") + + def get_body(self): + """Get active body if checkbox is checked.""" + if not self.use_body_cb.isChecked(): + return None + + # Try to get active 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 a body in document + 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_selected_geometry(self, geo_type): + """Extract geometry of specified type from selection. + + Returns: + List of tuples: (shape, source_object, subname) + """ + results = [] + for sel in self.selected_items: + obj = sel.Object + if not hasattr(obj, "Shape"): + continue + + if sel.SubElementNames: + for sub in sel.SubElementNames: + # Only process valid sub-element names (Face#, Edge#, Vertex#) + # Skip invalid names like "Plane" from datum objects + if not ( + sub.startswith("Face") + or sub.startswith("Edge") + or sub.startswith("Vertex") + ): + # Try to use the whole object's shape instead + shape = obj.Shape + if geo_type == "face" and shape.Faces: + # Use the first face of the object (e.g., datum plane) + results.append((shape.Faces[0], obj, "Face1")) + elif geo_type == "edge" and shape.Edges: + results.append((shape.Edges[0], obj, "Edge1")) + elif geo_type == "vertex" and shape.Vertexes: + results.append((shape.Vertexes[0], obj, "Vertex1")) + continue + + try: + shape = obj.Shape.getElement(sub) + if geo_type == "face" and isinstance(shape, Part.Face): + results.append((shape, obj, sub)) + elif geo_type == "edge" and isinstance(shape, Part.Edge): + results.append((shape, obj, sub)) + elif geo_type == "vertex" and isinstance(shape, Part.Vertex): + results.append((shape, obj, sub)) + except Exception: + # If getElement fails, try to use the whole shape + shape = obj.Shape + if geo_type == "face" and shape.Faces: + results.append((shape.Faces[0], obj, "Face1")) + elif geo_type == "edge" and shape.Edges: + results.append((shape.Edges[0], obj, "Edge1")) + elif geo_type == "vertex" and shape.Vertexes: + results.append((shape.Vertexes[0], obj, "Vertex1")) + else: + # No sub-element selected, use the whole object's shape + shape = obj.Shape + if geo_type == "face" and shape.Faces: + results.append((shape.Faces[0], obj, "Face1")) + elif geo_type == "edge" and shape.Edges: + results.append((shape.Edges[0], obj, "Edge1")) + elif geo_type == "vertex" and shape.Vertexes: + results.append((shape.Vertexes[0], obj, "Vertex1")) + return results + + def on_create(self): + """Create the datum based on current settings.""" + from ztools.datums import core + + tab = self.tabs.currentIndex() + body = self.get_body() + name = self.get_name() + link_ss = self.link_spreadsheet_cb.isChecked() + + try: + if tab == 0: # Planes + self.create_plane(core, body, name, link_ss) + elif tab == 1: # Axes + self.create_axis(core, body, name) + elif tab == 2: # Points + self.create_point(core, body, name, link_ss) + + App.Console.PrintMessage("Datum created successfully\n") + + except Exception as e: + App.Console.PrintError(f"Failed to create datum: {e}\n") + QtGui.QMessageBox.warning(self.form, "Error", str(e)) + + def create_plane(self, core, body, name, link_ss): + mode = self.PLANE_MODES[self.plane_mode.currentIndex()][1] + + if mode == "offset_face": + faces = self.get_selected_geometry("face") + if not faces: + raise ValueError("Select a face") + face, src_obj, src_sub = faces[0] + core.plane_offset_from_face( + face, + self.plane_offset_spin.value(), + name=name, + body=body, + link_spreadsheet=link_ss, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "midplane": + faces = self.get_selected_geometry("face") + if len(faces) < 2: + raise ValueError("Select 2 faces") + face1, src_obj1, src_sub1 = faces[0] + face2, src_obj2, src_sub2 = faces[1] + core.plane_midplane( + face1, + face2, + name=name, + body=body, + source_object1=src_obj1, + source_subname1=src_sub1, + source_object2=src_obj2, + source_subname2=src_sub2, + ) + + elif mode == "3_points": + verts = self.get_selected_geometry("vertex") + if len(verts) < 3: + raise ValueError("Select 3 vertices") + v1, src_obj1, src_sub1 = verts[0] + v2, src_obj2, src_sub2 = verts[1] + v3, src_obj3, src_sub3 = verts[2] + core.plane_from_3_points( + v1.Point, + v2.Point, + v3.Point, + name=name, + body=body, + source_refs=[ + (src_obj1, src_sub1), + (src_obj2, src_sub2), + (src_obj3, src_sub3), + ], + ) + + elif mode == "normal_edge": + edges = self.get_selected_geometry("edge") + if not edges: + raise ValueError("Select an edge") + edge, src_obj, src_sub = edges[0] + core.plane_normal_to_edge( + edge, + parameter=self.plane_param_spin.value(), + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "angled": + faces = self.get_selected_geometry("face") + edges = self.get_selected_geometry("edge") + if not faces or not edges: + raise ValueError("Select a face and an edge") + face, face_obj, face_sub = faces[0] + edge, edge_obj, edge_sub = edges[0] + core.plane_angled( + face, + edge, + self.plane_angle_spin.value(), + name=name, + body=body, + link_spreadsheet=link_ss, + source_face_obj=face_obj, + source_face_sub=face_sub, + source_edge_obj=edge_obj, + source_edge_sub=edge_sub, + ) + + elif mode == "tangent_cyl": + faces = self.get_selected_geometry("face") + if not faces: + raise ValueError("Select a cylindrical face") + face, src_obj, src_sub = faces[0] + core.plane_tangent_to_cylinder( + face, + angle=self.plane_angle_spin.value(), + name=name, + body=body, + link_spreadsheet=link_ss, + source_object=src_obj, + source_subname=src_sub, + ) + + def create_axis(self, core, body, name): + mode = self.AXIS_MODES[self.axis_mode.currentIndex()][1] + + if mode == "axis_2pt": + verts = self.get_selected_geometry("vertex") + if len(verts) < 2: + raise ValueError("Select 2 vertices") + v1, obj1, sub1 = verts[0] + v2, obj2, sub2 = verts[1] + core.axis_from_2_points( + v1.Point, + v2.Point, + name=name, + body=body, + source_refs=[(obj1, sub1), (obj2, sub2)], + ) + + elif mode == "axis_edge": + edges = self.get_selected_geometry("edge") + if not edges: + raise ValueError("Select a linear edge") + edge, src_obj, src_sub = edges[0] + core.axis_from_edge( + edge, + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "axis_cyl": + faces = self.get_selected_geometry("face") + if not faces: + raise ValueError("Select a cylindrical face") + face, src_obj, src_sub = faces[0] + core.axis_cylinder_center( + face, + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "axis_intersect": + # Need 2 plane objects selected + if len(self.selected_items) < 2: + raise ValueError("Select 2 datum planes") + core.axis_intersection_planes( + self.selected_items[0].Object, + self.selected_items[1].Object, + name=name, + body=body, + ) + + def create_point(self, core, body, name, link_ss): + mode = self.POINT_MODES[self.point_mode.currentIndex()][1] + + if mode == "point_vertex": + verts = self.get_selected_geometry("vertex") + if not verts: + raise ValueError("Select a vertex") + vert, src_obj, src_sub = verts[0] + core.point_at_vertex( + vert, + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "point_xyz": + core.point_at_coordinates( + self.point_x_spin.value(), + self.point_y_spin.value(), + self.point_z_spin.value(), + name=name, + body=body, + link_spreadsheet=link_ss, + ) + + elif mode == "point_edge": + edges = self.get_selected_geometry("edge") + if not edges: + raise ValueError("Select an edge") + edge, src_obj, src_sub = edges[0] + core.point_on_edge( + edge, + parameter=self.point_param_spin.value(), + name=name, + source_object=src_obj, + source_subname=src_sub, + body=body, + link_spreadsheet=link_ss, + ) + + elif mode == "point_face": + faces = self.get_selected_geometry("face") + if not faces: + raise ValueError("Select a face") + face, src_obj, src_sub = faces[0] + core.point_center_of_face( + face, + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + elif mode == "point_circle": + edges = self.get_selected_geometry("edge") + if not edges: + raise ValueError("Select a circular edge") + edge, src_obj, src_sub = edges[0] + core.point_center_of_circle( + edge, + name=name, + body=body, + source_object=src_obj, + source_subname=src_sub, + ) + + 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 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("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()) diff --git a/ztools/ztools/commands/pattern_commands.py b/ztools/ztools/commands/pattern_commands.py new file mode 100644 index 0000000..e94a58e --- /dev/null +++ b/ztools/ztools/commands/pattern_commands.py @@ -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()) diff --git a/ztools/ztools/commands/pocket_commands.py b/ztools/ztools/commands/pocket_commands.py new file mode 100644 index 0000000..286ead0 --- /dev/null +++ b/ztools/ztools/commands/pocket_commands.py @@ -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( + "When enabled, material outside the sketch profile is removed,\n" + "leaving only the material inside the sketch boundary." + ) + 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()) diff --git a/ztools/ztools/datums/__init__.py b/ztools/ztools/datums/__init__.py new file mode 100644 index 0000000..fcbfac7 --- /dev/null +++ b/ztools/ztools/datums/__init__.py @@ -0,0 +1,39 @@ +# ztools/datums - Datum creation tools +from .core import ( + # Planes + plane_offset_from_face, + plane_midplane, + plane_from_3_points, + plane_normal_to_edge, + plane_angled, + plane_tangent_to_cylinder, + # Axes + axis_from_2_points, + axis_from_edge, + axis_cylinder_center, + axis_intersection_planes, + # Points + point_at_vertex, + point_at_coordinates, + point_on_edge, + point_center_of_face, + point_center_of_circle, +) + +__all__ = [ + "plane_offset_from_face", + "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", +] diff --git a/ztools/ztools/datums/__pycache__/__init__.cpython-312.pyc b/ztools/ztools/datums/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8461c8a0d4b5f26bd6f18a7bc0838c633fed0dcc GIT binary patch literal 707 zcmZ9KyN=W_6o#G4Oy-tkhFunT0E(d)OVA=D1fm0NLFL9Wu`@)8V+pUY2Ugx6UED#LK~;Er9UsRG zt{hk6!tzUHth&M-{jT1qv^KKogjIvfgNt=xN|2#DUPicsv;W9O+2JqnM~W9_j)xQuhrww!+<*C@$vN#AQwwASXG8tCp) z7%KXnYO~+fMyX23-77p_Jby{0u!0`NYb_~zt96S-^n)FP+XM41V4$2gU1K@l^#?b5 r3R+&Y>Y2HRL;Wu?JVg9RqA0paPonsS{DYrf{(7`t#ve0t7c>6=seRde literal 0 HcmV?d00001 diff --git a/ztools/ztools/datums/__pycache__/core.cpython-312.pyc b/ztools/ztools/datums/__pycache__/core.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f818041055b536b3d2213fa60753e2cb3568147a GIT binary patch literal 41705 zcmd6Q3ve9AdFC$m{a(BY@U;L)0tFMe2>FNIe?|*dfpL;w`1CD=rG1g!78wSHq=|Vm#rO1OBhrw{& zz!^B>py4c?iw2EDMQ4kKOlM6)=CkG@%UO$2yl*{gWBKf7?VRbXgENP117&i7>Y1~I zU7R)S?z3{XubR(#xDL*a6bEvzP;<$#J~QY1s^zSYa}~H>#JN$5pYwdxaJHE9A}ry2 zD9;}*#k0k@S5~kkCCFDAF6YX^u0A_gj=U8PgZz)%%TsA*?Uuug34{dFY z(%RaG=Brck6?&@@ZVldQx%gIG7q=E~zfJBt^~|j+kX~OPeW~x+fEE^o&GePnmnzgw zFBe2RSE{|w?c*BJ$Gg-v$}w*JV~mW?Ix4(e6WX;}Z5JCC`wNcRLT%pg7;UD}-2Bk* z-lBZ>jStP&s^lxw?-g7d-Y=kiKkh)mw|m^T_^iF}=i1S_8nJbsV)TCAqtwl9LVIfE z-r^1x9HUFE`Lp(ro7;>Q)oEH(h@SrHTC@c%S|hiJ&Y+`9t^HR;7roq8v~sQ7N_@#} zpf*Qj8l#?<-15)LPoS1Ysg^?Y_p{gXBx+fI#TY#DGa4Mnf@Z;dq(2%H z?8h&}`Xj@kLBVo*yj*alcy+!H> zGuRL};qO7*NH{QV7|`65qg{rpMZHDm4ZTG#HX00LMe(8mZ5dLw0gIepJ;#l2qFg$n z#@ANsKXcwXNdXbylV`+$!K`a@LKee;j-yD!zE+TIvP9>js=Fpmt%p@#ZdoX=-gm9 z&_B$DF9-S}d>}Lui)7Y0MUVZk~Wjq&{#1WWY7V1G=ohAvzP4|9U^ znb6=!cpuM4c)>Ijx*W9ec2vSs*9)e|xdFi*zKqd_h6pyaufI2py3n#P%I7Xe>2n3d zlFSlXptf75nalQWXc& zC5I+DGw#x>$0m;@+V8qoFWL;nRUf+zZr{YEY2&=BcKT$>wJvF1_pd)MH~3E&|K(%9 z!C(DzgV9m`V|Ur23Fl9uE79(&fxV69SucV=a&>t2wiNwwi(~Ij%OCGBA{6XHp$k1j z5e}a)($^Oa$9m0pMm{Lcrt}vPxo(I_6VQOk%^Bt4$=l+_xS>b6mz7JIPRLb+T*_pR zoA~mqoF=6_d)$PaR4+TOnCV~85_$zyIB7I=V{ngL2!sMBFdVzW(f;$p0ro8e7*ZIo z0UDu!3w-247=t|)3-!J<6dsN>JK576{Q2k(7NfH8#E!t}m%`ZvG^i!-*l}VoG#qXU z9O7F(caNOg zHyEad)d!mb&CSg(Dn-g4C96#ON&%W?!{-AH;pX$rC}aO%D7GIHupaT_?A^~iNAc5q zXgG>{;d-^5;`iMVkO@6nMISuS$47<&G4z1iQ-KTgimW+;ty_FQ!FDus0h1{#_z^h~ zioJwB!c2{YdEz3rmqJk#!1HVH(fG9p1i#uL$5~ZDGha_o zHqW=}6Lrbet@9;qnbm72On2=o7EOlI_VJ4kP>MltFd9Z5B8#6XgZ~T2RCw@nG_3Up zzdEFCoH=Xo(}d9sa18FZA9HXM*`+22BEy4M0u93v&F2d?vrqSQG>q@s9}Qf3DLfpw z6b|%;hU;Sijy@S^KR$T=90`EGhxzbuZ&;cw`)R=EOtWa9A!|;E6QYT#7!HTIKr9jn zV+MwKahkO@%QH4_N(LIF(G^%9kcJoLLWF1SZ)*XN>ZVLTlfredRo%MA^40dSeYeoS4O zB1(B;4~yg6rD+Nq{+JJZJq8WC%~AvhI!ydv!6G&Z_$FkC5;>FyeMQ!wS8C!(ThvOK zDZrSo(2n^!8sZ;YwMo^Kv}iSyuNv?CcSrg4cuIdL4uzy|jVWjf$jzOT4VOSx)E78_)-c0gz!h+uxd3v~V!>sYTc+PN{2p+n z5h%%QxKgNv3T6^(qEN1t&u(FfnnR8iXrTp2QV-i@x6rc07V79gl@@Nv>ca}TUFx|& z3$1bM5?W}@ZDG(B+9A`V{g_;A{6|O5Z3v4@D?l(!WkeX=t;Biuap%Kr^+6G52BI&8 zE?`E281xSlxe0-^fMhXFrOS@R96btGmQb9`*n1LTvtaycMvMZ696UAyK z#@NJ&S2MLr{SG{B%+pPjyT4?%?HX4`3#E0b(z@{@M4U>d*4`|8z3N8QyshqDWng;6LQQ+BrhWG0k1DrL9+>Euh~b&5 zuTH+2*nHPllc`#n2qn6wMnCY^ELsf9wk-w>ZvV%X24~sSzMGp~-+p8JykqUBDW{Ce zamnTEys>lMQGc(hc6!%B-L_QSwz--gRqdKQg!Wv__{y)2O^zj=zU!;YRIQ#iCZbdE z5Bzm#PxbCaJIcu?tV9-xsXurLL16*-#X1svW0aE}@gfyVL$vj4$ZHXVUWqJ@nMkB1 z!8dN|(~!ELX>@@NH)W2BvkUVhgqe0}IEs09zW*W+0Q+#dvks#Dj(|MXaDgEw!9N^X5>v8X(8*c3z!0NIhg0<(ze5(xh)xK|oWL7F%XG;^BR{0ZhyAf6`1?V@AHf+n6w zw>Vp9;DNs~IdTuB%8-%`(HtOI+(R>59I`#Esr*4aGl;+FZUp0o#R`KTL+`a$CrtNz z6;si~fqCDWjJxc^lHg3;%t*Rq^Mp-sm1IgQ61J&bA3zZE82rl@{RVdh8v%k-1gWtr zm}6GVvCvSoSNXQ6f@D~_e(-S~yNyZT)?oNu$Wy44$%XLDL6w8+h!lXTaEBxc-oA3H1#CuyP7hx(GsHqX1ZUr0i%=)Ic+HV6fywm-5_uMP*Mc<94)^;Y{;LIPmYwvj~6ULkEukXIG zd)^bgziRdCeK-1Ef9b|cGbi7A=Jqphowc)eyNfZNHc-Zk{#2<9yqh+<7d$@%UZy35gT1 z57NWbmzIm9`4lYpSfOaImX<3h;S2Zz3?)c&5nn|)fC!}2F$fsZ67T~pPm%I*8sso$ z;Y?68TXgw`Ic_BZNiLJGj$1j?C@AidpD?~wjj|PrpfuC~;fs}xxi4`uqoF`fi z8fR5G63efnjOXP1+LDcW90dr<7B{muRNG+gHU#aX>t$^hGzf$J!!HMz5F)Cr2**TW z&Gt7P99BcNV^-x_x;%A&-hEPd#-tstJ-Fdru};- zESQWg_jqT<>059vPdS$-Ler+iNaE1Ea~)>*#4D5b2@{lOE`RpzyUulZ(&GHup)Vhr zGEUV^l~431%ID3iGiFCt+P=ir>AJ+qd2>D8IrimaQzxgQQ{k!YiSWF64V7VEFqfyy zOGM`p`sZOPhgAQSgp{_!Jhmhm0b8FmhYlVl~Hm>(#3gu5d6Ss!RR0KFR-;d{d* zPJ$E*Ug=VTpuO7R>_j;<#U3~Tr=S%tm>vZ1s6c%RXi{5(oCLy?OYLgw%}}{vt0j} zL%+PZBW@f8DH}!JF=xDJyhLNYg0@q6-i2ulAFkgu{~jz?s={9nqG|5xL7gIyJCh77 z;tr(-H%tj%2Oyzl=Nupl3aB3Q#4Y^OaVy^)TP|mdBQ0+2D>`rF>~hIk7_H}J_UKls za9ER92js^Jj7L>t zjZEKQt`V&?asq+op`k#-g-Em?>hEx1cOWzvfj(?F!ViT8)e;DXXpB9*s0el*N@XuY zFGN&RlZ7scCxUaaSAq2)B8up!MAixf2E!M_nrd=s?hd(|G;|m(1g~Q%LHRY5He^v~ zQOO`FFmiN#)OmphX1-SJZ0g%)Q+7#Nk*loK>hki7Vol0Z1BxG=lgxwCS7+>m7Nf2``I{?c68DkYgmfreu8-?6Rfr6Bpf- zl7B`TlAjUb6Z~R*qKc0C3QCg^+~B+Eu2mczf~`l)Hd>i$n`b@Dkj1(Xd^iIaBpPj z!bmI}^f6o@zA-@p{vrjJD7Z|)6$(gaBovKY<7f<^pNgtekXhG~Lm z4pALi!7@pmsP;AgEj;jF>B;{E*bcF3qrv4}aIQ!>SB!VwcloaFn%uSE3Zz^C81C-1h-F}Y&Fu{z~g zJ#AhHw59^BX-C`m-i)XC>Wh;vCfX9A8=I%$4RC91+S4$;KjZQ(xK^ZGD-suH?5Wio zQ?8BUT~v*GJbur+GSNGYs@o?_nc~&crs>Yam835?VTK&&@?LG4Z2DI8`slUM>6+=2 zGsbjfhX+%MguAz$wCFh~kH(D=B|6oj$*wYzfLL%t(=2EaFEc^AM^w@rE_86R4b6E!J- zN^J)Kga~yVIZ)Syn)UKKrTosE{O*_nq~opu1vY7Fa?5Wep8-NWocU!S3SKJ?SmgT< z2XuR9%!{_^m`%rgac|s5Ae^%j93S&DC|)kN0dU?XL&C9Q&K@u39C=V&2c~I2oR0Pr zKwJyOT}lllfM3S}#T9@pw*&2ikf7u%1yqARfnNqVUJASt6(^u={*qtkQ}sxY^ni$RW@>0;gpE7Y5>3WAqCMdW;!S7JOfS_sQVyl3lO(+^}#a|o$`?x0*_2Qzp4gRYA9D|4af1pQx z6G4=OcpA&OhX@Y6A~=-0i2noRUV#}FeSjbr6uOH^6Sv^5O}T65-D_sb-&%QlJ&YG07W_s;H?WR=irnGZ2)6*5YNjc83LR*ceQV_Z|c-H2ENgP!fnopXTAc|2)w!{?R_F?e}WLH zZ^5xViv@UPo{SqPdA@PHJnJEdOBVG4B^QS&C7r$Cq}3F*>iV# zQ_Bwn^IELXuUhPR2(jn_#?u81vJ35kOCmRLvWxaAo>Lem3e@`q(BP5UM=LqplFnp| zIn}b59oh^@vw`xIBW`~agO3hq@*D%&I>lrdo?p&|BZ{E_aKxdfaMv&r1N49>DPF`t zh-nls4f;S6^no6!0t1W^@sXc0Xw2DqW?;^ae-0TOWa$IV z{Au`&DJ)wp5gf~pcjtwIMPAa6Q^WU_{-v`<>>%dw$pL*7eB+|AOhng{58N=0qB+OH}U35;? zhaVTf(l3BBUuY*45S9Z#EjGNNH!=t{RT!!{=GPo$r7AWMEWLar8WmYOp#vy}m>RQx6i!9-xS}#*ct9`>hKF@QfgT?4<}^bC3f`u& zMRgEBK+(lEQ6=;)C6h`>!}EzSW%GNH+n0*mp1H)yZ_dAm%r9dEM~4uAbK*T%U z6Xzx?iF3N7h{t#J%;cG=bKiJjeE)r$=WAnM9-E4NU|Y_ZZdcm7FKOR*-|Sp4m!-^Q z^X3YyD4A-TJfA3<9882#p4H>~AIWV$bb20Nvr~DqJ@IOK`TChxK3KjH9Cv!(%+6VU z&N%Z#a@Wzj?qeUiYZaA^^^Pgs*mc*vZ%HgR<1U%%zW)5R=f7P_7WkRHvqiHV>6$I+ z@~tWN*11wBhL$^-(gy5QRN9nee8q{9>9*O5J6lrr!%T6r=+|amjt%}#|N>S}R zx>h~Drg=(_!;%%uEcb>y3K)k~-T^IAzF(|fZh73ztYG#$E0|SJ?V#bVZg5v`h!(0a z?x(|DMaoMyA94xCauO!*vjskZ!ALLjjnb9*DZ66UqKlT^5&oho`771|Fh!{x$!5hW zYS`Gkk!o#i-uSB#`*Q{NT)CZ?GPUf}v138D@El{DOtJ8Ym-?*LE#5pGp=DM4I(+P0 z=IRx{4taP`(_q{oDhHzliQ9q=z7XdjjR(s$yw%H;Ft8Sh-Y$aYNGKXpFKx_2sUs#h zq=)mYG#U|{#-G@(jTBpgFz!)lv4-znrl(a~jD-yi{!b|NPbtXZy#z6{c(2HM%S6_j z-6cGnNezEMyth~6y^Yzt*HgUUUY&x&Sa2q`(9)4=>A2Ii(DiHz|686-)t*VY&y4TO zxcyg;Odd&;r`@Y@v6o^UFufv2NX8ZW1+c8{dlOm?cuLVIF?hhn2;+*Yh zqqQa&_56obryA(Yb#v%B)eLtbufl9K?Jy1y;LbT@R#fmA9nn^PhS-;$Fe@`52;7BQ zqEjmJ>a>q387!n^kZXmL2d2At=y^XwbXV;TzPN9oM$WCC;d&yK0s9H5E>wZm2_JX~ zoWOo!gZ;#wr@A1bm2+s?v?k{kE_3WBex-&Iz^7YKT`25ceHMU4RSA^BZZZhF$v`Y9 z=ZGVXSxk(aQwA1Uu1t+`PW8NmRw}MKn!Jzyz9xEOxmD`-kl!1&jS9I{fPBPV*pX%9 z(P=*{ABufME{j1v=Rm7mhI-}zV&z<8rGQbTJ&O8UztZLIFu!T17>xPq#dGrxdq59R4G7ap?U?-y-$Z^M@#pk z@`C0k!O_(Z2OL^}@iQWk3h?cqZ^0?RSG?3%m-sIscP_xSvaaH=vg^N7S$6hHFr^5- z2;YCc9~;y3h?JF72fRdUXw2 zzC@RQNx}c1AW1=r0@7UZ*ANIUHg=?*5j&0R=lKlXqZKVZG?jRP@@Hdf=2RXfazpbk z?+}5uTLjwTr{RBs%>M{R8KvdqP;PD00Bl_+r{=V`C24OV#aBT$rjm;58?J4bE}Cw= zWtrw@YHnf4-G=d};6H>lCY!GBxV9tl>bz?`e9j%7tIkR1f_=pY*uTMQfd3OA^|FPk z^{J}$X?N31?5+6i_*~b*uH&g)$I~q*QtlIf?JS)Peq;Tj%V6*5XASPF?$%zLWlsmLe6{G%IO!Rd+F(N9-5?cFjFxtN=;D zBj3w%+}AA=syw@%13t`ec0Dr|jAeE`v_IxjRcLxE7uD^s#@sOl$G}{tD9q$GXPJ9+ z6ldhiqdL~VDIsC`EO{$!M#(yAG!5oia>)ZVYRlx9j6udc!<#-;O@uHNr*OIL53fnT zp14PT8a`JZ%{Qo(+p3;bxq(!@ajzz~4g$>1O_B_}x18|ZaVd&of7~|)%mxWWkpSX0 zjq$Vu#?unXc&c4YLA)mCc*K{%7gy0}yjULVW2I2^-h!fceNJ(>g!h;#NtFS64nhhU z(74zmOAKshF_kg^hU2x%p{81izLV3o9r~G)TTqA$j20G;X>RF!qD6fo} zfR(L~bmR|7S4rYW?-hV?j_>aci4JL`v1Ul+A>v18&NEIK_^=1D-PZ8=kwIoBrqvaY zI#h`w(6AB9Al9D=0AGoySCvLSXFKxeA!LXy$cls@EqT)u5OVwR(+LZoEXxuWG=+*L zWReihfA_#oM$5kYPfsYa!D;k6^wxq;azm4@HNpa1(ZoEt0)t-~3O!;~VIDeh!q3>7 zGQ{eCf$7NkzrsEK9)e(vMi$sYsb8aj)FY(SnnW5$brpAxu!$a4kuorMGfh)`n0PdZ41tHePG*|f$-6It?_2}>t}DAoiWa|&VOKk7fN=f zN_M78c2C;wKQhmRjqC&ilLLv$w6})Y3B2Xsie0~a?eetgH^)+*HPe^AU-r(jx0lV; zeQ#x|eoxA?2kkDaPCPT+ccUj&)--+;GDTVS_z|sWk*uSaC)c^abm?^Wv?J+joG=%V zEsms}+Y8GUqJ;q_{1@krC7Yf~u0QgA|GfLmhie;hS7>}XvBGl)W{9Wmx{oYTtf)$K zzW&sWr@mu)%X!;5+Xlaa)^x+}^ol(x_ntc@9kHT1Q&Ks-W$vhK{Qt=P>E#I^HRW(1 zM#k<3UWAXdGFZq3fcPbr<68CX4T>wBO|AhLOTyD|rPF-|E__x!=Ku{?lw9nvVjpdX zd_nloCD#DQG}XDz0et9?@gb1hmw@CH*+qUL2gy0X#aG9C@?CWg56&UE!6~M2pjC1Z zaKH^tqhlWaQw{D=mXgZlWl0}8i(p|0cX4h-H>_&gT^bhwr=|u4r>Ql>^-cey*oQ#D zS88rx85PHpAiY%11$%~GJx1Mnj0NjadJttR zx?;qMEVDzSJ$FKHOwM?)3*_pJ_4MPK5$6OB^M?W$x(py?;V`C~755CKB>`lAewZ&F z{P2*CmVNeP$HmXr0gp8$7EwT)#=G`0{O=yi`;Jv9Vk=fH|2_2jXypmk4T`E%eqH`1 z?j92yXFGr+dfMP+=TxF1#`(k#i@dVd^9UUD_<1}dxY28@P?kjCiNUB(oRodX4oVg5 zZL-6ing1^3c#EENDEJh+GP4zw!72^l`6gwgIV37F0~BMRT%BhW$&5nz2HdGbd+_X{@k3lrGsb(RmL(cALIaN(%#KFJo)yAXZE6zwawy z8`HfovnlP{IAPALS@%ZQt*)8cx7OTVGh6hI`E7I3UNd2S%{Eo`ExhW@`bXzr?(FV~ zf$5T&wz)&elh3BA&*Xcw`^KMQJ8)#}W<#HmanRuDzH~4g+rxcp3mZdxeZD8ESEaQq?t;r=tp&PWt?W7Un zER4rvtsEc4?rDl%Pkja)UeR8Rky{?Er7PCB$ssLn&ULrT;?X*MNW))s_+DXGd(9>l zE=?J=YJ25a0S9>E(QD;A>KU36?OWOg$R{f2z=DT{oFrT#RmWxv9_>_xrK-;SVUu`1^S_H57g$)(yUnY^g9|3?E;7PguEbkj13jw z&`>0h*E`wLbd@4>gE*ym-(Jw9Mtlqow=sP6{IyJ3!{TbPIm& zj2Tm0QZo>f=Kmi8{0N4$hq}7Rj{gPSa4n9^Vhr357x zJX&8OZijH0Vpk|&+?TQ2?3WSwu*!l-c+j$7X@7RF%7dv-ct0xsJL12?h~)5JQL-Pn zHt_8=Zv<}z=Uq*V^-8uu4O`V)DRv)5KkR=n#Vu3>Qx(Cqqmi8Fm9&ObMMK&VL|Sp_ z)VXVw3;w#4zb@@x3vRDv-i5fcBp7$i9G%;fTz@#(_|*Gn??N#Cx5SlSp01tQJNH~_ z`GLn}$=tsMmaK0iBLUirv6XW$Vc4IOh)d=lufv>0#>o{a1{5a|Q&CW61{N&o2P9Ah zC9L97Ab}e@Q@N#`sjy;+9|6Nv*n-*yIDM(C$ODF34u%UeiO6K(ex{OpPh7KZ3T6PU zV5~3#wPX#$)M3e-1OF5;8|(OFDC5&Cg4eD(3#cW_G4zH{eoj84UBqas|u9*YFi^=O1tkA?wzv}bXmYgm&vRiQHi@V z#xDJk(b-(S?h`*qnC}t45xD!iC|VRu{}!=)?yeXM`2S5g8FT-8iv1k|!M>OFf8QIq z%m*k5szWX9qBqD7z}hyOX{q!$wGjAKJJfV~pWp>-%i{;342L)V?#bQbUH2Wn zsfI*vs-!mMs3inIm#406z@|v!do>VZsgn7RUCVBK;r_C<(_c(hZb?>boj5>C+`4A! zXSd&V?=Hap@taJ$vxV}DaK8^S_xoR(P@b_@WlF1N>~qX#KV?6V?~1R;FgEY|i zAchCWz&0MK*|uxta&)c*Y1{7l)p%JMMfFoP@BwJ}d;sju6*6K_6=rwP2xG%-(MC>8k8K@8D~1c` ziRl)t+om%%Q6YUXt=guwL2S1fcH{H%=w+-VUuP_err5bAj{&mUTbIiIP(mL6qlMl} zEJbVvIzhh~ZA)ziZM2iihLvXMl)^sdBlbSbW5R715u^1d*q6orknHDAly6Tz??CI& z<*&#%FYNpsq`f^Pj}XD1lb~VR{Oc%RC}BN^ALLWV6t--)GP^^obZ!g!H5%@EJjQ-H z0?W=LLqm~aekY|cMx7&o2k3^%qZQGaDU^dHD4vZ?>D;E#VPB^w6BOK_;CCn>`YTx3 zPh_BF?A*qa5k*`ISBT|CiLS_8Ry~XSb{XTh+PCjvZHn@1QE-TOZj;D!+cY|F*uqz> zoqlHK+4+^5C!V?b!q;Dz+cLiY-pVz3X<)M6GPaT6mb9mqwht`6y6fw^n25eN?d?q3 zJMUA{l~_^-MSJ@ch_a{bToXF+@Y) zotvdU&y>|pA5WHUn*HLPFD17;n=CmqVY_F?Hi*;bXRgfKw|{6~f%SSBcK}-hPM=D- z8^9akMd~y-+*du5p4WWP$WORF^wnT{#B(!iX3c3I?We<(*nB5CU0;*^_vp_@a!d<2 zqeKx=kaEB>OadoyUv|JctBO=MEYr40%d|=0d;tO{S)g+c?5M6jgWp2T5jY(h+b`)2 zmF3zH?5?Pus&TtUywnl3@hcm6Pw~*@htfS+Y?RNaWk1BnQu$f+oTZs2;Yvp@j#R}` zsXmERMSN3JAwJrB6zetO%3C0=X!$i7XzXGLVf6pe167g z40)A_KaTY9MbKg%Wf`=1$;zZD{n=zTu_1++mJYKZp-pP^l6TC_;}_y?xzDg4hH9

v2 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 E2d{MnZvX%Q literal 0 HcmV?d00001 diff --git a/ztools/ztools/datums/core.py b/ztools/ztools/datums/core.py new file mode 100644 index 0000000..dd42814 --- /dev/null +++ b/ztools/ztools/datums/core.py @@ -0,0 +1,1138 @@ +# ztools/datums/core.py +# Core datum creation functions + +import math +from typing import List, Optional, Tuple, Union + +import FreeCAD as App +import Part + +# Metadata property prefix +ZTOOLS_META_PREFIX = "ZTools_" + + +def _get_next_index(doc: App.Document, prefix: str) -> int: + """Get next available index for auto-naming.""" + existing = [obj.Name for obj in doc.Objects if obj.Name.startswith(prefix)] + if not existing: + return 1 + indices = [] + for name in existing: + try: + idx = int(name.replace(prefix, "").lstrip("_").split("_")[0]) + indices.append(idx) + except ValueError: + continue + return max(indices, default=0) + 1 + + +def _setup_datum_attachment(obj, support, map_mode: str, offset: App.Placement = None): + """ + Set up a PartDesign datum object with proper attachment. + + Args: + obj: The datum object (PartDesign::Plane, Line, or Point) + support: Attachment support - list of tuples [(object, 'SubElement'), ...] + map_mode: Attachment mode string (e.g., 'FlatFace', 'ObjectXY', 'Translate') + offset: Optional offset from the attachment point + """ + if hasattr(obj, "Support"): + obj.Support = support + + if hasattr(obj, "MapMode"): + obj.MapMode = map_mode + + if offset and hasattr(obj, "MapPathParameter"): + obj.AttachmentOffset = offset + + +def _setup_datum_placement(obj, placement: App.Placement): + """ + Set up a PartDesign datum object with placement only (no attachment). + Use this when we can't determine a proper attachment reference. + + For PartDesign datums (Plane, Line, Point), we need to either: + 1. Set up proper attachment (Support + MapMode), or + 2. Explicitly set MapMode to 'Deactivated' to indicate we're using placement only + + This function sets MapMode to 'Deactivated' and applies the placement. + """ + if hasattr(obj, "MapMode"): + obj.MapMode = "Deactivated" + + # Clear any attachment support + if hasattr(obj, "Support"): + obj.Support = None + + # Apply placement + obj.Placement = placement + + +def _get_subname_from_shape(parent_obj, shape): + """ + Find the sub-element name (e.g., 'Face1', 'Edge2') for a shape within a parent object. + + Args: + parent_obj: The FreeCAD object containing the shape + shape: The Part.Face, Part.Edge, or Part.Vertex to find + + Returns: + String like 'Face1', 'Edge2', 'Vertex3' or None if not found + """ + if not hasattr(parent_obj, "Shape"): + return None + + parent_shape = parent_obj.Shape + + # Check faces + if isinstance(shape, Part.Face): + for i, face in enumerate(parent_shape.Faces, 1): + if face.isSame(shape): + return f"Face{i}" + + # Check edges + elif isinstance(shape, Part.Edge): + for i, edge in enumerate(parent_shape.Edges, 1): + if edge.isSame(shape): + return f"Edge{i}" + + # Check vertices + elif isinstance(shape, Part.Vertex): + for i, vertex in enumerate(parent_shape.Vertexes, 1): + if vertex.isSame(shape): + return f"Vertex{i}" + + return None + + +def _find_shape_owner(doc, shape): + """ + Find the object that owns a given shape. + + Args: + doc: FreeCAD document + shape: The shape to find the owner of + + Returns: + Tuple of (object, subname) or (None, None) if not found + """ + for obj in doc.Objects: + if not hasattr(obj, "Shape"): + continue + + subname = _get_subname_from_shape(obj, shape) + if subname: + return obj, subname + + return None, None + + +def _add_ztools_metadata(obj, datum_type: str, params: dict): + """Store ztools metadata in object properties.""" + # Add custom properties for ztools tracking + if not hasattr(obj, f"{ZTOOLS_META_PREFIX}Type"): + obj.addProperty( + "App::PropertyString", + f"{ZTOOLS_META_PREFIX}Type", + "ztools", + "Datum creation method", + ) + if not hasattr(obj, f"{ZTOOLS_META_PREFIX}Params"): + obj.addProperty( + "App::PropertyString", + f"{ZTOOLS_META_PREFIX}Params", + "ztools", + "Creation parameters (JSON)", + ) + + setattr(obj, f"{ZTOOLS_META_PREFIX}Type", datum_type) + + import json + + # Convert vectors/placements to serializable format + serializable_params = {} + for k, v in params.items(): + if isinstance(v, App.Vector): + serializable_params[k] = {"x": v.x, "y": v.y, "z": v.z} + elif isinstance(v, App.Placement): + serializable_params[k] = { + "base": {"x": v.Base.x, "y": v.Base.y, "z": v.Base.z}, + "rotation": list(v.Rotation.Q), + } + else: + serializable_params[k] = v + + setattr(obj, f"{ZTOOLS_META_PREFIX}Params", json.dumps(serializable_params)) + + +def _link_to_spreadsheet( + doc: App.Document, obj, param_name: str, value: float, alias: str +): + """Optionally link a parameter to spreadsheet.""" + sheet = doc.getObject("Spreadsheet") + if not sheet: + sheet = doc.addObject("Spreadsheet::Sheet", "Spreadsheet") + + # Find next empty row + row = 1 + while sheet.getContents(f"A{row}"): + row += 1 + + sheet.set(f"A{row}", alias) + sheet.set(f"B{row}", f"{value} mm") + sheet.setAlias(f"B{row}", alias) + + # Set expression on object + obj.setExpression(param_name, f"Spreadsheet.{alias}") + + return alias + + +# ============================================================================= +# DATUM PLANES +# ============================================================================= + + +def plane_offset_from_face( + face: Part.Face, + distance: float, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + link_spreadsheet: bool = False, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum plane offset from a planar face. + + Args: + face: Source face (must be planar) + distance: Offset distance in mm (positive = along normal) + name: Optional custom name + body: Optional body to add plane to (None = document level) + link_spreadsheet: Create spreadsheet alias for distance + source_object: The object containing the face (for attachment) + source_subname: The sub-element name like 'Face1' (for attachment) + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + if not face.Surface.isPlanar(): + raise ValueError("Face must be planar for offset plane") + + # Try to find the source object if not provided + if source_object is None or source_subname is None: + source_object, source_subname = _find_shape_owner(doc, face) + + # Get face center and normal + uv = face.Surface.parameter(face.CenterOfMass) + normal = face.normalAt(uv[0], uv[1]) + base = face.CenterOfMass + normal * distance + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_Offset") + name = f"ZPlane_Offset_{idx:03d}" + + # Create plane + rot = App.Rotation(App.Vector(0, 0, 1), normal) + + if body: + plane = body.newObject("PartDesign::Plane", name) + + # Try to use proper attachment if we have the source reference + if source_object and source_subname: + # Use FlatFace mode with offset + _setup_datum_attachment( + plane, [(source_object, source_subname)], "FlatFace" + ) + # Set the offset along the normal + plane.AttachmentOffset = App.Placement( + App.Vector(0, 0, distance), App.Rotation() + ) + else: + # Fallback to placement-only mode + _setup_datum_placement(plane, App.Placement(base, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + # Center the plane visually (for Part::Plane) + plane.Placement = App.Placement(base - rot.multVec(App.Vector(25, 25, 0)), rot) + + # Store metadata + _add_ztools_metadata( + plane, + "offset_from_face", + {"distance": distance, "base": base, "normal": normal}, + ) + + # Spreadsheet link + if link_spreadsheet and not body: + # Part::Plane doesn't have Offset property, would need expression on Placement + pass + elif link_spreadsheet and body: + alias = f"{name}_offset" + _link_to_spreadsheet(doc, plane, "AttachmentOffset.Base.z", distance, alias) + + doc.recompute() + return plane + + +def plane_midplane( + face1: Part.Face, + face2: Part.Face, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object1: Optional[App.DocumentObject] = None, + source_subname1: Optional[str] = None, + source_object2: Optional[App.DocumentObject] = None, + source_subname2: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum plane midway between two parallel faces. + + Args: + face1: First face + face2: Second face (must be parallel to face1) + name: Optional custom name + body: Optional body to add plane to + source_object1, source_subname1: Reference for face1 + source_object2, source_subname2: Reference for face2 + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + if not (face1.Surface.isPlanar() and face2.Surface.isPlanar()): + raise ValueError("Both faces must be planar") + + # Get normals + uv1 = face1.Surface.parameter(face1.CenterOfMass) + uv2 = face2.Surface.parameter(face2.CenterOfMass) + n1 = face1.normalAt(uv1[0], uv1[1]) + n2 = face2.normalAt(uv2[0], uv2[1]) + + # Check parallel (dot product ~1 or ~-1) + dot = abs(n1.dot(n2)) + if dot < 0.9999: + raise ValueError("Faces must be parallel for midplane") + + # Midpoint + c1 = face1.CenterOfMass + c2 = face2.CenterOfMass + mid = (c1 + c2) * 0.5 + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_Mid") + name = f"ZPlane_Mid_{idx:03d}" + + # Create plane + rot = App.Rotation(App.Vector(0, 0, 1), n1) + + if body: + plane = body.newObject("PartDesign::Plane", name) + # No direct "midplane" attachment mode exists in FreeCAD + # Use placement-only mode with calculated midpoint + _setup_datum_placement(plane, App.Placement(mid, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + plane.Placement = App.Placement(mid - rot.multVec(App.Vector(25, 25, 0)), rot) + + _add_ztools_metadata( + plane, "midplane", {"center1": c1, "center2": c2, "midpoint": mid} + ) + + doc.recompute() + return plane + + +def plane_from_3_points( + p1: App.Vector, + p2: App.Vector, + p3: App.Vector, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_refs: Optional[List[Tuple]] = None, +) -> App.DocumentObject: + """ + Create datum plane from 3 points. + + Args: + p1, p2, p3: Three non-collinear points + name: Optional custom name + body: Optional body to add plane to + source_refs: List of (object, subname) tuples for the 3 vertices + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + # Calculate normal + v1 = p2 - p1 + v2 = p3 - p1 + normal = v1.cross(v2) + + if normal.Length < 1e-6: + raise ValueError("Points are collinear, cannot define plane") + + normal.normalize() + center = (p1 + p2 + p3) / 3 + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_3Pt") + name = f"ZPlane_3Pt_{idx:03d}" + + rot = App.Rotation(App.Vector(0, 0, 1), normal) + + if body: + plane = body.newObject("PartDesign::Plane", name) + + # Use ThreePointPlane attachment if we have references + if source_refs and len(source_refs) >= 3: + _setup_datum_attachment(plane, source_refs[:3], "ThreePointPlane") + else: + _setup_datum_placement(plane, App.Placement(center, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + plane.Placement = App.Placement( + center - rot.multVec(App.Vector(25, 25, 0)), rot + ) + + _add_ztools_metadata( + plane, + "3_points", + {"p1": p1, "p2": p2, "p3": p3, "center": center, "normal": normal}, + ) + + doc.recompute() + return plane + + +def plane_normal_to_edge( + edge: Part.Edge, + parameter: float = 0.5, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum plane normal to edge at parameter location. + + Args: + edge: Source edge/curve + parameter: Location along edge (0.0 to 1.0) + name: Optional custom name + body: Optional body to add plane to + source_object, source_subname: Reference for the edge + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + # Get point and tangent at parameter + param = edge.FirstParameter + parameter * (edge.LastParameter - edge.FirstParameter) + point = edge.valueAt(param) + tangent = edge.tangentAt(param) + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_Normal") + name = f"ZPlane_Normal_{idx:03d}" + + # Plane normal = edge tangent + rot = App.Rotation(App.Vector(0, 0, 1), tangent) + + if body: + plane = body.newObject("PartDesign::Plane", name) + + # Use NormalToPath attachment if we have a reference + if source_object and source_subname: + _setup_datum_attachment( + plane, [(source_object, source_subname)], "NormalToPath" + ) + # Set parameter along path + if hasattr(plane, "MapPathParameter"): + plane.MapPathParameter = parameter + else: + _setup_datum_placement(plane, App.Placement(point, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + plane.Placement = App.Placement(point - rot.multVec(App.Vector(25, 25, 0)), rot) + + _add_ztools_metadata( + plane, + "normal_to_edge", + {"parameter": parameter, "point": point, "tangent": tangent}, + ) + + doc.recompute() + return plane + + +def plane_angled( + face: Part.Face, + edge: Part.Edge, + angle: float, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + link_spreadsheet: bool = False, + source_face_obj: Optional[App.DocumentObject] = None, + source_face_sub: Optional[str] = None, + source_edge_obj: Optional[App.DocumentObject] = None, + source_edge_sub: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum plane at angle to face, rotating about edge. + + Args: + face: Reference face + edge: Rotation axis (should lie on face) + angle: Rotation angle in degrees + name: Optional custom name + body: Optional body to add plane to + link_spreadsheet: Create spreadsheet alias for angle + source_face_obj, source_face_sub: Reference for the face + source_edge_obj, source_edge_sub: Reference for the edge + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + if not face.Surface.isPlanar(): + raise ValueError("Face must be planar") + + # Get face normal and edge direction + uv = face.Surface.parameter(face.CenterOfMass) + face_normal = face.normalAt(uv[0], uv[1]) + + edge_dir = ( + edge.Curve.Direction + if hasattr(edge.Curve, "Direction") + else ( + edge.valueAt(edge.LastParameter) - edge.valueAt(edge.FirstParameter) + ).normalize() + ) + + edge_mid = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2) + + # Rotate face normal about edge + rot_axis = App.Rotation(edge_dir, angle) + new_normal = rot_axis.multVec(face_normal) + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_Angled") + name = f"ZPlane_Angled_{idx:03d}" + + rot = App.Rotation(App.Vector(0, 0, 1), new_normal) + + if body: + plane = body.newObject("PartDesign::Plane", name) + + # Use FlatFace on the reference face with rotation offset + if source_face_obj and source_face_sub: + _setup_datum_attachment( + plane, [(source_face_obj, source_face_sub)], "FlatFace" + ) + # Apply rotation as attachment offset + plane.AttachmentOffset = App.Placement( + App.Vector(0, 0, 0), App.Rotation(App.Vector(1, 0, 0), angle) + ) + else: + _setup_datum_placement(plane, App.Placement(edge_mid, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + plane.Placement = App.Placement( + edge_mid - rot.multVec(App.Vector(25, 25, 0)), rot + ) + + _add_ztools_metadata( + plane, + "angled", + { + "angle": angle, + "edge_mid": edge_mid, + "original_normal": face_normal, + "new_normal": new_normal, + }, + ) + + if link_spreadsheet and body: + alias = f"{name}_angle" + _link_to_spreadsheet( + doc, plane, "AttachmentOffset.Rotation.Angle", angle, alias + ) + + doc.recompute() + return plane + + +def plane_tangent_to_cylinder( + face: Part.Face, + angle: float = 0, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + link_spreadsheet: bool = False, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum plane tangent to cylindrical face at angle. + + Args: + face: Cylindrical face + angle: Angular position in degrees (0 = +X direction) + name: Optional custom name + body: Optional body to add plane to + link_spreadsheet: Create spreadsheet alias for angle + source_object, source_subname: Reference for the cylindrical face + + Returns: + Created datum plane object + """ + doc = App.ActiveDocument + + if not isinstance(face.Surface, Part.Cylinder): + raise ValueError("Face must be cylindrical") + + cyl = face.Surface + axis = cyl.Axis + center = cyl.Center + radius = cyl.Radius + + # Calculate tangent point at angle + rad = math.radians(angle) + # Local X direction perpendicular to axis + 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) + + # Point on cylinder surface + radial = local_x * math.cos(rad) + local_y * math.sin(rad) + tangent_point = center + radial * radius + + # Plane normal is radial direction (tangent plane) + plane_normal = radial + + # Auto-name + if name is None: + idx = _get_next_index(doc, "ZPlane_Tangent") + name = f"ZPlane_Tangent_{idx:03d}" + + rot = App.Rotation(App.Vector(0, 0, 1), plane_normal) + + if body: + plane = body.newObject("PartDesign::Plane", name) + + # Use Tangent attachment mode for cylindrical face + if source_object and source_subname: + _setup_datum_attachment(plane, [(source_object, source_subname)], "Tangent") + # Set rotation angle via attachment offset + plane.AttachmentOffset = App.Placement( + App.Vector(0, 0, 0), App.Rotation(App.Vector(0, 0, 1), angle) + ) + else: + _setup_datum_placement(plane, App.Placement(tangent_point, rot)) + else: + plane = doc.addObject("Part::Plane", name) + plane.Length = 50 + plane.Width = 50 + plane.Placement = App.Placement( + tangent_point - rot.multVec(App.Vector(25, 25, 0)), rot + ) + + _add_ztools_metadata( + plane, + "tangent_cylinder", + {"angle": angle, "radius": radius, "tangent_point": tangent_point}, + ) + + if link_spreadsheet and body: + alias = f"{name}_angle" + _link_to_spreadsheet( + doc, plane, "AttachmentOffset.Rotation.Angle", angle, alias + ) + + doc.recompute() + return plane + + +# ============================================================================= +# DATUM AXES +# ============================================================================= + + +def axis_from_2_points( + p1: App.Vector, + p2: App.Vector, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_refs: Optional[List[Tuple]] = None, +) -> App.DocumentObject: + """ + Create datum axis from two points. + + Args: + p1, p2: Two distinct points + name: Optional custom name + body: Optional body to add axis to + source_refs: List of (object, subname) tuples for the vertices + + Returns: + Created datum axis object + """ + doc = App.ActiveDocument + + direction = p2 - p1 + if direction.Length < 1e-6: + raise ValueError("Points must be distinct") + + length = direction.Length + direction.normalize() + midpoint = (p1 + p2) * 0.5 + + if name is None: + idx = _get_next_index(doc, "ZAxis_2Pt") + name = f"ZAxis_2Pt_{idx:03d}" + + if body: + axis = body.newObject("PartDesign::Line", name) + + # Use TwoPointLine attachment if we have references + if source_refs and len(source_refs) >= 2: + _setup_datum_attachment(axis, source_refs[:2], "TwoPointLine") + else: + rot = App.Rotation(App.Vector(0, 0, 1), direction) + _setup_datum_placement(axis, App.Placement(midpoint, rot)) + else: + axis = doc.addObject("Part::Line", name) + axis.X1, axis.Y1, axis.Z1 = p1.x, p1.y, p1.z + axis.X2, axis.Y2, axis.Z2 = p2.x, p2.y, p2.z + + _add_ztools_metadata( + axis, "2_points", {"p1": p1, "p2": p2, "direction": direction, "length": length} + ) + + doc.recompute() + return axis + + +def axis_from_edge( + edge: Part.Edge, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum axis from linear edge. + + Args: + edge: Linear edge + name: Optional custom name + body: Optional body to add axis to + source_object, source_subname: Reference for the edge + + Returns: + Created datum axis object + """ + doc = App.ActiveDocument + + if not isinstance(edge.Curve, Part.Line): + raise ValueError("Edge must be linear") + + if name is None: + idx = _get_next_index(doc, "ZAxis_Edge") + name = f"ZAxis_Edge_{idx:03d}" + + p1 = edge.valueAt(edge.FirstParameter) + p2 = edge.valueAt(edge.LastParameter) + direction = (p2 - p1).normalize() + midpoint = (p1 + p2) * 0.5 + + if body: + axis = body.newObject("PartDesign::Line", name) + + # Use ObjectXY attachment for edge + if source_object and source_subname: + _setup_datum_attachment(axis, [(source_object, source_subname)], "ObjectXY") + else: + rot = App.Rotation(App.Vector(0, 0, 1), direction) + _setup_datum_placement(axis, App.Placement(midpoint, rot)) + else: + axis = doc.addObject("Part::Line", name) + axis.X1, axis.Y1, axis.Z1 = p1.x, p1.y, p1.z + axis.X2, axis.Y2, axis.Z2 = p2.x, p2.y, p2.z + + _add_ztools_metadata( + axis, "from_edge", {"p1": p1, "p2": p2, "direction": direction} + ) + + doc.recompute() + return axis + + +def axis_cylinder_center( + face: Part.Face, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum axis at center of cylindrical face. + + Args: + face: Cylindrical face + name: Optional custom name + body: Optional body to add axis to + source_object: Object that owns the face (for attachment) + source_subname: Sub-element name like 'Face1' (for attachment) + + Returns: + Created datum axis object + """ + doc = App.ActiveDocument + + if not isinstance(face.Surface, Part.Cylinder): + raise ValueError("Face must be cylindrical") + + cyl = face.Surface + center = cyl.Center + axis_dir = cyl.Axis + + # Get cylinder extent from face bounds + bbox = face.BoundBox + # Project to axis + p1 = center + axis_dir * (-50) # Arbitrary length + p2 = center + axis_dir * 50 + + if name is None: + idx = _get_next_index(doc, "ZAxis_Cyl") + name = f"ZAxis_Cyl_{idx:03d}" + + if body: + axis = body.newObject("PartDesign::Line", name) + if source_object and source_subname: + # Use 'ObjectZ' to align axis with cylindrical face's axis + support = [(source_object, source_subname)] + _setup_datum_attachment(axis, support, "ObjectZ") + else: + rot = App.Rotation(App.Vector(0, 0, 1), axis_dir) + _setup_datum_placement(axis, App.Placement(center, rot)) + else: + axis = doc.addObject("Part::Line", name) + axis.X1, axis.Y1, axis.Z1 = p1.x, p1.y, p1.z + axis.X2, axis.Y2, axis.Z2 = p2.x, p2.y, p2.z + + _add_ztools_metadata( + axis, + "cylinder_center", + {"center": center, "direction": axis_dir, "radius": cyl.Radius}, + ) + + doc.recompute() + return axis + + +def axis_intersection_planes( + plane1, + plane2, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object1: Optional[App.DocumentObject] = None, + source_subname1: Optional[str] = None, + source_object2: Optional[App.DocumentObject] = None, + source_subname2: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum axis at intersection of two planes. + + Args: + plane1, plane2: Two non-parallel planes + name: Optional custom name + body: Optional body to add axis to + source_object1: Object that owns plane1 (for attachment) + source_subname1: Sub-element name for plane1 (for attachment) + source_object2: Object that owns plane2 (for attachment) + source_subname2: Sub-element name for plane2 (for attachment) + + Returns: + Created datum axis object + """ + doc = App.ActiveDocument + + # Get plane shapes + shape1 = plane1.Shape if hasattr(plane1, "Shape") else plane1 + shape2 = plane2.Shape if hasattr(plane2, "Shape") else plane2 + + # Find intersection + common = shape1.common(shape2) + if not common.Edges: + raise ValueError("Planes do not intersect or are parallel") + + edge = common.Edges[0] + p1 = edge.valueAt(edge.FirstParameter) + p2 = edge.valueAt(edge.LastParameter) + + if name is None: + idx = _get_next_index(doc, "ZAxis_Intersect") + name = f"ZAxis_Intersect_{idx:03d}" + + if ( + body + and source_object1 + and source_subname1 + and source_object2 + and source_subname2 + ): + # Create axis with TwoFace attachment (intersection of two planes) + axis = body.newObject("PartDesign::Line", name) + support = [(source_object1, source_subname1), (source_object2, source_subname2)] + _setup_datum_attachment(axis, support, "TwoFace") + + _add_ztools_metadata( + axis, + "plane_intersection", + {"point1": p1, "point2": p2}, + ) + doc.recompute() + return axis + else: + return axis_from_2_points(p1, p2, name, body) + + +# ============================================================================= +# DATUM POINTS +# ============================================================================= + + +def point_at_vertex( + vertex: Part.Vertex, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + source_object: Optional[App.DocumentObject] = None, + source_subname: Optional[str] = None, +) -> App.DocumentObject: + """ + Create datum point at vertex location. + + Args: + vertex: Source vertex + name: Optional custom name + body: Optional body to add point to + source_object: Object that owns the vertex (for attachment) + source_subname: Sub-element name like 'Vertex1' (for attachment) + + Returns: + Created datum point object + """ + doc = App.ActiveDocument + pos = vertex.Point + + if name is None: + idx = _get_next_index(doc, "ZPoint_Vtx") + name = f"ZPoint_Vtx_{idx:03d}" + + if body: + point = body.newObject("PartDesign::Point", name) + if source_object and source_subname: + # Use 'Vertex' attachment mode to attach to the vertex + support = [(source_object, source_subname)] + _setup_datum_attachment(point, support, "Vertex") + else: + _setup_datum_placement(point, App.Placement(pos, App.Rotation())) + else: + point = doc.addObject("Part::Vertex", name) + point.X, point.Y, point.Z = pos.x, pos.y, pos.z + + _add_ztools_metadata(point, "vertex", {"position": pos}) + + doc.recompute() + return point + + +def point_at_coordinates( + x: float, + y: float, + z: float, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + link_spreadsheet: bool = False, +) -> App.DocumentObject: + """ + Create datum point at XYZ coordinates. + + Args: + x, y, z: Coordinates in mm + name: Optional custom name + body: Optional body to add point to + link_spreadsheet: Create spreadsheet aliases for coordinates + + Returns: + Created datum point object + """ + doc = App.ActiveDocument + + if name is None: + idx = _get_next_index(doc, "ZPoint_XYZ") + name = f"ZPoint_XYZ_{idx:03d}" + + if body: + point = body.newObject("PartDesign::Point", name) + _setup_datum_placement( + point, App.Placement(App.Vector(x, y, z), App.Rotation()) + ) + else: + point = doc.addObject("Part::Vertex", name) + point.X, point.Y, point.Z = x, y, z + + _add_ztools_metadata(point, "coordinates", {"x": x, "y": y, "z": z}) + + if link_spreadsheet and not body: + _link_to_spreadsheet(doc, point, "X", x, f"{name}_X") + _link_to_spreadsheet(doc, point, "Y", y, f"{name}_Y") + _link_to_spreadsheet(doc, point, "Z", z, f"{name}_Z") + + doc.recompute() + return point + + +def point_on_edge( + edge: Part.Edge, + parameter: float = 0.5, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, + link_spreadsheet: bool = False, +) -> App.DocumentObject: + """ + Create datum point on edge at parameter. + + Args: + edge: Source edge + parameter: Location along edge (0.0 to 1.0) + name: Optional custom name + body: Optional body to add point to + link_spreadsheet: Create spreadsheet alias for parameter + + Returns: + Created datum point object + """ + doc = App.ActiveDocument + + param = edge.FirstParameter + parameter * (edge.LastParameter - edge.FirstParameter) + pos = edge.valueAt(param) + + if name is None: + idx = _get_next_index(doc, "ZPoint_Edge") + name = f"ZPoint_Edge_{idx:03d}" + + if body: + point = body.newObject("PartDesign::Point", name) + _setup_datum_placement(point, App.Placement(pos, App.Rotation())) + else: + point = doc.addObject("Part::Vertex", name) + point.X, point.Y, point.Z = pos.x, pos.y, pos.z + + _add_ztools_metadata(point, "on_edge", {"parameter": parameter, "position": pos}) + + doc.recompute() + return point + + +def point_center_of_face( + face: Part.Face, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, +) -> App.DocumentObject: + """ + Create datum point at center of mass of face. + + Args: + face: Source face + name: Optional custom name + body: Optional body to add point to + + Returns: + Created datum point object + """ + doc = App.ActiveDocument + pos = face.CenterOfMass + + if name is None: + idx = _get_next_index(doc, "ZPoint_FaceCenter") + name = f"ZPoint_FaceCenter_{idx:03d}" + + if body: + point = body.newObject("PartDesign::Point", name) + _setup_datum_placement(point, App.Placement(pos, App.Rotation())) + else: + point = doc.addObject("Part::Vertex", name) + point.X, point.Y, point.Z = pos.x, pos.y, pos.z + + _add_ztools_metadata(point, "face_center", {"position": pos}) + + doc.recompute() + return point + + +def point_center_of_circle( + edge: Part.Edge, + name: Optional[str] = None, + body: Optional[App.DocumentObject] = None, +) -> App.DocumentObject: + """ + Create datum point at center of circular edge. + + Args: + edge: Circular edge (circle or arc) + name: Optional custom name + body: Optional body to add point to + + Returns: + Created datum point object + """ + doc = App.ActiveDocument + + if not isinstance(edge.Curve, (Part.Circle, Part.ArcOfCircle)): + raise ValueError("Edge must be circular") + + pos = edge.Curve.Center + + if name is None: + idx = _get_next_index(doc, "ZPoint_CircleCenter") + name = f"ZPoint_CircleCenter_{idx:03d}" + + if body: + point = body.newObject("PartDesign::Point", name) + _setup_datum_placement(point, App.Placement(pos, App.Rotation())) + else: + point = doc.addObject("Part::Vertex", name) + point.X, point.Y, point.Z = pos.x, pos.y, pos.z + + _add_ztools_metadata( + point, "circle_center", {"position": pos, "radius": edge.Curve.Radius} + ) + + doc.recompute() + return point diff --git a/ztools/ztools/resources/__init__.py b/ztools/ztools/resources/__init__.py new file mode 100644 index 0000000..eaa5d09 --- /dev/null +++ b/ztools/ztools/resources/__init__.py @@ -0,0 +1,10 @@ +# ztools/resources - Icons and assets +from .icons import MOCHA, get_icon, save_icons_to_disk +from .theme import get_stylesheet + +__all__ = [ + "get_icon", + "save_icons_to_disk", + "MOCHA", + "get_stylesheet", +] diff --git a/ztools/ztools/resources/__pycache__/__init__.cpython-312.pyc b/ztools/ztools/resources/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9811f03a3cba06b79c75a17b709a2a12c28f97ef GIT binary patch literal 380 zcmX|6Jx{|h5Ve!Isi?|;#1CMMgyO9eLaK@oOJQNkVww7yM%Z;_yOqkuuV826w=l7~ zVq!qu5Z#!tOUn)Kz594~y4P;^0EJjLu2}{Ch{-;Zo!FcP@q`2tSR#chtdc5GX_eww zOJt@|3{# zU?%{s$V?XehD1msv;tfgxhHXn{j2?tc;Y6YU_0b<#<-M>y`W9-pr4vfv;sQT<_;_^ z7u--jn^A4*a%=!bT$GoA2mK2wxZ`x647H%ut=2M_=!4se9T~8CZbo4pW6iX2jFq#c nZ=WeG<`T|LH{6Z?+v5OVDaQB{_1;kL9i4np4sYJPd2g7uDE1)MMtl7h`$ro7itk8!9LX~@(6ABIj9ATy*MdYpX?&E` zl4Kt$Xv8g}&MYhChQ&xJg1ox9xI7QtnewP7b{P)b_(5%{LvL}a-CfDTe%d>~HpOJo25 literal 0 HcmV?d00001 diff --git a/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc b/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5120d08ac8d6f4448c8390a8947d611d3badca50 GIT binary patch literal 17584 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 a7aq@=zoAEB!d>e=_~O8CU3jnz^M3*Jo*!fY literal 0 HcmV?d00001 diff --git a/ztools/ztools/resources/__pycache__/icons.cpython-313.pyc b/ztools/ztools/resources/__pycache__/icons.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27bd1d5641f5d90b74f10adb4429f1ea46dd8f2a GIT binary patch literal 17519 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 literal 0 HcmV?d00001 diff --git a/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc b/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5f2b30bc0e1b5ad5216078cf979d15e7d3cea24 GIT binary patch literal 38238 zcmchA>r-4wmR}X(Ef5GK&;!8*AzJEgM5|xZUA;Xc0YVZ&2mt~;5OASxQ6-A1>UtnR zYIToyc2;ArcQt;E*6X#$-d)@48Nc85+WQBrH^OU&BRU9&_rrei<4c7*9UETzgU`ud z-dmNG^`LvYtO!Z>v!0-1t{$R~Xf*;h-(;@kEh@NWYQ!PE!$)`Gcs+Ujo^wc1q8tAD}J~h%) zlYDBTr)K%oOizd9(_wmQkxwo3)GD7^>FJ1kIzmrv@~MrU+T~L_Jsp)#N9n0UK6TJj zr+n(9r(^Qz7(E@APsi!0OFnhcQ@4ETrl%9~=>$EUlusw=>6CmrMNg;Y)9GN3(-W!* zp7~zwgEQ*6m!5lf`z~1jjV$K7;NP#ib$jgkmHpZ^JLT-xZ`rYQ)Y-3FPi9i-{l-)# z8M4=%x4frI-qYLrjqz^Da3Z6_9Vl2mCl z<4{gJkqAeU&iq+lg=b#>#n=-|F!h@5dHn~|8`23@jqrm z6#Uun1#5ojPrL5r`!(MW$%!b1x#b_3QfuU?qm+6rPrZ?+fl?Z^JdKnhot#r;$xW2f zqUC9(lp|WoVM@{EX`z&MEl;bF=Ln_fwVPVBQ662Mb|d8|rRa5a7$t(yMJc*o-A0}hM#@P_(KR(CJY{g5Hd2^NZ?7Io(PchEDSBO|re^KEMqPcB zqWhR-r1VpYE)UnG`wy3+=NX_BQ$k$}=F;<=HOd*J6x}mTP0vvty}islLq=V%87b$D zx=b5hFu1s`W12^qT(27?n-acZDMfF=_YAK0jXb6nrp3NRT)Ibb^wYiW10&Cel%i|Ob?NpA7xu{T61r?iv}gDY&LL?}hK z&p{&6W22l+O3_D8Gtx#Wk3N2F8C)?VS zb4t;B%6BP6?}Mg9HsnmCVKzVfC z4(3IG-WI=Xlssjm{0gP$y1%59udeA2jgtSIQO=(?QvL#^ z=(X2TimnBZx%zDIFB)8biBfb=_>qz4Ano;vxOClrl~VNn^_Pv3f6d@Bef6&xd4AnU zF}eOK<di=R*)-D2#kdJ6lh?uS366y2k^WZk1oz5Xuc(S6>O=kHM- zz4pI4X8-yJ2Po$sQXahp{}HA1X!D-m@j3 z$F5FO+~Mj}Z6yu&F6^7j8VIDEgiV2fI(A8}hp^p~NH84^SnnowK!=^kMmVh!xY>#p z8wm5kNa@0dlt{q{+op8BH@tjvnYBuKo`8gF@gyC#1guMm9V-=&MuOJfdqs-*wpG#N zzL0EH@e(JNu}1A=wY-9gr_xK?wJ+aP!)x($I=-b@?5D_(pwLLcJ7B%d?Jt`fMF#?r zw6j&Rf^?EPH@TP-OBWPGj%0n{Iun{|IB5qXnN+?{_O9Cbnzw83*QFQ%x{1A#D&m{F&+cmE!0QHJ+?(rsEOz%9CTSp9W64%F#tw5pFOfwbNHkxQeUDRI+sdHf zHL;Ya(xl}Qf>BO|K*b@(WLXb8>ziJjGu>iO#m2l^ipNI#SEc{xDm+ZY6Pd)Vc+mNH z;L^Y&WnP9m34;T*x*vTYKe6`MW=`Gi< z-R^=E5}+DRJ(Ed=U8k;+6LS^lHYJ^SPtAJ@B9S*RctMZ{*W3s~S9*qBE4?l4#?6$g zuab)%%MZie%1L=)UNHxC+&q?wj{7#cM;=Ro;}&stQ?ekIuC8H~3sMwx=kk9jtEq

{^5N9lssFh}M5an*xk@#K~r)o}Ki zTup(1%v-ubY6LA-dQPgGx5)AUji6NmF%dxC)awsL<950rwitSsi0Sz{d!5D|6iW(# zeo{t^N*(F{XdTI&N4s&a8s0Z=uT|Oks0R36Ads+QWu{r*Ev-B|N;;khSi`vi)Ltt$ zHPqTw4mvVFc_k_QRC->Kz@XMKUW>~xDVv=oP}6EPo_Wd2Ur9bWf%m2Zg$~UbB(I*M zK)3^}V@_*^LO=1MZa_^Ja_YkA0{xX2wC7FUf?(>dkRIzf^LwGPt}+&uG_D8VA!ndg z3k32RDs&3l=gn9PhBdc8R-<3cttaErC~uZjD=4D|wuZ4DDB_vhXmfXzzT|nIJF|St z;xr5YX%6Cs5$TWYZMm8Y78XV7{R~C9Q!u02yh^RN4qB~RaUDqUCn%JsC`t&-P`E=D z_``edjL)@+7c6obyH!`#{5NtHyH@{3YR zxOxqAW4wF@uLRddCHP%fb$G)?T&v}LwTOTHy94`-Csug_7E!O5oxRdrEsACG%F;8H z96#*ud5fch7D?C}zR~mXt`eShG2?=9A>?&1!HKDUZ55v z9tWLH1c&3X=&oCVF>l@bmKuTST@NT;&}@3MeuxZNDRpkj^2cbsZD9ali*leXjW3~d@Ky0oDy%xCj`5KW>|+zpf5N?cXt znOhT}oVl-~yQl(K8rB!5_=2OG8@hNx1F>pic zJU9_gMfgxPz>|GT&XtkJ9a{>g)R}u`+@6~k>4bFnBxgrx=RknYQt5bC zZZnxm=7B$hnwYzPEloexy0KaW=*x?8%DyU4)@x5S4iffpW}!J%G3i{`&@NxTO1o6{_j9AXTM0=Z_aKYPR%Cb5J4A3NN#0(* zE_aDSrMO?E*lI*vx_Gk|uWHDZHBihg?JVEddV#yr@f<%t#^%QP!Z_;M~4eB_SH*C~Ik%)) zE{D7lj1@g|;vnpW1}q}~x677O*+2h8@>jz@=jf)PQ@uTES$ZHqcVg*gB{fvR#TD0G za_cg4P+r#N3rTh9?j80PZdxedRpIV%=?+@1{9im+`MBg`s^E43uL@VEyyJ0ohijNN zn7vJIQ_){2Dv0b_`HCt*iYuC{w6qv+SuBXE%oIbN6cp@@ml7k_r09mcv#1)qKv2D# zn)R;f?P=KCy|z-OhDs~1JvBvS8}Ng}`N)VZy#yxQngFr0~P zmbSMjT4^=yeU07)NK|ziDr?A1LyP$B?-$kD3s*Sn8m4s^-K6%8=iDW(O!oS|TbX`0 zBX0xF-Je6Ds5lSS_vd(Jh%9Q2SXg_2P9r^+aHl0FlsaYr*{|9lIZv+wK}#;=Qx~jZE0c0k?#p4;S|$=r57X{H z#hY;2ifqwYTS~rw6||F^{L&e{k43LO<=JEnl|3!E2SWSK`EL;Db_5TdAJgkYku_T9 zZ=?eKcJOgLLN65Mzm|Z7l@udjWs=eJpQKU&NVUQe(djG6IfC+5wt2^3MJ8QLZEsxQ z2}b3LfA)H};>iP5+s_KGha=G-y;c#Bl&AjBE(Lv|>>#?!>mosEdry@7hH%G>uV#8_ z0u+4^A<7s6g?v3J*aQa`BA%dD&?NbW?7#&d#0sVWnooRv8x$9y8ndXWNGe^;;egBk zriTN!%&$2d=E0NY+#>N!CyeVIJ zbvw8@fBTh;dIXgQF3Z>&h&m0U+Htc!70+GMj=z2ne)_EJZb<&IwR`P~y$ zfSGh8P?L8&B?%yfj0F zm#V85^ykucn(kQ9Mnsj?ZK$B^Mn_M62Un|X@4AUS?76;Ilorz610B4}B%4F(#8uvz zvez8V2S{+uk_bqu*sb4NPv7ZQCKVgcsD zc7k3ceAj!CkW{@sMKdd2#LH39u;Jc)Sa-|b@m^%}E`OUvzn5S1LiwWYL(<}0FPH9) zL+bdV8m+*6i1!?=^JCZmq1^_@Qq!-h&35FQpXILF&?#^Jl%(+E35vhM3pT|MgUs&l zZCU0Qdw?4W>LRViRet=k|4TJ{)Of^>#y6^bevC>mPtV&*mS@((^yzq7fzVcL)n@18 z^l7J&`3P;Y@kQin#Z<0EU3BKU)FY?5q#Ikz=&)Pbb=jr!2OUha+{(Ngw{9I?LVm^N zZs7h-L4mouTXa>>D^*L?iq0WQy}a#sceh^24<++(eY@VgPFwk|*D$0Xv!k?2WhWOa zd@Je0qS^Ca*Dl>e;qpy{1Btv3!W4*9zL4+W zH_QH$l(rhf%wlBEP6n$qvqs^i?b44%P}#JpushQ^Sb2SlZA9y19Is4B%#V3rYjnm#Hbhh=mTjMx>P|H zenEuh74j2mMh-s4(8{@NP~Au%5m=jJH$c52g_p|{1O^5AYRLf`ls zz9RP@aU@^TE#`|>Ay3QY^4lmWZ{#TLxzF=Ub(byN{|au|m<@8Un)OEUIZTJXqM5u6M}kt*-~J+C{-H2F$j>8h^fSEais6)c@9S4_Dn(;c_#T-}iug6M@H z?<1eNz?lQ7_sB1bmih1;ZI?p1IV#Z(;sc`;g7Ziz;2eK>RPHU8-}dc-uEv%>M6Xcl zo&0Uiio5EIsLBgQ(#4?6%x*d?Z4;vp4$9TBR07Fk^f@I?eOfAjD%a4zpE>EI=^&m? zwCS6yu{eJ_b0U(o$^1cw2^4DccQm&M=WD7CvZQrUb3Kj8|2P3|Dls4 z%^l^p$LLG3^!f&U!Z+nevD3Q={_q;Th?}C1#Io@2%u!QfjhyX?W6@WHc$aZE?Ni*_=`o?L|Y2_S

A{R(~`(St7SH?62&K75NFX{Uorr5`+8;U6k&TJe7Iux^Fk z%iPT6zC>lIpEg{NK7O6$hZ3+UD}OjlGNNI@;chZ z#O{9QhWae}O75fQ$q%Uh*XeKS59vGbpZor}#qMmW{nE$(W_>N~Em`>}PWW0oUVJNi zQl_7%>+Hs(81S7q^Kv75R;Ca5dt2}*29f`qOds@j_Gjgz81fCg@#DkU*JS#T|IIEu ziu1nio|lW+3o`wD*fIfQ6{c z&MXKKuJ;`~^YV>s1X#Gfmi!uoNGQHffFkZK12C(Z5tOE!!LuIm@cYw__G*X-ZAyx_FSYQKNZQ#%bLab4J91Pcitu@q>twD$w z@v(o#fyElQzk?77#rFxYL_-brcMu}ofc~BVmTqXF{tiM+H7fsXgnu?Rv42WJEEC3Z zt^iwZTtQ4;|mBet+3m`rX>bKED^?XJ_5GX%zX@mc&xBZV2_*GW+23#!kz-#Yv#5EA*K$i zc0Y`EKg@i$Bq5d*_6XS0VUBem#Gb;Q0^2*xHUlA^9Y#BR3hdcojuRlnbH(==u;+&l z4fJP0i18NR!1*6HX0OWmL3X^wf1yheVxq;@edgu8>@{E$Ew#PW|0E%%ThOO(1DkH) zJ`F-Fw7}Mjz!qA#uYeHuTF`#?f!%AVrS=0MRu!KOY_)}_1t3J2Fm8tkuy70O4njPp zGP&Pw0(;yd$2dudsN&lK7Hz2^-9dkyN^;fF-5wAjF>1{VA}$mPXPY zgqUoFoHtu#+fKH!oFK$hE9ATdY^s&z1R0WMyLLqCH*tjdK#0jBN_SwBN9xFTZ%RVUE9@??d5M7#%Y<>fUIDgzq=Vu$2r=4* zGRN9veWPt`PY`0f4febWY`l%_2|`RLzH7iH+8W88AjCo&#*Rf`3vKMzAjC3ZJXWm$ zTW(`pgAgGqlWn~LEY!xf1|h;E3)?yZEZoMn1|hcFV6!K{w%ZPo%|M7}O3qJ#J!|9f z7K9jYhwfL~rS9YH978~e1%)jFTWDvyfDrfEVVC>B?zOXBK!^v5?;)@U?c5(gh~;)n z;#YtzxAQa}gjj9IMBWCr+TKjD6@;MQ!orvXEZxpJWF#S;DeO~V&q#-sEC?}u6ms4^ zD)pN_%5ffqSRjn;xd?3GXg$RV5Mu2p>RSi4cC?1-10g~R+W;0iTFdhuNr>1{v~3(% z>?pS_2(e9k?1N8$Z6Edbbt*#aD85}_JCYBCxZD9*u5?HpE_d)e3xpW$fS-*48|~mZ z3J5VteC$g%flYS!txiRVDaCgS*i=Uojkh4gatCa-0&Ka1Z3aSw2$TICSg3<-wjl|T zRC1<(B|F$=AjG!9o&ekKV4Hyu&q)^U51#>h-oZ8lAtpLuvumAF_lZun83=K`6MQ#- zUGHQ*5MqY-IF{c5Hq*&A10iOKkLRXyz-BwyW+21^;$xdV1oohlZ3aRtDZWR*mO7h= z4}^%R`r^Q1oxG+5A@&sZ6xd!TkHsLwEM1nA$*L?ykag6l?A$C-myTEpi)lr!s#OQIzGIm_bGJ3p;Fc4y%Fs|<| zu=(R03qXj43T*HAVUh)enCXIl-szG$%yjYm6NH%ULYZ^GX1jQ;4MMCbY#rEI7tcRIh#-~8 zW4{9|*v0yR5Fz4|?Eoy)#p~M*Nr-0(`xMx-E*W=$jdz3ZYPXbgyqoO_LQHg{zH7iH zx@)OE5Mo;K-3B(@eSzu&Ay!X7Kl_9%bM=H=cSu64sZE%5U~4B>2M{7ie5`{5EO>%- z03r4i_7vFO3DyCG7(EHTG3@M&o|NOJB*geh=x`O-_{m194}_Q}K5pB)z~)b~oFK%4 z;#&l^a8mjb%DkuQyASN%$(n)VSrB6Bq_3;*<<0CPU`r>PXkG_G#1vl~SnMS0077gl z>lI+jr)s*-WI>3vQ@-BUUfs;D16w=gKi{he5j=&pp#v;< zs*cu%AVf^n7Y7!T^??uxs*ipC6JUu`tUCy?qxg1#?VRHM5D;SNH0<)|w6x38=@!C3 zh%{k5{$+rrPjjpTA;xpi}nbFbdW-T-#J zr+tXV07-~2mC1e`0T%9Idx8*;sZ2Sa2llv!{cIC_QN_0fEZWmdd?3W-GvK>&2JL>P zj>bk1Vv;Z(8*c)eJkv_C2ZRWofh-QN;29ntK!}9GJ^_|E!*ehYVzd{1Wvo}$H`>c_ z5QG>fjP<(;Y`m9!351wd*ll3by{tP3G1Chj?f{$VW!*uDS;aR8Y_^x>1R<6cwgPOK z%IwU75Uafyi)~=5y*w6!5Mjj^0T%9Ur!ql^>wVDSMxWI0dLPRPLQM8S&YQp{`&dp8 zVoLGd0yfoGPqKgz_xd2qePH+c8cz3QL5NTv=E)ntLVY|>1|g!V%q?KiK8_P0M4B*; zSs7sIJ|0s+h-ZrLQ((`e4j{yHrNd{yo=Y7-h|3mqzhc3cEV&OY2{EOxTfn9)mIZ{E zSJ+)(^AZCgmK62~*pkFRh#+CC105Ff4t>z__x3A7geY2(Yll z<0c5PqsrU`wqw;&EC(Sj_p83r5B>U^sQs=;Ld^CCJ3>kuw7s~vP=+S zPhn4i?e()?gAfw~@at;>(w-9ob=1c|h?xP{8`$aq`#cB{R(ug)VXBYTqLL7g2hhhhfju6mr#=QkY%9Jez_y8x z)~VpzQP?i9odLEP2r+sV`i-5Hx{sb^{XmHEv(WDUP;<2i46WHUkY)=p(s`$2mMbFleJwb@=vuM93z_!nF z`+*Rb2URQ>L|++X{jNwtjH&~%F<_&ETptKAuK2D38y}Qo0QjZ}WBqOen;zso213lJ zGVcJJp)!wWL5SHw%%$dl%?|pl{wxSFKZy1FU10NrynY8E76!3?Uj()=SWD}75Mo)$ zxdLoi>IXusk}N!K+Q3!^dCm?(tSP>AU~7XMBSDB5VLYFR1B(svSpf)jNRCsJ>$_7PGg2O%3^v zw<I0hS)B>!5QB@I5Dt=YyXCdp=at=@*Ld*`0N@$@l4?d!7DzfA|;557FN* z`G?7W5+`c``Q+$UOmEq(v-u=Ve2Ey*-}p>MxV{-j}&6-u7v zf6nng=lLK0QT5~n{%4T?;S>Ml2>p}0vha59g2P+MHyGz1Sx*l0KW`&viY_IDBS|Ou z4%2+a!j2{{afWyKpSL)b?^n>b`(nvAIq5b2=OU*!t%RcVo7u6A_yU#wa{lddM zflLv-6pzcx?6K!+YwdXX(I2$-ecqg{-{|z$zMA{e$Ny*NU#s``zd9s*^q=&G{|EV3 BQbGU# literal 0 HcmV?d00001 diff --git a/ztools/ztools/resources/__pycache__/theme.cpython-313.pyc b/ztools/ztools/resources/__pycache__/theme.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95c0645029863d269ff90a25a6df904374e5f1c5 GIT binary patch literal 38184 zcmchA=~G}cerKdXiR7X$s@~NJl8st+0JvGXwMtW+J zPfhgHET5X`>8N}45FsfC_er!IQxmQUUEbV@#*qNmgH=`=m{$fq89IwPOX%=a$$zFRYY zw#WClHhET2eUj?i?7wLJH}abAqW_@o&fT$_*A8me?UZv+zir3TQRkp;BbiC14;oXM zWXRrdE_+W`yr;Jh8sj@oGHUM;s-6f{PlTp2>uF~IO3r;(2-b%N4# zXm*`6q`HWs+tqEPIc0E86Gyky)Ud~pI%9BHO0Tb8;^;b`C5~Q}X{lLypHWsnar79o z49);?bbYuiJ$^VJJUd zG^Dt!6Iw)>Qm-31n;O1hqCMktN$)whV#rRj5tIJ$o>8=Nb|(aU<<;JiZ| zJzCx+j$VWB8B*^XX-q3jk9~)v^oZi#Pmj6}j5Hq-N4J#A()|-K(tJc5-HO9#_%kF` zNyCqgG~fM3Wqr@c=PGe3S$d7q=yf`3aK;SIxWTzj9KGHrh@;zngE(K`hc}I!Zy7mH z5~q@-w<(Qo>6F2_Lmb_TY2ti+ow?rivdnS$u90TOD1DYV`mA7%IC`wk8&V6z(RIE@ z9K9|U4UXAAFHsu3EKZ|y?h{A%Jg3oZ|AaWY6%UMj9ui0Q{4#NLD;^O?w_?TMm{zP( z8eM18iZx18$qJj&=yt6eoDJgW8U_uHL!47u-y0%MkH*FGn1p7};)?HFY} zF*v)2D1Fb6GV5#KNb}U-uvhgueMTJJ^E@-xIiDFEvv!^vX+9^8Udvw)M{g&Y^3=$gJVkl6-uLfW0wB6NJ{t5Z+)YD z{HlWN|DM|I8}eQJ-TllBC*~w+tI$e^9cv@LorpR_ zjM?c#BD1j(iCK5z8)4g;ou9W->Ak3v3Oi2P3dNJw1Z_`_T^oO^xp_|RF{T2|7X3-3 zlL2eY&1g2AFGd(`P`=fC;In^^v}nHcy7lq5`G*xSR9^@?+fKg5oN6=^3r3xw^(2xG zTl?vFJesoXU@#tIFq}>&Qh`gCHasu9MV`6D(6_1W$JXnYnycc28`1c>9ks@tke!L9 zQ`J%Yb?Yf-OhFTkQ4Hj{CxtP!`ERutosgCJ8*)Y(W! z;<1q;BHe{)T;cyAi>yWz-m$6aEJb3$_!G;H1+6>dx2$VP$F5FQ%yMz6w33c{7yiv9 z4Fpn7!luSgjk%=ML-_7VB$y5dtalT;puxsB5H-J7B%d^)IU%SqB1j(y*CxL2RDJ-hX(Ri>AOgsiu@y7=KxRfc>HQD3j+VoAvaBPn~G$7R&9 z+fRQlKVe~~<#t8X(3RE3pkZ&&Yy%)2pKeBCGRR;}p7k5?m?{{lMxsuL`Z#HHUb0v) zLNf!e(^OsK70dUI+O*P$gJDRY6Tr$N=NsLLG3i_2&1<@##jO^32g{DkTW#5vY zGgo|pW&s-1(i8)3a<{_0K=INS#;VzA*+20@(+wmyj$M1W@wvKJD`;p!Or3u$WnEz1 zDTJ&DwT9HiDveCiL9)b>wQP(X=73%zi9V2Mu_oIdb6nrbPQ$BWsZpg#%N2A+ITHdE zhZs|2BkXK!dHtLj7W=Ai%qyjM-)R4;j33>EhlzM1leiNPIv)>S8GNLCtXkYi5>q#H zSh`>!uubF2$huZFY9f_u%x<8%JsH{uoQDkTWRfPnT4TC$*^O(rxqw3oR3oToGO4f| z)Kv;%E&|P_WDxJGc~3!4}1grAu8S+$-mgs~#MSC%5gWhI7p1N(uyI+R_D5C1`!6 z7o;k9i!2XN30fgg6CKE#dikMf+)fwtEr#7C`t*F8y+-2>iX|05J1L_^rH1r>w1(u) zquqY58qv34uU9$vs074bAds+QWu{r*FKs*rN;;khSR=Us)Lt()HPq@=4mvVDMI{;h zR7PHrjzO(syc(C?q^x$NHypn!$0`E-+3Im!GNLf8afpUjf$DGj|g?8fg zx&bv^$eD|v3k+0V&|WZQ3p!JGh4ffYncoVPca^iSq;Vs7mV$w5EfB~TsL&{EpEqMI z7}nhOSdDfuzmbebqr6#Ctxg#wus4kLK#|DYL7ls!^c64q+?nNNi+L9Q(*nfpMr1s4 zwB<@JSXdM(_j6?BPQi>~^9r>tAGTPv;yRS!Pmn23QIryxp>TsNh==#w8K3JDud~Qm z>=s?w@?Xyt?0WrMDLGf$2z(kYN~>3)ZS4Op+F@P>v~@NTjl`Th=jtWUE%EXRyc}E~ zm5_H~)!>Z~alMv{)gtlr?+zU^o?7J{Sj4vNH0@K`J-uMQIh8ruozeZG6hza17kAIuB07QxQH?4e(^& zl5=IGamSXzDK%!7G|aopbUnDu8MhbaMH(R;KFQe;>Nyagvs60XmD^0Fk$K=xVJ7D8 z-^kLBwPvgq0s8WyoUyM8jP>eMje`YI9>~>dZlEmUs8Z5-?n08%k@n+~nyKw0vodnZ zyGdwHRZKen?-oKD$UVrSx)s@-^bQf+Ns_mhuj^eRQ>pIP$+j9j zE?v4=k5{$Jl_gN0TRK>NptS;brQ-#Dejl3~=L`E;FM08OP*)2*aP=Wy%?`Yzm1hTD z(#n1BFQG+v5`kcnwh}l7N_RcV#;@JQ8cQ6bi(S=RUapIUi@tW(^g&5;^%^keM6_1q z9z4%c+=UluCuI%?JZWq8+B#hzvp3SW=+41n#BmQ8iVPjU4WGm_=_oI~)InsvE@p1U zkH0AvbAhVGqm0tSWLe2eYEs~Osg!1i*ogW=>F=uaEqq>8-M)oO$n`DWfw9*!yIWo7 zRZyw)g>xl0*}Z~f0lJJ;^xWPn!0r04cpOZNI*2G(DhX3!PR(?T<=lQn`oxuTr=gfjCE(glLeHv%qStEMytl$zJJb zR9m!pB89$n1M)r`nx8Tk3TohR=ha>W{uD9!p}YH5q;l0_wYVGyU=jVlTQ;A{@%g`{d^O^8o^BdC)!U<%xd#GtCzftjQbiS9TyeuCw=OdWUnFxS_d1ORI6&VntM8rW)#`pkQyjlp47zMKk1`MOEnqit5eOEO%Y6Ps88t zwUsg@R9boMt5M3;koRW`HcnOC6luf7w*-wiH1=9c=lH*hR=AsEHCp>jGEQGwSSwe0aS|o0Nuc*~tcZIXA5n6}QO=|CW&Ryb4w%7LE!t}ctc^h!<{v0wz z!FjO0KgTOWBvEU`!qNkD8tH|EJ1se-%rQHVgQ^XZ3-s!c#Vfcf-OS?r%P$art!O2B z&MC5Rbn`5^Oe~tCtO6cwbn!}k_xtF(3xq0P7*Nx*5^@1rJ4bd(-}tZvy#(i0`Y7Gq z-w0dYs)??KQpiN@M9R73?9$DVpd}absf*T#l}R}%_k}QPJrjwhM`-t-`kQdtifq$a zTS~rw6||FE{Gu7Xk43LO<@sbCg*`2}2SWSK`EL;DeuNC2AJgkYk#$<9nRcJOgL zLN65Mzm|Z7l~f~OWs=bgpQcg)Xtl~3(djGcc?tQg?(mMms${*D+S$Cw6O76Q|McZ< z$CHODc7P4u2uGqpdaWWLDbM_$UJLpv*G72%EudQ+?P4~v=4UK9J;aouZN_(s5i0h5vo5@I!`*QCZfVxpz zY;1Q6D^r_t4A9e6sy6pZK!H@{dK*_mjw@*KJ-w(i7EjX4+Sr)lmpA1LuWkc3=WqYB zYPElpu0zm!om@RME#Q~IrGBYPJn^X7OCu2Wx1o5e0ldoRmGdTP4oZv zt&aIbG?J#(YqfTM-TW)RgZ2tdv`~QRwO~t5IZAd*+=mzv1shN9lDxD)g_o+U*Xhrv z?KIu7qK$|ut=mvQ*^Q3g{06R8+1_;%d&CQUFDtD}cMo*%E|aVdX%iPYJ7ups?kjy> zL#73s*O03*_u&`3(vd{90-Cd5h$Ko65`V1u)7=gB22sxs#Hp$ur1Pugh5VXz_dTvF z!;Nv%SaM3~!b1s_josl!Ts4AYB57||?f#t1+zkx5P;*B*ZWj`L7-9wH!*+sRBz)I< zk&sNiIYl!oUc}2$(eUB^C-CmJz3aWm=3V|avwkl>>&5a}+ef6wxm?cO9f#EMMKx-H z+Ys+LS{KH!0YbYCj-{qwRh#X|H$Tf=wV_kq{3%J{#}kx%g%@m!9|oDt-`ldx&-M^E z64XUnO{)C(W&f9Iwy5!l9gS~R`TQ7#V4j}0l`PM!N9c3$v;v{6*s9IW$LVuUmlh(l z$;KCvtJSA+De9s#m!%$=>ymEYV)hPur9+oPI)Bi?G|Mf_yK(E5;W6?HE_Va>cM3Ah z-QA+Af?lo~SIatwDD(2Rb?U@)ko)?3Hpl1 zp6nxO1B0eAH!_it5tq;XSv*-tmvta_U7sdYz9V!+Ba;~Q`ab$VnuR7+kc3|lp?QV; zgqo3pk1@17Zp(T)jHce}E~Rz;8ku>Q6Q7ov@m+HphBQWvu%25bQ@c`C4v^=a^K*5VE!_SJZrPX>a=4Q9O7R-8x0S4Rc1=HE-Beof z$k$Pv97H`;-uruAbCNvyN?+-sZsZrGhbUz%gDRIzxhd1E+jOq)$_qjCLXh`?&s@iu z1F7}MuZWiU@EmQI!nk?L(GKFHqSOWFky62V{@SSATQ0xt+XY*VEq|z9A=labZO)3j z>Pslf>x^WG!I+u7bXfW(MjsrMt7B;d*kklLCFVXY)q$$e(7&HK>7?l(o=&vsJFKxd ze>-y`lC;VFL5B%sYV&tAw+ZKKst)wSAw!7|V&Z}@kAlz`gPj*$;3{TGeJ9;IoO2TK zBo6SrEaZ1b=&PM00Pkkf{1R%(ynYT!e+#ywCdRMbkf#$leXiXY8Q~8ixf^_DXKDY0 zKEmtT^sOJ5&2Kqr+PP2WW;pZmbGp@;?@`ry1=72I{MrO{wD?pjedspnShx-tw00u4 zMQt$T&~1VZC-o-1Dv_37`=!=cl@Bw`@6m_WcCDLq=R#hxpnDq>&NTm_lO!!1<+sP^ zOR@C&27SUevm&8JnK zz4F_vc5IKn11!JyLrHD=VsR!RBbw#js#XPYzM8$3@{H?@!Vei=l;2k#;jWiH(nUK6 z`^Pws5oWai*Nb4e2iIJ!2pOXv-ivP|D6DH6d|xqTt?voFaDr!L{M z-6NMTzyF?kcK@R*x$#VrD)W-OZ1z@S@1S#2edv5O_Zjo#2bBBk^f&bf^kw%ie1F_x zced1i<>P;|zLxfutb7!we61ZXzLPyI`KRhSyYVOneW%X8+{~Vn{6T+T3m(M~(w~?7 zA%EvURz8Yh-{2cRKAL?^@`wFzcHvQ6@OAgTT*_XQ{0og|yYVPK@SW;^8OeSq`5)9; z-FOsN{k~HJFSoMS{1Un9uOIAIgqR|%?`1f92iTOqw!d2uVnJc|fGtQ2gjgnwjd=uY z+3)Y`R)knl<*fo+@z+v$AVf&XZ2}AV8%Yj?*ddG!cmiz4@3&eMA$I+6(jKr~zrV9D z3qtJsef<|+J* z1tF#g>w57~_71SABX!+WN0JbWD$gZgi$^-CJP_g;VeHS(fIU0XKp_D_Ox8k|+qF`c z$=WuO10li+ivSDPb`SDLQE^{F0ko3_B9BxsIVnqixLANRur}h zY(?e?LaY(S@o58FtE=sAQG^KB!7mYD;kqvJ3kb1L4?XYIOFb9r+1DV%Vm*Al1Z=UM z>kovuujD=fcE7%XYycrb3flx0qC7jZAVj#{cjD~JH?k36;rd#NYY-x#-}UO2(eFkcE0!^`xMxIz5hg~BE&N)ujl23>}SB9)z_Y(x|M_&Z-DLB z8>H>y4Qx9IG0^~@-2gVx;P39tf)F?jV+Hkd*`xg*mT48sAO-l@fSSF11d<1N{ncEl$@mOJ7 zz#ccV&p?QMg*^qf-^_IjLQEZ1^?nrfew5|zNJ1=Cf#qukel5c>*y3T*!<`wWD5 zb`j^?kwL;H3z@}PRPY_~S$=wAu z-P%C?H3+fX3SU10w%p3T1|fnZ$NiE6EZEAv1|dRJ9=DZEV4>EA?p{TRbgS>|@T-rq z8DQzwn%Agbl7!e*y6gelC0%;6AjHHm$lW+5?VC8pav;RyF=acj$zydCySF7F78G_5 z*n-4Bh!w)Pzg`8la;$^;YY<|z4S9~W$?`_q*qRUmGw8}FBEZxpF zfDq3V_8G8eWJ609gqS`KJ?|cu_Dvt>ejbEaB#iyJ1Z?qmJ@peH#QJggWdqpy@fs=* zga|2Y6IkeYEzf%-A!5f-w{c*x<6O5O#16@E3_bz2bKKwGsR*&F83+*~OtyDmp$_)hrX)mC>6rqS>|mdP5IYKc0&J&)eFj22CtbKbd=Bh+2m1_! znCOJhZgfi9Cpy_@AjHj1$lU^Vvy{?83?hgw@|qHa*jLz7VEdgs7K0FzCt%<06VkrP z6FiRuAs#5~A+QH0>InlOLWHq>o4`URxE}-|5`^)*?o(ii6Ko#{v8(dj1Gamjj`9Q{ zMo&VQv6E7l(UVPtfe;IXae4QEEu7@O0EAdPiFjE8ws?~J0ubW9lKTYM{gZP2APEst zhd2R|q?5jMV0^2`%lym_hX1Wlcvt81LnJ%7xf)I0E$a5ap zToeHe3fb ze!7v$10fbjj_dXwu!YmCCkU~qDidA(j>P2-q^^*^&hzR(cTEtH4%zYP!#6L5TGpU*Bu5 zZf7@ut@rpZ^eI9Fd$2ZifCYQ%Xl)2W#8i24U@=)92$7)jIOabEmgr&IL5N)?w+C#u zhxbE3h~+cz%cC>WFUx0I2m>L~gz@;70hT_)eH{of-iv2d+3!L>W%CzU^jc)hiMFuga}if9M=(G;a>J92=SQml=FFDk9#@JwjdW( za@)Y7z0D*CLR>uyxoc-p?`P|1Yy=@D3FEQxHn7RFtq2U@8ehkA*K~}7ua+k+YUm^^udN%U^9Je zI|wnSj^?kDY-kqruyqi7ZBopKXmy7*!}*7Grd_5BGiw0@+PoQ zKhKjvh^WeQ8(6fT`w0*tO&IrC8DQyt9#cVxXG-ofV9%rtAjETJ!{@-BOB+Cls}^j( zW+9d=xeqM~F{Q9Oz@{wL1%y~o*gaqi5(6QY74`_&vcy1$AYp6+9R>0ZebDmv4Jbl{ zEUeKtfrTtyqk|A(3v2WUu&~ABCJ3>s^4tTqYt>R;4nkZVP;F%Z_6;;q{aurUm>YmD z^T6f?cnkm`7L?pQU<(6W?;yk?$#Kjt0b3mK4_b;4_Xm6fufKYl{RG(k0skBQiVzP5 zeAdNRTiJ)e9t`+j?^lFaChYtx+Pep~JWxx02nZ2W`xG4LCM_%wlK)$fe>p{9?z3)U~7XM^B_c6$wh#LsXSVXNt=z_IR+K+87A2qvW0d+aWnxr$TO5VSB)K2ia#J#OOKLH+D|iK6;Mr10lxG!M^Lj z#?Lj7eIUe~lA8xMcaG}_gm^$0w}*$o9-M0+`#^}Wl8XQf%kn^o$Ev(7V2{tSKS7A7 zlG_FrJy%Em1R-|Lq5hr#+d0Se2SQvOQhmV?+R6~ycTEyvR2_(o0UI6S@<51jC3hXz z_>dd}AU91I+jke(^bof(5MoBWUiD|^ zQAg)F&ufwp_fWkHAu%CqZ5IC}%w#IV1+OA+GcFzWplu$#mFv+Y?BVshAb{N#(B>}_C^ zvOExCipo3jVkvtE*wnE9WUC^?^sw(($BT*VU0~D0O}uX-39&Yeb08bo+Hl(!+HfbZ!B;=Y;Wm@N;0#hif|hLJ>Z@v#vJz zJ{@##&|mKl|K|7+`ui3C)cB8l59xk4{=BFDr(X#7@6X<#i#BwOZvL=#?jXIwZ-8sR z9sXAHMZY|FPjWF*tK@g+>EIas;8ygaI3l^(wEi56c=|9?Dc{rg%=3Qb?^ zKd6&GQkY~TlIQrJ^Zd^R{)c}WJ$aG;8RCEVls|ci{>fcJcsqAx;d1f~#`&kxlOz1k z+en$R6Nzvn=_KD_nlDY*(c~3Q@Gk#znYnypg1*KVOTNjh*Z7~enBTM-iqfxO$2Q{! z&8tcJ?XD+ut0Z}FgzjR&e73L92aP|o9S>%r&UcgJgmQ(X&eFfxBYwaCkGg#R)7g4o z)3Fz!AGUr`_hob2%f=sG`l8`W;yn3b-xu{?wj6)?`tP@#{i5kxdTMM{MddOs!fI%F zaqs)jzNr1u!#ssd6+Mn8Fg&em^s`fFd!3m^ZVU3jhDKk(|v LANlA%84dpr(#z%X literal 0 HcmV?d00001 diff --git a/ztools/ztools/resources/icons.py b/ztools/ztools/resources/icons.py new file mode 100644 index 0000000..cd711fe --- /dev/null +++ b/ztools/ztools/resources/icons.py @@ -0,0 +1,386 @@ +# ztools/resources/icons.py +# Catppuccin Mocha themed icons for ztools + +# Catppuccin Mocha Palette +MOCHA = { + "rosewater": "#f5e0dc", + "flamingo": "#f2cdcd", + "pink": "#f5c2e7", + "mauve": "#cba6f7", + "red": "#f38ba8", + "maroon": "#eba0ac", + "peach": "#fab387", + "yellow": "#f9e2af", + "green": "#a6e3a1", + "teal": "#94e2d5", + "sky": "#89dceb", + "sapphire": "#74c7ec", + "blue": "#89b4fa", + "lavender": "#b4befe", + "text": "#cdd6f4", + "subtext1": "#bac2de", + "subtext0": "#a6adc8", + "overlay2": "#9399b2", + "overlay1": "#7f849c", + "overlay0": "#6c7086", + "surface2": "#585b70", + "surface1": "#45475a", + "surface0": "#313244", + "base": "#1e1e2e", + "mantle": "#181825", + "crust": "#11111b", +} + + +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''' + + + +''' + +# Datum Creator icon - plane with plus +ICON_DATUM_CREATOR_SVG = f''' + + + + + + +''' + +# Datum Manager icon - stacked planes with list +ICON_DATUM_MANAGER_SVG = f''' + + + + + + + + +''' + +# Plane Offset icon +ICON_PLANE_OFFSET_SVG = f''' + + + + + + + + +''' + +# Plane Midplane icon +ICON_PLANE_MIDPLANE_SVG = f''' + + + + + + + +''' + +# Plane 3 Points icon +ICON_PLANE_3PT_SVG = f''' + + + + + + + +''' + +# Plane Normal to Edge icon +ICON_PLANE_NORMAL_SVG = f''' + + + + + + + +''' + +# Plane Angled icon +ICON_PLANE_ANGLED_SVG = f''' + + + + + + + +''' + +# Plane Tangent icon +ICON_PLANE_TANGENT_SVG = f''' + + + + + + + +''' + +# Axis 2 Points icon +ICON_AXIS_2PT_SVG = f''' + + + + + + +''' + +# Axis from Edge icon +ICON_AXIS_EDGE_SVG = f''' + + + + + + + +''' + +# Axis Cylinder Center icon +ICON_AXIS_CYL_SVG = f''' + + + + + + + + + +''' + +# Axis Intersection icon +ICON_AXIS_INTERSECT_SVG = f''' + + + + + + + +''' + +# Point at Vertex icon +ICON_POINT_VERTEX_SVG = f''' + + + + + + + + +''' + +# Point XYZ icon +ICON_POINT_XYZ_SVG = f''' + + + + + + + + + + +''' + +# Point on Edge icon +ICON_POINT_EDGE_SVG = f''' + + + + + + + t +''' + +# Point Face Center icon +ICON_POINT_FACE_SVG = f''' + + + + + + +''' + +# Point Circle Center icon +ICON_POINT_CIRCLE_SVG = f''' + + + + + + + + +''' + +# Rotated Linear Pattern icon - objects along line with rotation +ICON_ROTATED_PATTERN_SVG = f''' + + + + + + + + + + + + + + + + +''' + +# Enhanced Pocket icon - pocket with plus/settings indicator +ICON_POCKET_ENHANCED_SVG = f''' + + + + + + + + + + +''' + +# Flipped Pocket icon - pocket cutting outside the profile +ICON_POCKET_FLIPPED_SVG = f''' + + + + + + + + + + + + +''' + + +# ============================================================================= +# 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, + } + + 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, + } + + 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}") diff --git a/ztools/ztools/resources/icons/ztools_axis_2pt.svg b/ztools/ztools/resources/icons/ztools_axis_2pt.svg new file mode 100644 index 0000000..439f092 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_axis_2pt.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_axis_cyl.svg b/ztools/ztools/resources/icons/ztools_axis_cyl.svg new file mode 100644 index 0000000..0e723e7 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_axis_cyl.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_axis_edge.svg b/ztools/ztools/resources/icons/ztools_axis_edge.svg new file mode 100644 index 0000000..ab54592 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_axis_edge.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_axis_intersect.svg b/ztools/ztools/resources/icons/ztools_axis_intersect.svg new file mode 100644 index 0000000..4069f73 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_axis_intersect.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_datum_creator.svg b/ztools/ztools/resources/icons/ztools_datum_creator.svg new file mode 100644 index 0000000..de02931 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_datum_creator.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_datum_manager.svg b/ztools/ztools/resources/icons/ztools_datum_manager.svg new file mode 100644 index 0000000..9c80090 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_datum_manager.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_3pt.svg b/ztools/ztools/resources/icons/ztools_plane_3pt.svg new file mode 100644 index 0000000..4ed7efb --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_3pt.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_angled.svg b/ztools/ztools/resources/icons/ztools_plane_angled.svg new file mode 100644 index 0000000..6790fcd --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_angled.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_midplane.svg b/ztools/ztools/resources/icons/ztools_plane_midplane.svg new file mode 100644 index 0000000..bdd5ea9 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_midplane.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_normal.svg b/ztools/ztools/resources/icons/ztools_plane_normal.svg new file mode 100644 index 0000000..e564b74 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_normal.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_offset.svg b/ztools/ztools/resources/icons/ztools_plane_offset.svg new file mode 100644 index 0000000..7f1f35b --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_offset.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_plane_tangent.svg b/ztools/ztools/resources/icons/ztools_plane_tangent.svg new file mode 100644 index 0000000..b683e2f --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_plane_tangent.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_pocket_enhanced.svg b/ztools/ztools/resources/icons/ztools_pocket_enhanced.svg new file mode 100644 index 0000000..16eb0b8 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_pocket_enhanced.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_pocket_flipped.svg b/ztools/ztools/resources/icons/ztools_pocket_flipped.svg new file mode 100644 index 0000000..6e667a7 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_pocket_flipped.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_point_circle.svg b/ztools/ztools/resources/icons/ztools_point_circle.svg new file mode 100644 index 0000000..b05da8b --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_point_circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_point_edge.svg b/ztools/ztools/resources/icons/ztools_point_edge.svg new file mode 100644 index 0000000..7032701 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_point_edge.svg @@ -0,0 +1,9 @@ + + + + + + + + t + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_point_face.svg b/ztools/ztools/resources/icons/ztools_point_face.svg new file mode 100644 index 0000000..887201a --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_point_face.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_point_vertex.svg b/ztools/ztools/resources/icons/ztools_point_vertex.svg new file mode 100644 index 0000000..4db1731 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_point_vertex.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_point_xyz.svg b/ztools/ztools/resources/icons/ztools_point_xyz.svg new file mode 100644 index 0000000..37a355a --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_point_xyz.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_rotated_pattern.svg b/ztools/ztools/resources/icons/ztools_rotated_pattern.svg new file mode 100644 index 0000000..9b75476 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_rotated_pattern.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/ztools/ztools/resources/icons/ztools_theme_apply.svg b/ztools/ztools/resources/icons/ztools_theme_apply.svg new file mode 100644 index 0000000..5dfafd7 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_theme_apply.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_theme_export.svg b/ztools/ztools/resources/icons/ztools_theme_export.svg new file mode 100644 index 0000000..a4456e2 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_theme_export.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_theme_remove.svg b/ztools/ztools/resources/icons/ztools_theme_remove.svg new file mode 100644 index 0000000..aaba292 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_theme_remove.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_theme_toggle.svg b/ztools/ztools/resources/icons/ztools_theme_toggle.svg new file mode 100644 index 0000000..2845df5 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_theme_toggle.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_workbench.svg b/ztools/ztools/resources/icons/ztools_workbench.svg new file mode 100644 index 0000000..e879935 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_workbench.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/theme.py b/ztools/ztools/resources/theme.py new file mode 100644 index 0000000..a7c2652 --- /dev/null +++ b/ztools/ztools/resources/theme.py @@ -0,0 +1,1306 @@ +# ztools/resources/theme.py +# Catppuccin Mocha theme for FreeCAD +# +# This module generates a comprehensive Qt stylesheet (QSS) that applies the +# Catppuccin Mocha color palette across the entire FreeCAD interface. +# +# The theme is automatically installed to FreeCAD's Gui/Stylesheets directory +# by Init.py at startup, making it available in: +# Edit > Preferences > General > Stylesheet + +from .icons import MOCHA + +# Convenience aliases for commonly used colors +_base = MOCHA["base"] +_mantle = MOCHA["mantle"] +_crust = MOCHA["crust"] +_surface0 = MOCHA["surface0"] +_surface1 = MOCHA["surface1"] +_surface2 = MOCHA["surface2"] +_overlay0 = MOCHA["overlay0"] +_overlay1 = MOCHA["overlay1"] +_overlay2 = MOCHA["overlay2"] +_subtext0 = MOCHA["subtext0"] +_subtext1 = MOCHA["subtext1"] +_text = MOCHA["text"] +_lavender = MOCHA["lavender"] +_blue = MOCHA["blue"] +_sapphire = MOCHA["sapphire"] +_sky = MOCHA["sky"] +_teal = MOCHA["teal"] +_green = MOCHA["green"] +_yellow = MOCHA["yellow"] +_peach = MOCHA["peach"] +_maroon = MOCHA["maroon"] +_red = MOCHA["red"] +_mauve = MOCHA["mauve"] +_pink = MOCHA["pink"] +_flamingo = MOCHA["flamingo"] +_rosewater = MOCHA["rosewater"] + + +def generate_stylesheet() -> str: + """Generate the complete Catppuccin Mocha QSS stylesheet for FreeCAD. + + Returns: + str: Complete Qt stylesheet string. + """ + return f""" +/* ============================================================================= + Catppuccin Mocha Theme for FreeCAD + Bundled with ztools addon + https://catppuccin.com/ + ============================================================================= */ + +/* ============================================================================= + Global Defaults + ============================================================================= */ + +* {{ + color: {_text}; + font-family: "Segoe UI", "Ubuntu", "Noto Sans", sans-serif; +}} + +QWidget {{ + background-color: {_base}; + color: {_text}; + selection-background-color: {_surface2}; + selection-color: {_text}; +}} + +/* ============================================================================= + Main Window and MDI Area + ============================================================================= */ + +QMainWindow {{ + background-color: {_mantle}; +}} + +QMainWindow::separator {{ + background-color: {_surface0}; + width: 4px; + height: 4px; +}} + +QMainWindow::separator:hover {{ + background-color: {_mauve}; +}} + +QMdiArea {{ + background-color: {_crust}; +}} + +QMdiSubWindow {{ + background-color: {_base}; + border: 1px solid {_surface1}; +}} + +QMdiSubWindow > QWidget {{ + background-color: {_base}; +}} + +/* ============================================================================= + Menu Bar + ============================================================================= */ + +QMenuBar {{ + background-color: {_mantle}; + color: {_text}; + border-bottom: 1px solid {_surface0}; + padding: 2px; +}} + +QMenuBar::item {{ + background-color: transparent; + padding: 4px 8px; + border-radius: 4px; +}} + +QMenuBar::item:selected {{ + background-color: {_surface0}; +}} + +QMenuBar::item:pressed {{ + background-color: {_surface1}; +}} + +/* ============================================================================= + Menus + ============================================================================= */ + +QMenu {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 6px; + padding: 4px; +}} + +QMenu::item {{ + padding: 6px 24px 6px 8px; + border-radius: 4px; +}} + +QMenu::item:selected {{ + background-color: {_surface1}; + color: {_text}; +}} + +QMenu::item:disabled {{ + color: {_overlay0}; +}} + +QMenu::separator {{ + height: 1px; + background-color: {_surface1}; + margin: 4px 8px; +}} + +QMenu::icon {{ + margin-left: 8px; +}} + +QMenu::indicator {{ + width: 16px; + height: 16px; + margin-left: 4px; +}} + +/* ============================================================================= + Toolbars + ============================================================================= */ + +QToolBar {{ + background-color: {_mantle}; + border: none; + spacing: 2px; + padding: 2px; +}} + +QToolBar::handle {{ + background-color: {_surface1}; + width: 8px; + margin: 2px; + border-radius: 2px; +}} + +QToolBar::handle:horizontal {{ + width: 8px; +}} + +QToolBar::handle:vertical {{ + height: 8px; +}} + +QToolBar::separator {{ + background-color: {_surface1}; + width: 1px; + margin: 4px 2px; +}} + +/* ============================================================================= + Tool Buttons (Toolbar icons) + ============================================================================= */ + +QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; + padding: 4px; + margin: 1px; +}} + +QToolButton:hover {{ + background-color: {_surface0}; + border: 1px solid {_surface1}; +}} + +QToolButton:pressed {{ + background-color: {_surface1}; +}} + +QToolButton:checked {{ + background-color: {_surface1}; + border: 1px solid {_mauve}; +}} + +QToolButton:disabled {{ + color: {_overlay0}; +}} + +QToolButton[popupMode="1"] {{ + padding-right: 16px; +}} + +QToolButton::menu-button {{ + border: none; + width: 14px; +}} + +QToolButton::menu-arrow {{ + width: 10px; + height: 10px; +}} + +/* ============================================================================= + Push Buttons + ============================================================================= */ + +QPushButton {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 6px; + padding: 6px 16px; + min-height: 20px; +}} + +QPushButton:hover {{ + background-color: {_surface1}; + border-color: {_surface2}; +}} + +QPushButton:pressed {{ + background-color: {_surface2}; +}} + +QPushButton:checked {{ + background-color: {_mauve}; + color: {_crust}; + border-color: {_mauve}; +}} + +QPushButton:disabled {{ + background-color: {_surface0}; + color: {_overlay0}; + border-color: {_surface0}; +}} + +QPushButton:default {{ + border: 2px solid {_mauve}; +}} + +/* ============================================================================= + Dock Widgets + ============================================================================= */ + +QDockWidget {{ + background-color: {_base}; + color: {_text}; + titlebar-close-icon: none; + titlebar-normal-icon: none; +}} + +QDockWidget::title {{ + background-color: {_mantle}; + color: {_text}; + padding: 6px; + border-bottom: 1px solid {_surface0}; +}} + +QDockWidget::close-button, +QDockWidget::float-button {{ + background-color: transparent; + border: none; + padding: 2px; +}} + +QDockWidget::close-button:hover, +QDockWidget::float-button:hover {{ + background-color: {_surface0}; + border-radius: 4px; +}} + +/* ============================================================================= + Tab Widgets + ============================================================================= */ + +QTabWidget::pane {{ + background-color: {_base}; + border: 1px solid {_surface1}; + border-radius: 4px; + top: -1px; +}} + +QTabBar {{ + background-color: transparent; +}} + +QTabBar::tab {{ + background-color: {_surface0}; + color: {_subtext1}; + border: 1px solid {_surface1}; + padding: 6px 12px; + margin-right: 2px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; +}} + +QTabBar::tab:selected {{ + background-color: {_base}; + color: {_text}; + border-bottom-color: {_base}; +}} + +QTabBar::tab:hover:!selected {{ + background-color: {_surface1}; + color: {_text}; +}} + +QTabBar::tab:disabled {{ + color: {_overlay0}; +}} + +QTabBar::close-button {{ + margin-left: 4px; +}} + +QTabBar::close-button:hover {{ + background-color: {_red}; + border-radius: 2px; +}} + +/* ============================================================================= + Scroll Bars + ============================================================================= */ + +QScrollBar:horizontal {{ + background-color: {_mantle}; + height: 12px; + margin: 0 12px 0 12px; + border-radius: 6px; +}} + +QScrollBar:vertical {{ + background-color: {_mantle}; + width: 12px; + margin: 12px 0 12px 0; + border-radius: 6px; +}} + +QScrollBar::handle:horizontal {{ + background-color: {_surface1}; + min-width: 20px; + border-radius: 5px; + margin: 1px; +}} + +QScrollBar::handle:vertical {{ + background-color: {_surface1}; + min-height: 20px; + border-radius: 5px; + margin: 1px; +}} + +QScrollBar::handle:horizontal:hover, +QScrollBar::handle:vertical:hover {{ + background-color: {_surface2}; +}} + +QScrollBar::add-line:horizontal, +QScrollBar::sub-line:horizontal, +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical {{ + width: 12px; + height: 12px; + background-color: {_surface0}; + border-radius: 6px; +}} + +QScrollBar::add-line:horizontal:hover, +QScrollBar::sub-line:horizontal:hover, +QScrollBar::add-line:vertical:hover, +QScrollBar::sub-line:vertical:hover {{ + background-color: {_surface1}; +}} + +QScrollBar::add-page:horizontal, +QScrollBar::sub-page:horizontal, +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical {{ + background-color: transparent; +}} + +/* ============================================================================= + Input Fields + ============================================================================= */ + +QLineEdit {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px 8px; + selection-background-color: {_mauve}; + selection-color: {_crust}; +}} + +QLineEdit:focus {{ + border-color: {_mauve}; +}} + +QLineEdit:disabled {{ + background-color: {_mantle}; + color: {_overlay0}; +}} + +QLineEdit:read-only {{ + background-color: {_mantle}; +}} + +QTextEdit, QPlainTextEdit {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + selection-background-color: {_mauve}; + selection-color: {_crust}; +}} + +QTextEdit:focus, QPlainTextEdit:focus {{ + border-color: {_mauve}; +}} + +/* ============================================================================= + Spin Boxes + ============================================================================= */ + +QSpinBox, QDoubleSpinBox {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px; + padding-right: 20px; +}} + +QSpinBox:focus, QDoubleSpinBox:focus {{ + border-color: {_mauve}; +}} + +QSpinBox:disabled, QDoubleSpinBox:disabled {{ + background-color: {_mantle}; + color: {_overlay0}; +}} + +QSpinBox::up-button, QDoubleSpinBox::up-button {{ + subcontrol-origin: border; + subcontrol-position: top right; + width: 16px; + border-left: 1px solid {_surface1}; + border-top-right-radius: 4px; + background-color: {_surface1}; +}} + +QSpinBox::down-button, QDoubleSpinBox::down-button {{ + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 16px; + border-left: 1px solid {_surface1}; + border-bottom-right-radius: 4px; + background-color: {_surface1}; +}} + +QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, +QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover {{ + background-color: {_surface2}; +}} + +QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed, +QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed {{ + background-color: {_mauve}; +}} + +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow {{ + width: 8px; + height: 8px; +}} + +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow {{ + width: 8px; + height: 8px; +}} + +/* ============================================================================= + Combo Boxes + ============================================================================= */ + +QComboBox {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px 8px; + padding-right: 24px; + min-height: 20px; +}} + +QComboBox:hover {{ + border-color: {_surface2}; +}} + +QComboBox:focus {{ + border-color: {_mauve}; +}} + +QComboBox:disabled {{ + background-color: {_mantle}; + color: {_overlay0}; +}} + +QComboBox::drop-down {{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 20px; + border-left: 1px solid {_surface1}; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background-color: {_surface1}; +}} + +QComboBox::drop-down:hover {{ + background-color: {_surface2}; +}} + +QComboBox::down-arrow {{ + width: 10px; + height: 10px; +}} + +QComboBox QAbstractItemView {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + selection-background-color: {_surface1}; + selection-color: {_text}; + outline: none; +}} + +QComboBox QAbstractItemView::item {{ + padding: 4px 8px; + min-height: 24px; +}} + +QComboBox QAbstractItemView::item:hover {{ + background-color: {_surface1}; +}} + +QComboBox QAbstractItemView::item:selected {{ + background-color: {_surface2}; +}} + +/* ============================================================================= + Check Boxes + ============================================================================= */ + +QCheckBox {{ + spacing: 8px; + color: {_text}; +}} + +QCheckBox:disabled {{ + color: {_overlay0}; +}} + +QCheckBox::indicator {{ + width: 18px; + height: 18px; + border: 2px solid {_surface2}; + border-radius: 4px; + background-color: {_surface0}; +}} + +QCheckBox::indicator:hover {{ + border-color: {_mauve}; +}} + +QCheckBox::indicator:checked {{ + background-color: {_mauve}; + border-color: {_mauve}; +}} + +QCheckBox::indicator:checked:disabled {{ + background-color: {_overlay0}; + border-color: {_overlay0}; +}} + +QCheckBox::indicator:disabled {{ + background-color: {_mantle}; + border-color: {_surface1}; +}} + +/* ============================================================================= + Radio Buttons + ============================================================================= */ + +QRadioButton {{ + spacing: 8px; + color: {_text}; +}} + +QRadioButton:disabled {{ + color: {_overlay0}; +}} + +QRadioButton::indicator {{ + width: 18px; + height: 18px; + border: 2px solid {_surface2}; + border-radius: 9px; + background-color: {_surface0}; +}} + +QRadioButton::indicator:hover {{ + border-color: {_mauve}; +}} + +QRadioButton::indicator:checked {{ + background-color: {_mauve}; + border-color: {_mauve}; +}} + +QRadioButton::indicator:checked:disabled {{ + background-color: {_overlay0}; + border-color: {_overlay0}; +}} + +QRadioButton::indicator:disabled {{ + background-color: {_mantle}; + border-color: {_surface1}; +}} + +/* ============================================================================= + Sliders + ============================================================================= */ + +QSlider::groove:horizontal {{ + height: 6px; + background-color: {_surface1}; + border-radius: 3px; +}} + +QSlider::groove:vertical {{ + width: 6px; + background-color: {_surface1}; + border-radius: 3px; +}} + +QSlider::handle:horizontal {{ + width: 16px; + height: 16px; + margin: -5px 0; + background-color: {_mauve}; + border-radius: 8px; +}} + +QSlider::handle:vertical {{ + width: 16px; + height: 16px; + margin: 0 -5px; + background-color: {_mauve}; + border-radius: 8px; +}} + +QSlider::handle:horizontal:hover, +QSlider::handle:vertical:hover {{ + background-color: {_lavender}; +}} + +QSlider::handle:horizontal:pressed, +QSlider::handle:vertical:pressed {{ + background-color: {_pink}; +}} + +QSlider::sub-page:horizontal {{ + background-color: {_mauve}; + border-radius: 3px; +}} + +QSlider::add-page:vertical {{ + background-color: {_mauve}; + border-radius: 3px; +}} + +/* ============================================================================= + Progress Bars + ============================================================================= */ + +QProgressBar {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + text-align: center; + height: 20px; +}} + +QProgressBar::chunk {{ + background-color: {_mauve}; + border-radius: 3px; +}} + +/* ============================================================================= + Group Boxes + ============================================================================= */ + +QGroupBox {{ + background-color: {_base}; + border: 1px solid {_surface1}; + border-radius: 6px; + margin-top: 12px; + padding-top: 8px; +}} + +QGroupBox::title {{ + subcontrol-origin: margin; + subcontrol-position: top left; + left: 12px; + padding: 0 4px; + color: {_subtext1}; + background-color: {_base}; +}} + +/* ============================================================================= + Tree View + ============================================================================= */ + +QTreeView {{ + background-color: {_base}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + outline: none; +}} + +QTreeView::item {{ + padding: 4px; + border-radius: 2px; +}} + +QTreeView::item:hover {{ + background-color: {_surface0}; +}} + +QTreeView::item:selected {{ + background-color: {_surface1}; + color: {_text}; +}} + +QTreeView::item:selected:active {{ + background-color: {_surface2}; +}} + +/* Branch indicators (collapse/expand arrows) - uses FreeCAD built-in light images for dark theme */ +QTreeView::branch {{ + background: transparent; +}} + +QTreeView::branch:has-siblings:!adjoins-item {{ + border-image: url(qss:images_dark-light/branch_vline_light.svg) 0; +}} + +QTreeView::branch:has-siblings:adjoins-item {{ + border-image: url(qss:images_dark-light/branch_more_light.svg) 0; +}} + +QTreeView::branch:!has-children:!has-siblings:adjoins-item {{ + border-image: url(qss:images_dark-light/branch_end_light.svg) 0; +}} + +QTreeView::branch:closed:has-children:has-siblings {{ + border-image: url(qss:images_dark-light/branch_more_closed_light.svg) 0; +}} + +QTreeView::branch:has-children:!has-siblings:closed {{ + border-image: url(qss:images_dark-light/branch_end_closed_light.svg) 0; +}} + +QTreeView::branch:open:has-children:has-siblings {{ + border-image: url(qss:images_dark-light/branch_more_open_light.svg) 0; +}} + +QTreeView::branch:open:has-children:!has-siblings {{ + border-image: url(qss:images_dark-light/branch_end_open_light.svg) 0; +}} + +/* ============================================================================= + List View + ============================================================================= */ + +QListView {{ + background-color: {_base}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + outline: none; +}} + +QListView::item {{ + padding: 4px; + border-radius: 2px; +}} + +QListView::item:hover {{ + background-color: {_surface0}; +}} + +QListView::item:selected {{ + background-color: {_surface1}; + color: {_text}; +}} + +/* ============================================================================= + Table View + ============================================================================= */ + +QTableView {{ + background-color: {_base}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + gridline-color: {_surface0}; + outline: none; +}} + +QTableView::item {{ + padding: 4px; +}} + +QTableView::item:hover {{ + background-color: {_surface0}; +}} + +QTableView::item:selected {{ + background-color: {_surface1}; + color: {_text}; +}} + +QTableView QTableCornerButton::section {{ + background-color: {_surface0}; + border: 1px solid {_surface1}; +}} + +/* ============================================================================= + Header Views (for Tables/Trees) + ============================================================================= */ + +QHeaderView {{ + background-color: {_surface0}; + border: none; +}} + +QHeaderView::section {{ + background-color: {_surface0}; + color: {_subtext1}; + border: none; + border-right: 1px solid {_surface1}; + border-bottom: 1px solid {_surface1}; + padding: 6px 8px; +}} + +QHeaderView::section:hover {{ + background-color: {_surface1}; + color: {_text}; +}} + +QHeaderView::section:checked {{ + background-color: {_surface1}; +}} + +QHeaderView::down-arrow {{ + width: 10px; + height: 10px; +}} + +QHeaderView::up-arrow {{ + width: 10px; + height: 10px; +}} + +/* ============================================================================= + Splitters + ============================================================================= */ + +QSplitter::handle {{ + background-color: {_surface0}; +}} + +QSplitter::handle:horizontal {{ + width: 4px; +}} + +QSplitter::handle:vertical {{ + height: 4px; +}} + +QSplitter::handle:hover {{ + background-color: {_mauve}; +}} + +/* ============================================================================= + Status Bar + ============================================================================= */ + +QStatusBar {{ + background-color: {_mantle}; + color: {_subtext1}; + border-top: 1px solid {_surface0}; +}} + +QStatusBar::item {{ + border: none; +}} + +QStatusBar QLabel {{ + padding: 2px 8px; +}} + +/* ============================================================================= + Tooltips + ============================================================================= */ + +QToolTip {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px 8px; +}} + +/* ============================================================================= + Labels + ============================================================================= */ + +QLabel {{ + color: {_text}; + background-color: transparent; +}} + +QLabel:disabled {{ + color: {_overlay0}; +}} + +/* ============================================================================= + Frames + ============================================================================= */ + +QFrame {{ + border: none; +}} + +QFrame[frameShape="4"] {{ + /* HLine */ + background-color: {_surface1}; + max-height: 1px; +}} + +QFrame[frameShape="5"] {{ + /* VLine */ + background-color: {_surface1}; + max-width: 1px; +}} + +/* ============================================================================= + Tool Box (Collapsible sections) + ============================================================================= */ + +QToolBox {{ + background-color: {_base}; + border: 1px solid {_surface1}; + border-radius: 4px; +}} + +QToolBox::tab {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 8px; +}} + +QToolBox::tab:selected {{ + background-color: {_surface1}; + border-color: {_mauve}; +}} + +QToolBox::tab:hover {{ + background-color: {_surface1}; +}} + +/* ============================================================================= + Dialog Buttons + ============================================================================= */ + +QDialogButtonBox {{ + button-layout: 0; +}} + +/* ============================================================================= + Date/Time Edits + ============================================================================= */ + +QDateEdit, QTimeEdit, QDateTimeEdit {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px; +}} + +QDateEdit:focus, QTimeEdit:focus, QDateTimeEdit:focus {{ + border-color: {_mauve}; +}} + +QDateEdit::drop-down, QTimeEdit::drop-down, QDateTimeEdit::drop-down {{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 20px; + border-left: 1px solid {_surface1}; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + background-color: {_surface1}; +}} + +QCalendarWidget {{ + background-color: {_base}; +}} + +QCalendarWidget QToolButton {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + margin: 2px; +}} + +QCalendarWidget QToolButton:hover {{ + background-color: {_surface1}; +}} + +QCalendarWidget QMenu {{ + background-color: {_surface0}; +}} + +QCalendarWidget QSpinBox {{ + background-color: {_surface0}; +}} + +QCalendarWidget QAbstractItemView {{ + background-color: {_base}; + selection-background-color: {_mauve}; + selection-color: {_crust}; +}} + +/* ============================================================================= + Wizard + ============================================================================= */ + +QWizard {{ + background-color: {_base}; +}} + +QWizard QLabel {{ + color: {_text}; +}} + +/* ============================================================================= + FreeCAD Specific Widgets + ============================================================================= */ + +/* Property Editor */ +Gui--PropertyEditor--PropertyEditor {{ + background-color: {_base}; + color: {_text}; + border: 1px solid {_surface1}; + qproperty-groupBackground: {_surface0}; + qproperty-groupTextColor: {_subtext1}; + qproperty-itemBackground: {_base}; +}} + +Gui--PropertyEditor--PropertyEditor QLineEdit {{ + background-color: {_surface0}; + border: 1px solid {_surface1}; +}} + +Gui--PropertyEditor--PropertyEditor QComboBox {{ + background-color: {_surface0}; +}} + +/* Color Button */ +Gui--ColorButton {{ + background-color: {_surface0}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 2px; +}} + +Gui--ColorButton:hover {{ + border-color: {_mauve}; +}} + +/* Workbench Selector */ +Gui--WorkbenchComboBox {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; + padding: 4px 8px; +}} + +Gui--WorkbenchComboBox:hover {{ + border-color: {_surface2}; +}} + +Gui--WorkbenchComboBox::drop-down {{ + background-color: {_surface1}; + border-left: 1px solid {_surface1}; + border-radius: 0 4px 4px 0; +}} + +/* Task Panel */ +QSint--ActionGroup {{ + background-color: {_surface0}; + border: 1px solid {_surface1}; + border-radius: 6px; +}} + +QSint--ActionGroup QToolButton {{ + background-color: {_surface0}; + color: {_text}; + border: none; + border-radius: 4px; + padding: 6px; +}} + +QSint--ActionGroup QToolButton:hover {{ + background-color: {_surface1}; +}} + +QSint--ActionGroup QFrame {{ + background-color: {_base}; + border: none; + border-radius: 4px; +}} + +/* Input Field */ +Gui--InputField {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; + border-radius: 4px; +}} + +Gui--InputField:focus {{ + border-color: {_mauve}; +}} + +/* Expression Completer */ +Gui--ExpressionCompleter {{ + background-color: {_surface0}; + color: {_text}; + border: 1px solid {_surface1}; +}} + +/* Spreadsheet */ +SpreadsheetGui--SheetTableView {{ + background-color: {_base}; + color: {_text}; + gridline-color: {_surface1}; + selection-background-color: {_surface1}; + selection-color: {_text}; +}} + +SpreadsheetGui--SheetTableView QHeaderView::section {{ + background-color: {_surface0}; + color: {_subtext1}; + border: 1px solid {_surface1}; + padding: 4px; +}} + +/* Python Console */ +Gui--PythonConsole {{ + background-color: {_crust}; + color: {_text}; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; + selection-background-color: {_surface1}; +}} + +/* Python Editor */ +Gui--PythonEditor {{ + background-color: {_crust}; + color: {_text}; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; + selection-background-color: {_mauve}; + selection-color: {_crust}; +}} + +/* Report View */ +Gui--DockWnd--ReportOutput {{ + background-color: {_crust}; + color: {_text}; + font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace; +}} + +/* DAG View */ +Gui--DAG--Model {{ + background-color: {_base}; +}} + +/* ============================================================================= + Sketcher Specific Styles + ============================================================================= */ + +/* Sketcher constraint colors are handled via preferences, not QSS */ + +/* ============================================================================= + Syntax Highlighting Colors (Python Editor) + Note: These are typically set via FreeCAD preferences, but we define them here + for reference and any widgets that support them. + ============================================================================= */ + +/* + Python Editor Syntax Colors (Catppuccin Mocha): + - Comment: {_overlay1} + - Number: {_peach} + - String: {_green} + - Keyword: {_mauve} + - Class/Def name: {_blue} + - Operator: {_sky} + - Output: {_text} + - Error: {_red} +*/ + +/* ============================================================================= + Custom Color Accents by Context + ============================================================================= */ + +/* Success states */ +*[state="success"] {{ + color: {_green}; +}} + +/* Warning states */ +*[state="warning"] {{ + color: {_yellow}; +}} + +/* Error states */ +*[state="error"] {{ + color: {_red}; +}} + +/* Info states */ +*[state="info"] {{ + color: {_blue}; +}} +""" + + +def get_stylesheet() -> str: + """Get the Catppuccin Mocha stylesheet. + + Returns: + str: Complete QSS stylesheet. + """ + return generate_stylesheet()