From 2f03558a33d33d4d45b00d3b6d55841f73408e97 Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Mon, 26 Jan 2026 06:34:59 -0600 Subject: [PATCH] add spreadsheet commands --- PLAN.md | 234 +++++- ROADMAP.md | 388 +++++++++ ztools/InitGui.py | 117 ++- ztools/ztools/commands/__init__.py | 11 +- .../__pycache__/__init__.cpython-312.pyc | Bin 350 -> 453 bytes .../assembly_pattern_commands.cpython-312.pyc | Bin 0 -> 38870 bytes .../commands/assembly_pattern_commands.py | 787 ++++++++++++++++++ .../ztools/commands/spreadsheet_commands.py | 567 +++++++++++++ .../__pycache__/icons.cpython-312.pyc | Bin 17584 -> 25562 bytes .../__pycache__/theme.cpython-312.pyc | Bin 38238 -> 41535 bytes ztools/ztools/resources/icons.py | 152 ++++ .../icons/ztools_assembly_linear_pattern.svg | 17 + .../icons/ztools_assembly_polar_pattern.svg | 27 + ztools/ztools/resources/theme.py | 87 ++ 14 files changed, 2376 insertions(+), 11 deletions(-) create mode 100644 ROADMAP.md create mode 100644 ztools/ztools/commands/__pycache__/assembly_pattern_commands.cpython-312.pyc create mode 100644 ztools/ztools/commands/assembly_pattern_commands.py create mode 100644 ztools/ztools/commands/spreadsheet_commands.py create mode 100644 ztools/ztools/resources/icons/ztools_assembly_linear_pattern.svg create mode 100644 ztools/ztools/resources/icons/ztools_assembly_polar_pattern.svg diff --git a/PLAN.md b/PLAN.md index 4ba5cdc..5c57208 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,18 +1,37 @@ # ZTools Development Plan -## Current Status: v0.1.0 (70% complete) +## Current Status: v0.3.0 (80% complete) ### What's Working -- Workbench registration with 10 toolbars and menus +- Workbench registration with 17 toolbars and menus - All 15 datum creation functions with custom ZTools attachment system - Datum Creator GUI (task panel with Planes/Axes/Points tabs) - OK button creates datum, Cancel dismisses without creating - Rotated Linear Pattern feature (complete) -- Icon system (21+ Catppuccin-themed SVGs) +- Icon system (32+ Catppuccin-themed SVGs) - Metadata storage system (ZTools_Type, ZTools_Params, ZTools_SourceRefs) - Spreadsheet linking for parametric control +- FreeCAD 1.0+ Assembly workbench integration (all stock commands) +- Assembly Linear Pattern tool (complete) +- Assembly Polar Pattern tool (complete) +- FreeCAD Spreadsheet workbench integration (all stock commands) +- zSpreadsheet formatting toolbar (9 commands) -### Recent Changes (2026-01-24) +### Recent Changes (2026-01-25) +- Added zSpreadsheet module with formatting toolbar +- Native Spreadsheet commands exposed (CreateSheet, Import, Export, SetAlias, MergeCells, SplitCell) +- Created 9 formatting commands: Bold, Italic, Underline, Align Left/Center/Right, Background Color, Text Color, Quick Alias +- Added 9 spreadsheet icons (Catppuccin Mocha theme) +- Spreadsheet text color now defaults to white for dark theme compatibility + +### Previous Changes (2026-01-25) +- Added FreeCAD 1.0+ Assembly workbench integration +- All native Assembly commands exposed in ztools workbench (21 commands) +- Created Assembly Linear Pattern tool with task panel UI +- Created Assembly Polar Pattern tool with task panel UI +- Added assembly pattern icons (Catppuccin Mocha theme) + +### Previous Changes (2026-01-24) - Replaced FreeCAD's vanilla attachment system with custom ZTools attachment - All datums now use `MapMode='Deactivated'` with calculated placements - Source references stored in `ZTools_SourceRefs` property for future update capability @@ -42,6 +61,174 @@ All ZTools datums have these custom properties: --- +## Phase 0: Complete (Assembly Integration) + +### FreeCAD 1.0+ Assembly Workbench Commands + +ZTools exposes all native FreeCAD Assembly workbench commands in 3 toolbars: + +**Assembly Structure:** +- `Assembly_CreateAssembly` - Create new assembly container +- `Assembly_InsertLink` - Insert component as link +- `Assembly_InsertNewPart` - Create and insert new part + +**Assembly Joints (13 types):** +- `Assembly_CreateJointFixed` - Lock parts together (0 DOF) +- `Assembly_CreateJointRevolute` - Rotation around axis +- `Assembly_CreateJointCylindrical` - Rotation + translation along axis +- `Assembly_CreateJointSlider` - Translation along axis +- `Assembly_CreateJointBall` - Spherical rotation +- `Assembly_CreateJointDistance` - Maintain distance +- `Assembly_CreateJointParallel` - Keep parallel +- `Assembly_CreateJointPerpendicular` - Keep perpendicular +- `Assembly_CreateJointAngle` - Maintain angle +- `Assembly_CreateJointRackPinion` - Rack and pinion motion +- `Assembly_CreateJointScrew` - Helical motion +- `Assembly_CreateJointGears` - Gear ratio constraint +- `Assembly_CreateJointBelt` - Belt/pulley constraint + +**Assembly Management:** +- `Assembly_ToggleGrounded` - Lock part in place +- `Assembly_SolveAssembly` - Run constraint solver +- `Assembly_CreateView` - Create exploded view +- `Assembly_CreateBom` - Generate bill of materials +- `Assembly_ExportASMT` - Export assembly file + +### ZTools Assembly Pattern Tools + +**Assembly Linear Pattern** (`ZTools_AssemblyLinearPattern`) + +Creates copies of assembly components along a linear direction. + +Features: +- Multi-component selection via table UI +- Direction vector (X, Y, Z) +- Occurrence count (2-100) +- Spacing modes: Total Length or Fixed Spacing +- Creates as Links (recommended) or copies +- Option to hide original components +- Auto-detects parent assembly + +UI Layout: +``` ++----------------------------------+ +| Components | +| +------------------------------+ | +| | Component_1 [X] | | +| | Component_2 [X] | | +| +------------------------------+ | +| Select components in 3D view | ++----------------------------------+ +| Pattern Parameters | +| Direction: X[1] Y[0] Z[0] | +| Occurrences: [3] | +| Mode: [Total Length v] | +| Total Length: [100 mm] | ++----------------------------------+ +| Options | +| [x] Create as Links | +| [ ] Hide original components | ++----------------------------------+ +``` + +**Assembly Polar Pattern** (`ZTools_AssemblyPolarPattern`) + +Creates copies of assembly components around a rotation axis. + +Features: +- Multi-component selection via table UI +- Axis presets (X, Y, Z) or custom axis vector +- Center point specification +- Occurrence count (2-100) +- Angle modes: Full Circle (360) or Custom Angle +- Creates as Links (recommended) or copies +- Option to hide original components + +UI Layout: +``` ++----------------------------------+ +| Components | +| +------------------------------+ | +| | Component_1 [X] | | +| +------------------------------+ | ++----------------------------------+ +| Rotation Axis | +| Axis: [Z Axis v] | +| Direction: X[0] Y[0] Z[1] | +| Center: X[0] Y[0] Z[0] | ++----------------------------------+ +| Pattern Parameters | +| Occurrences: [6] | +| Mode: [Full Circle v] | +| Total Angle: [360 deg] | ++----------------------------------+ +| Options | +| [x] Create as Links | +| [ ] Hide original components | ++----------------------------------+ +``` + +--- + +## Phase 0.5: Complete (zSpreadsheet) + +### FreeCAD Spreadsheet Workbench Commands + +ZTools exposes native Spreadsheet commands in the "Spreadsheet" toolbar: + +- `Spreadsheet_CreateSheet` - Create new spreadsheet +- `Spreadsheet_Import` - Import CSV file +- `Spreadsheet_Export` - Export to CSV +- `Spreadsheet_SetAlias` - Set cell alias +- `Spreadsheet_MergeCells` - Merge selected cells +- `Spreadsheet_SplitCell` - Split merged cell + +### ZTools Spreadsheet Formatting Tools + +Quick formatting toolbar for cell styling without dialogs: + +**Style Commands:** +- `ZTools_SpreadsheetStyleBold` - Toggle bold (B icon) +- `ZTools_SpreadsheetStyleItalic` - Toggle italic (I icon) +- `ZTools_SpreadsheetStyleUnderline` - Toggle underline (U icon) + +**Alignment Commands:** +- `ZTools_SpreadsheetAlignLeft` - Align text left +- `ZTools_SpreadsheetAlignCenter` - Align text center +- `ZTools_SpreadsheetAlignRight` - Align text right + +**Color Commands:** +- `ZTools_SpreadsheetBgColor` - Set cell background color (color picker) +- `ZTools_SpreadsheetTextColor` - Set cell text color (color picker) + +**Utility Commands:** +- `ZTools_SpreadsheetQuickAlias` - Create alias from adjacent label cell + +### Implementation Details + +**Cell Selection Helper:** +The `get_selected_cells()` function: +1. Gets active MDI subwindow +2. Finds QTableView widget +3. Gets selected indexes from selection model +4. Converts to A1 notation (handles AA, AB, etc.) + +**Style Toggle Pattern:** +```python +current = sheet.getStyle(cell) or "" +styles = set(s.strip() for s in current.split("|") if s.strip()) +if "bold" in styles: + styles.discard("bold") +else: + styles.add("bold") +sheet.setStyle(cell, "|".join(sorted(styles))) +``` + +**Color Picker Integration:** +Uses Qt's `QColorDialog.getColor()` with Catppuccin defaults for dark theme. + +--- + ## Phase 1: Complete (Datum Tools) All datum creation functions now work: @@ -150,8 +337,10 @@ UI Layout: | `ztools/ztools/commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 | | `ztools/ztools/commands/pocket_commands.py` | Enhanced Pocket GUI | ~600 | | `ztools/ztools/commands/pattern_commands.py` | Rotated Linear Pattern | ~206 | -| `ztools/InitGui.py` | Workbench registration | ~200 | -| `ztools/ztools/resources/icons.py` | SVG icon definitions | ~400 | +| `ztools/ztools/commands/assembly_pattern_commands.py` | Assembly Linear/Polar Patterns | ~580 | +| `ztools/ztools/commands/spreadsheet_commands.py` | Spreadsheet formatting tools | ~480 | +| `ztools/InitGui.py` | Workbench registration | ~330 | +| `ztools/ztools/resources/icons.py` | SVG icon definitions | ~540 | --- @@ -188,6 +377,39 @@ UI Layout: - [ ] Rename persists after recompute - [ ] Delete removes datum cleanly +### Assembly Integration Tests +- [ ] Assembly workbench commands appear in toolbars +- [ ] Assembly_CreateAssembly works from ztools +- [ ] Assembly_InsertLink works from ztools +- [ ] All joint commands accessible +- [ ] Assembly_SolveAssembly works + +### Assembly Pattern Tests +- [ ] Linear pattern with 3 occurrences along X axis +- [ ] Linear pattern with Total Length mode +- [ ] Linear pattern with Spacing mode +- [ ] Linear pattern creates links (not copies) +- [ ] Polar pattern with 6 occurrences (full circle) +- [ ] Polar pattern with custom angle (90 degrees, 4 occurrences) +- [ ] Polar pattern around Z axis +- [ ] Polar pattern with custom center point +- [ ] Multiple components can be patterned simultaneously +- [ ] Pattern instances added to parent assembly + +### zSpreadsheet Tests +- [ ] Spreadsheet toolbar appears with stock commands +- [ ] ztools Spreadsheet toolbar appears with formatting commands +- [ ] Create new spreadsheet via toolbar +- [ ] Select cells and toggle Bold style +- [ ] Select cells and toggle Italic style +- [ ] Select cells and toggle Underline style +- [ ] Align cells left/center/right +- [ ] Set background color via color picker +- [ ] Set text color via color picker (verify white default for dark theme) +- [ ] Quick Alias creates alias from left-adjacent cell content +- [ ] Undo/redo works for all formatting operations +- [ ] Commands are disabled when no cells selected + --- ## Notes diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..bf662ba --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,388 @@ +# ZTools Roadmap + +**Version:** 0.3.0 +**Last Updated:** 2026-01-25 +**Target Platform:** FreeCAD 1.0+ +**License:** LGPL-3.0-or-later + +--- + +## Executive Summary + +ZTools is an extended PartDesign workbench replacement for FreeCAD, focused on velocity-driven CAD workflows. It integrates enhanced datum creation, assembly patterning, spreadsheet formatting, and a unified dark theme (Catppuccin Mocha). + +**Current State:** 80% complete for v1.0 release +**Active Development Areas:** Datum management, Enhanced Pocket completion, documentation + +--- + +## Table of Contents + +1. [Implemented Features](#implemented-features) +2. [Known Gaps & Incomplete Features](#known-gaps--incomplete-features) +3. [FreeCAD Ecosystem Alignment](#freecad-ecosystem-alignment) +4. [Development Roadmap](#development-roadmap) +5. [Technical Architecture](#technical-architecture) +6. [File Reference](#file-reference) + +--- + +## Implemented Features + +### 1. Workbench Integration (17 Toolbars) + +ZTools consolidates multiple FreeCAD workbenches into a single unified interface: + +| Category | Toolbars | Commands | +|----------|----------|----------| +| PartDesign | Structure, Datums, Additive, Subtractive, Transformations, Dress-Up, Boolean | 35+ native commands | +| Sketcher | Sketcher | 4 native commands | +| Assembly | Assembly, Assembly Joints, Assembly Management | 21 native commands | +| Spreadsheet | Spreadsheet | 6 native commands | +| ZTools Custom | ztools Datums, ztools Patterns, ztools Features, ztools Assembly, ztools Spreadsheet | 14 custom commands | + +### 2. Datum Creation System (16 Functions) + +**Custom Attachment System** - Replaces FreeCAD's unreliable vanilla attachment: +- Calculates placement directly from source geometry +- Stores source references in `ZTools_SourceRefs` (JSON) +- Uses `MapMode='Deactivated'` to prevent interference +- Stores creation parameters in `ZTools_Params` for recalculation + +#### Datum Planes (7 modes) +| Mode | Function | Parameters | +|------|----------|------------| +| Offset from Face | `plane_offset_from_face()` | distance (mm) | +| Offset from Plane | `plane_offset_from_plane()` | distance (mm) | +| Midplane | `plane_midplane()` | 2 parallel faces | +| 3 Points | `plane_from_3_points()` | 3 vertices | +| Normal to Edge | `plane_normal_to_edge()` | parameter (0.0-1.0) | +| Angled | `plane_angled()` | angle (degrees) | +| Tangent to Cylinder | `plane_tangent_to_cylinder()` | angle (degrees) | + +#### Datum Axes (4 modes) +| Mode | Function | Parameters | +|------|----------|------------| +| 2 Points | `axis_from_2_points()` | 2 vertices | +| From Edge | `axis_from_edge()` | linear edge | +| Cylinder Center | `axis_cylinder_center()` | cylindrical face | +| Plane Intersection | `axis_intersection_planes()` | 2 planes | + +#### Datum Points (5 modes) +| Mode | Function | Parameters | +|------|----------|------------| +| At Vertex | `point_at_vertex()` | vertex | +| XYZ Coordinates | `point_at_coordinates()` | x, y, z | +| On Edge | `point_on_edge()` | parameter (0.0-1.0) | +| Face Center | `point_center_of_face()` | face | +| Circle Center | `point_center_of_circle()` | circular edge | + +**Datum Creator GUI:** +- Auto-detection of 15+ creation modes based on selection +- Manual mode override +- Spreadsheet linking option +- Custom naming +- Real-time selection observer + +### 3. Pattern Tools (3 Commands) + +| Command | Description | Status | +|---------|-------------|--------| +| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation per instance | Complete | +| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Complete | +| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Complete | + +**Assembly Pattern Features:** +- Multi-component selection via table UI +- Creates as Links (recommended) or copies +- Direction/axis presets or custom vectors +- Spacing modes: Total Length or Fixed Spacing +- Angle modes: Full Circle or Custom Angle +- Auto-detects parent assembly container + +### 4. Spreadsheet Formatting (9 Commands) + +| Command | Description | Status | +|---------|-------------|--------| +| `ZTools_SpreadsheetStyleBold` | Toggle bold | Complete | +| `ZTools_SpreadsheetStyleItalic` | Toggle italic | Complete | +| `ZTools_SpreadsheetStyleUnderline` | Toggle underline | Complete | +| `ZTools_SpreadsheetAlignLeft` | Left align | Complete | +| `ZTools_SpreadsheetAlignCenter` | Center align | Complete | +| `ZTools_SpreadsheetAlignRight` | Right align | Complete | +| `ZTools_SpreadsheetBgColor` | Background color picker | Complete | +| `ZTools_SpreadsheetTextColor` | Text color picker | Complete | +| `ZTools_SpreadsheetQuickAlias` | Auto-create alias from label | Complete | + +### 5. Enhanced Features + +| Command | Description | Status | +|---------|-------------|--------| +| `ZTools_EnhancedPocket` | Pocket with "Flip Side to Cut" (SOLIDWORKS-style) | 90% Complete | + +**Flip Side to Cut:** Removes material OUTSIDE the sketch profile instead of inside, using Boolean Common operation. + +### 6. Theme System (Catppuccin Mocha) + +- Complete Qt StyleSheet (QSS) for entire FreeCAD interface +- 26-color palette consistently applied +- 50+ widget types styled +- FreeCAD-specific widgets: PropertyEditor, Python Console, Spreadsheet +- Spreadsheet colors auto-applied on workbench activation + +### 7. Icon System (33 Icons) + +All icons use Catppuccin Mocha palette: +- Workbench icon +- Datum icons (planes, axes, points - 13 total) +- Pattern icons (3 total) +- Pocket icons (2 total) +- Assembly pattern icons (2 total) +- Spreadsheet formatting icons (9 total) + +--- + +## Known Gaps & Incomplete Features + +### Critical (Must Fix) + +| Issue | Location | Description | Priority | +|-------|----------|-------------|----------| +| Datum Manager stub | `datum_commands.py:853` | Placeholder only - needs full implementation | High | +| Datum edit recalculation | `datum_viewprovider.py:351,355,359` | Parameter changes don't recalculate placement from source geometry | High | + +### Non-Critical (Should Fix) + +| Issue | Location | Description | Priority | +|-------|----------|-------------|----------| +| Enhanced Pocket incomplete | `pocket_commands.py` | Taper angle disabled for flipped pockets | Medium | +| Pocket execution logic | `pocket_commands.py` | UI complete but execution needs verification | Medium | + +### Future Enhancements (Nice to Have) + +| Feature | Description | Priority | +|---------|-------------|----------| +| Curve-driven pattern | Sweep instances along spline | Low | +| Fill pattern | Populate region with instances | Low | +| Variable spacing pattern | Non-uniform spacing | Low | +| Enhanced Pad | Multi-body support, draft angles | Low | +| Body operations | Split, combine, shell improvements | Low | + +--- + +## FreeCAD Ecosystem Alignment + +### FreeCAD 1.0 (November 2024) - Current Target + +**Key Features ZTools Leverages:** +- **TNP Mitigation:** Topological Naming Problem largely resolved +- **Integrated Assembly Workbench:** Ondsel's assembly system now core +- **Material System Overhaul:** New material handling +- **UI/UX Improvements:** Dark/light themes, selection filters + +**ZTools Alignment:** +- Custom attachment system complements TNP fix +- Full integration with native Assembly workbench +- Catppuccin theme extends FreeCAD's theming + +### FreeCAD 1.1 (Expected Late 2025) + +**Planned Features:** +- New Transform Manipulator +- UI Material Rendering Improvements +- Continued TNP refinement for Sketcher/PartDesign + +**ZTools Opportunities:** +- Watch for new Assembly API standardization +- Monitor Sketcher improvements for datum integration + +### FreeCAD Strategic Priorities (from Roadmap) + +| FreeCAD Priority | ZTools Alignment | +|------------------|------------------| +| Model Stability | Custom attachment system prevents fragile models | +| Assembly Integration | Full native Assembly command exposure | +| Flatten Learning Curve | Unified toolbar consolidation | +| UI Modernization | Catppuccin Mocha theme | +| Streamlined Workflow | Quick formatting toolbars, auto-detection | + +### Ondsel Contributions (Note: Ondsel shut down October 2025) + +Ondsel's contributions now maintained by FreeCAD community: +- Assembly workbench (ZTools integrates) +- VarSets custom properties (potential future integration) +- Sketcher improvements + +--- + +## Development Roadmap + +### Phase 1: v0.3.x - Stability & Completion (Current) + +**Timeline:** Q1 2026 + +| Task | Status | Priority | +|------|--------|----------| +| Complete Datum Manager GUI | Not Started | High | +| Implement datum parameter recalculation | Not Started | High | +| Verify Enhanced Pocket execution | Partial | Medium | +| Add comprehensive test coverage | Not Started | Medium | +| Documentation completion | In Progress | Medium | + +### Phase 2: v0.4.0 - Polish & UX + +**Timeline:** Q2 2026 + +| Task | Description | +|------|-------------| +| Keyboard shortcuts | Add hotkeys for common operations | +| Context menus | Right-click menus in 3D view | +| Undo/redo improvements | Better transaction naming | +| Error handling | User-friendly error messages | +| Preferences panel | ZTools configuration UI | + +### Phase 3: v0.5.0 - Advanced Features + +**Timeline:** Q3 2026 + +| Task | Description | +|------|-------------| +| Curve-driven pattern | Pattern along splines | +| Enhanced Pad | Draft angles, lip/groove | +| Body operations | Split, combine, shell | +| Datum freeze/update | Control source geometry updates | + +### Phase 4: v1.0.0 - Production Release + +**Timeline:** Q4 2026 + +| Task | Description | +|------|-------------| +| Full test suite | Automated testing | +| User documentation | Complete user guide | +| Video tutorials | Getting started series | +| FreeCAD Addon Manager | Official listing | + +--- + +## Technical Architecture + +### Directory Structure + +``` +ztools/ +├── Init.py # Startup (non-GUI) +├── InitGui.py # Workbench registration +└── ztools/ + ├── __init__.py + ├── commands/ # GUI commands + │ ├── __init__.py + │ ├── datum_commands.py # Datum Creator/Manager + │ ├── datum_viewprovider.py # Custom ViewProvider + │ ├── pattern_commands.py # Rotated Linear Pattern + │ ├── pocket_commands.py # Enhanced Pocket + │ ├── assembly_pattern_commands.py # Assembly patterns + │ └── spreadsheet_commands.py # Spreadsheet formatting + ├── datums/ # Core datum library + │ ├── __init__.py + │ └── core.py # 16 datum functions + └── resources/ # Assets + ├── __init__.py + ├── icons.py # 33 SVG icons + ├── theme.py # Catppuccin QSS + └── icons/ # Generated SVG files +``` + +### Key Design Patterns + +1. **Command Pattern:** All tools follow FreeCAD's `GetResources()`, `Activated()`, `IsActive()` pattern +2. **Task Panel Pattern:** Complex UIs use `QWidget` with selection observers +3. **Feature Python Pattern:** Custom features use `Part::FeaturePython` with ViewProvider +4. **Metadata System:** JSON properties for tracking ZTools objects + +### Metadata Properties + +All ZTools objects have: +- `ZTools_Type`: Feature type identifier +- `ZTools_Params`: JSON creation parameters +- `ZTools_SourceRefs`: JSON source geometry references + +--- + +## File Reference + +| File | Purpose | Lines | +|------|---------|-------| +| `InitGui.py` | Workbench registration, toolbars, menus | ~330 | +| `datums/core.py` | 16 datum creation functions | ~1300 | +| `commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 | +| `commands/datum_viewprovider.py` | Custom ViewProvider, edit panel | ~400 | +| `commands/pattern_commands.py` | Rotated Linear Pattern | ~206 | +| `commands/pocket_commands.py` | Enhanced Pocket | ~600 | +| `commands/assembly_pattern_commands.py` | Assembly patterns | ~580 | +| `commands/spreadsheet_commands.py` | Spreadsheet formatting | ~480 | +| `resources/icons.py` | 33 SVG icon definitions | ~540 | +| `resources/theme.py` | Catppuccin Mocha QSS | ~1500 | + +**Total:** ~6,400+ lines of code + +--- + +## Statistics Summary + +| Category | Count | +|----------|-------| +| Command Files | 6 | +| Command Classes | 24+ | +| Datum Creation Functions | 16 | +| Icons Defined | 33 | +| Toolbars Registered | 17 | +| Menu Hierarchies | 7 major | +| Native Commands Exposed | 66 | +| Custom ZTools Commands | 14 | +| Theme Colors | 26 | +| Styled Widget Types | 50+ | + +--- + +## Contributing + +ZTools follows FreeCAD's contribution guidelines. Key areas needing help: + +1. **Testing:** Manual testing on different platforms +2. **Documentation:** User guides and tutorials +3. **Translations:** Internationalization support +4. **Bug Reports:** Issue tracking and reproduction + +--- + +## License + +LGPL-3.0-or-later + +Compatible with FreeCAD's licensing model. + +--- + +## Changelog + +### v0.3.0 (2026-01-25) +- Added zSpreadsheet module with 9 formatting commands +- Added Spreadsheet workbench integration (6 native commands) +- Added 9 spreadsheet formatting icons + +### v0.2.0 (2026-01-25) +- Added Assembly workbench integration (21 native commands) +- Added Assembly Linear Pattern tool +- Added Assembly Polar Pattern tool +- Added assembly pattern icons + +### v0.1.0 (2026-01-24) +- Initial release +- Custom attachment system for datums +- 16 datum creation functions +- Datum Creator GUI with auto-detection +- Rotated Linear Pattern +- Enhanced Pocket (partial) +- Catppuccin Mocha theme +- 21 initial icons diff --git a/ztools/InitGui.py b/ztools/InitGui.py index 2507c31..6f2c785 100644 --- a/ztools/InitGui.py +++ b/ztools/InitGui.py @@ -60,10 +60,30 @@ class ZToolsWorkbench(Gui.Workbench): if hasattr(sketcher_wb, "Initialize"): sketcher_wb.Initialize() - except Exception as e: - App.Console.PrintWarning(f"Could not initialize PartDesign/Sketcher: {e}\n") + # Initialize Assembly workbench if available (FreeCAD 1.0+) + if "AssemblyWorkbench" in wb_list: + asm_wb = Gui.getWorkbench("AssemblyWorkbench") + if hasattr(asm_wb, "Initialize"): + asm_wb.Initialize() - from ztools.commands import datum_commands, pattern_commands, pocket_commands + # Initialize Spreadsheet workbench if available + if "SpreadsheetWorkbench" in wb_list: + ss_wb = Gui.getWorkbench("SpreadsheetWorkbench") + if hasattr(ss_wb, "Initialize"): + ss_wb.Initialize() + + except Exception as e: + App.Console.PrintWarning( + f"Could not initialize PartDesign/Sketcher/Assembly/Spreadsheet: {e}\n" + ) + + from ztools.commands import ( + assembly_pattern_commands, + datum_commands, + pattern_commands, + pocket_commands, + spreadsheet_commands, + ) # ===================================================================== # PartDesign Structure Tools @@ -176,6 +196,74 @@ class ZToolsWorkbench(Gui.Workbench): "ZTools_EnhancedPocket", ] + # ===================================================================== + # Assembly Workbench Tools (FreeCAD 1.0+) + # ===================================================================== + self.assembly_structure_tools = [ + "Assembly_CreateAssembly", + "Assembly_InsertLink", + "Assembly_InsertNewPart", + ] + + self.assembly_joint_tools = [ + "Assembly_CreateJointFixed", + "Assembly_CreateJointRevolute", + "Assembly_CreateJointCylindrical", + "Assembly_CreateJointSlider", + "Assembly_CreateJointBall", + "Assembly_CreateJointDistance", + "Assembly_CreateJointParallel", + "Assembly_CreateJointPerpendicular", + "Assembly_CreateJointAngle", + "Assembly_CreateJointRackPinion", + "Assembly_CreateJointScrew", + "Assembly_CreateJointGears", + "Assembly_CreateJointBelt", + ] + + self.assembly_management_tools = [ + "Assembly_ToggleGrounded", + "Assembly_SolveAssembly", + "Assembly_CreateView", + "Assembly_CreateBom", + "Assembly_ExportASMT", + ] + + # ===================================================================== + # ZTools Assembly Pattern Tools + # ===================================================================== + self.ztools_assembly_tools = [ + "ZTools_AssemblyLinearPattern", + "ZTools_AssemblyPolarPattern", + ] + + # ===================================================================== + # Spreadsheet Workbench Tools + # ===================================================================== + self.spreadsheet_tools = [ + "Spreadsheet_CreateSheet", + "Spreadsheet_Import", + "Spreadsheet_Export", + "Spreadsheet_SetAlias", + "Spreadsheet_MergeCells", + "Spreadsheet_SplitCell", + ] + + # ===================================================================== + # ZTools Spreadsheet Formatting Tools + # ===================================================================== + self.ztools_spreadsheet_tools = [ + "ZTools_SpreadsheetStyleBold", + "ZTools_SpreadsheetStyleItalic", + "ZTools_SpreadsheetStyleUnderline", + "ZTools_SpreadsheetAlignLeft", + "ZTools_SpreadsheetAlignCenter", + "ZTools_SpreadsheetAlignRight", + "ZTools_SpreadsheetBgColor", + "ZTools_SpreadsheetTextColor", + "ZTools_SpreadsheetQuickAlias", + ] + # ===================================================================== # Append Toolbars # ===================================================================== @@ -189,9 +277,15 @@ class ZToolsWorkbench(Gui.Workbench): self.appendToolbar("Transformations", self.transformation_tools) self.appendToolbar("Dress-Up", self.dressup_tools) self.appendToolbar("Boolean", self.boolean_tools) + self.appendToolbar("Assembly", self.assembly_structure_tools) + self.appendToolbar("Assembly Joints", self.assembly_joint_tools) + self.appendToolbar("Assembly Management", self.assembly_management_tools) self.appendToolbar("ztools Datums", self.ztools_datum_tools) self.appendToolbar("ztools Patterns", self.ztools_pattern_tools) self.appendToolbar("ztools Features", self.ztools_pocket_tools) + self.appendToolbar("ztools Assembly", self.ztools_assembly_tools) + self.appendToolbar("Spreadsheet", self.spreadsheet_tools) + self.appendToolbar("ztools Spreadsheet", self.ztools_spreadsheet_tools) # ===================================================================== # Append Menus @@ -209,17 +303,32 @@ class ZToolsWorkbench(Gui.Workbench): self.appendMenu(["PartDesign", "Transformations"], self.transformation_tools) self.appendMenu(["PartDesign", "Dress-Up"], self.dressup_tools) self.appendMenu(["PartDesign", "Boolean"], self.boolean_tools) + self.appendMenu(["Assembly", "Structure"], self.assembly_structure_tools) + self.appendMenu(["Assembly", "Joints"], self.assembly_joint_tools) + self.appendMenu(["Assembly", "Management"], self.assembly_management_tools) + self.appendMenu(["Spreadsheet", "Edit"], self.spreadsheet_tools) + self.appendMenu(["Spreadsheet", "Format"], self.ztools_spreadsheet_tools) self.appendMenu( "ztools", self.ztools_datum_tools + self.ztools_pattern_tools - + self.ztools_pocket_tools, + + self.ztools_pocket_tools + + self.ztools_assembly_tools + + self.ztools_spreadsheet_tools, ) App.Console.PrintMessage("ztools workbench initialized\n") def Activated(self): """Called when workbench is activated.""" + # Apply Catppuccin Mocha colors to Spreadsheet preferences + try: + from ztools.resources.theme import apply_spreadsheet_colors + + apply_spreadsheet_colors() + except Exception as e: + App.Console.PrintWarning(f"Could not apply spreadsheet colors: {e}\n") + App.Console.PrintMessage("ztools workbench activated\n") def Deactivated(self): diff --git a/ztools/ztools/commands/__init__.py b/ztools/ztools/commands/__init__.py index 70d804a..8a1b211 100644 --- a/ztools/ztools/commands/__init__.py +++ b/ztools/ztools/commands/__init__.py @@ -1,9 +1,18 @@ # ztools/commands - GUI commands -from . import datum_commands, datum_viewprovider, pattern_commands, pocket_commands +from . import ( + assembly_pattern_commands, + datum_commands, + datum_viewprovider, + pattern_commands, + pocket_commands, + spreadsheet_commands, +) __all__ = [ "datum_commands", "datum_viewprovider", "pattern_commands", "pocket_commands", + "assembly_pattern_commands", + "spreadsheet_commands", ] diff --git a/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc b/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc index 17471f63cbcf7c5b3f60ab71bbcf770cb324331a..0aa032a19a5e2b5a952e4fac7b36db608742dd01 100644 GIT binary patch delta 223 zcmcb|bd;IzG%qg~0}ym6mSyHKPUMpa(E;+NGo&!2Fy=7iGDb1xGDR`vGDk7zvP7}u zvPQAyvPH2mGNd!4FfC$?Vy|S@WO>O5)S}6DOER&zI5jsZr!u}Ev7{umC@(%aKQ}iq zFQs^5xVz;ok>Y}))WnqHjMUT;xQr%S5erav5i5vb0}<>Xf*C|`00}=$j)@D!*dd}E q6SuT!@Bu|Z78Z*Ei4V+-jEvt|*cq9=GXRNCLhRg3jqF7{Kq&zER5NS< delta 139 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 1 else 0 + else: + # Spacing mode + spacing = length_value + + use_links = self.link_checkbox.isChecked() + hide_original = self.hide_original_checkbox.isChecked() + + # Find parent assembly + assembly = self._find_parent_assembly(self.selected_components[0]) + + doc.openTransaction("Assembly Linear Pattern") + + try: + created_objects = [] + + for comp in self.selected_components: + # Get base placement + if hasattr(comp, "Placement"): + base_placement = comp.Placement + else: + base_placement = App.Placement() + + # Create pattern instances (skip i=0 as that's the original) + for i in range(1, occurrences): + offset = direction * spacing * i + new_placement = App.Placement( + base_placement.Base + offset, + base_placement.Rotation, + ) + + if use_links: + # Create link to original + if comp.TypeId == "App::Link": + # Link to the linked object + link_target = comp.LinkedObject + else: + link_target = comp + + new_obj = doc.addObject("App::Link", f"{comp.Label}_Pattern{i}") + new_obj.LinkedObject = link_target + new_obj.Placement = new_placement + else: + # Create copy + new_obj = doc.copyObject(comp, False) + new_obj.Label = f"{comp.Label}_Pattern{i}" + new_obj.Placement = new_placement + + # Add to assembly if found + if assembly and hasattr(assembly, "addObject"): + assembly.addObject(new_obj) + + created_objects.append(new_obj) + + # Hide original if requested + if hide_original and hasattr(comp, "ViewObject"): + comp.ViewObject.Visibility = False + + doc.commitTransaction() + doc.recompute() + + App.Console.PrintMessage( + f"Created {len(created_objects)} pattern instances\n" + ) + + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to create pattern: {e}\n") + return False + + return True + + def reject(self): + """Cancel the operation.""" + Gui.Selection.removeObserver(self.observer) + return True + + def _find_parent_assembly(self, obj): + """Find the parent assembly of an object.""" + doc = obj.Document + + for candidate in doc.Objects: + if candidate.TypeId == "Assembly::AssemblyObject": + # Check if obj is in this assembly's group + if hasattr(candidate, "Group"): + if obj in candidate.Group: + return candidate + # Check nested + for member in candidate.Group: + if hasattr(member, "Group") and obj in member.Group: + return candidate + + return None + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + + +class AssemblyLinearPatternCommand: + """Command to create a linear pattern of assembly components.""" + + def GetResources(self): + return { + "Pixmap": get_icon("assembly_linear_pattern"), + "MenuText": "Assembly Linear Pattern", + "ToolTip": "Create a linear pattern of assembly components", + } + + def IsActive(self): + return App.ActiveDocument is not None + + def Activated(self): + panel = AssemblyLinearPatternTaskPanel() + Gui.Control.showDialog(panel) + + +# ============================================================================= +# Assembly Polar Pattern +# ============================================================================= + + +class AssemblyPolarPatternTaskPanel: + """Task panel for creating polar patterns of assembly components.""" + + def __init__(self): + self.form = QtGui.QWidget() + self.setup_ui() + self.setup_selection_observer() + self.selected_components = [] + self.axis_selection = None + + def setup_ui(self): + layout = QtGui.QVBoxLayout(self.form) + + # Component selection section + component_group = QtGui.QGroupBox("Components") + component_layout = QtGui.QVBoxLayout(component_group) + + # Selection table + self.component_table = QtGui.QTableWidget() + self.component_table.setColumnCount(2) + self.component_table.setHorizontalHeaderLabels(["Component", "Remove"]) + self.component_table.horizontalHeader().setStretchLastSection(False) + self.component_table.horizontalHeader().setSectionResizeMode( + 0, QtGui.QHeaderView.Stretch + ) + self.component_table.horizontalHeader().setSectionResizeMode( + 1, QtGui.QHeaderView.Fixed + ) + self.component_table.setColumnWidth(1, 60) + self.component_table.setMaximumHeight(100) + component_layout.addWidget(self.component_table) + + hint_label = QtGui.QLabel("Select assembly components in the 3D view") + hint_label.setStyleSheet("color: gray; font-style: italic;") + component_layout.addWidget(hint_label) + + layout.addWidget(component_group) + + # Axis section + axis_group = QtGui.QGroupBox("Rotation Axis") + axis_layout = QtGui.QFormLayout(axis_group) + + # Axis definition mode + self.axis_mode = QtGui.QComboBox() + self.axis_mode.addItems(["Custom Axis", "X Axis", "Y Axis", "Z Axis"]) + self.axis_mode.currentIndexChanged.connect(self.on_axis_mode_changed) + axis_layout.addRow("Axis:", self.axis_mode) + + # Custom axis direction + self.axis_widget = QtGui.QWidget() + axis_dir_layout = QtGui.QHBoxLayout(self.axis_widget) + axis_dir_layout.setContentsMargins(0, 0, 0, 0) + self.axis_x_spin = QtGui.QDoubleSpinBox() + self.axis_x_spin.setRange(-1, 1) + self.axis_x_spin.setValue(0) + self.axis_x_spin.setDecimals(3) + self.axis_x_spin.setSingleStep(0.1) + self.axis_y_spin = QtGui.QDoubleSpinBox() + self.axis_y_spin.setRange(-1, 1) + self.axis_y_spin.setValue(0) + self.axis_y_spin.setDecimals(3) + self.axis_y_spin.setSingleStep(0.1) + self.axis_z_spin = QtGui.QDoubleSpinBox() + self.axis_z_spin.setRange(-1, 1) + self.axis_z_spin.setValue(1) + self.axis_z_spin.setDecimals(3) + self.axis_z_spin.setSingleStep(0.1) + axis_dir_layout.addWidget(QtGui.QLabel("X:")) + axis_dir_layout.addWidget(self.axis_x_spin) + axis_dir_layout.addWidget(QtGui.QLabel("Y:")) + axis_dir_layout.addWidget(self.axis_y_spin) + axis_dir_layout.addWidget(QtGui.QLabel("Z:")) + axis_dir_layout.addWidget(self.axis_z_spin) + axis_layout.addRow("Direction:", self.axis_widget) + + # Center point + center_layout = QtGui.QHBoxLayout() + self.center_x_spin = QtGui.QDoubleSpinBox() + self.center_x_spin.setRange(-100000, 100000) + self.center_x_spin.setValue(0) + self.center_x_spin.setDecimals(3) + self.center_y_spin = QtGui.QDoubleSpinBox() + self.center_y_spin.setRange(-100000, 100000) + self.center_y_spin.setValue(0) + self.center_y_spin.setDecimals(3) + self.center_z_spin = QtGui.QDoubleSpinBox() + self.center_z_spin.setRange(-100000, 100000) + self.center_z_spin.setValue(0) + self.center_z_spin.setDecimals(3) + center_layout.addWidget(QtGui.QLabel("X:")) + center_layout.addWidget(self.center_x_spin) + center_layout.addWidget(QtGui.QLabel("Y:")) + center_layout.addWidget(self.center_y_spin) + center_layout.addWidget(QtGui.QLabel("Z:")) + center_layout.addWidget(self.center_z_spin) + axis_layout.addRow("Center:", center_layout) + + layout.addWidget(axis_group) + + # Pattern parameters section + params_group = QtGui.QGroupBox("Pattern Parameters") + params_layout = QtGui.QFormLayout(params_group) + + # Occurrences + self.occurrences_spin = QtGui.QSpinBox() + self.occurrences_spin.setRange(2, 100) + self.occurrences_spin.setValue(6) + params_layout.addRow("Occurrences:", self.occurrences_spin) + + # Angle mode + self.angle_mode = QtGui.QComboBox() + self.angle_mode.addItems(["Full Circle (360)", "Custom Angle"]) + self.angle_mode.currentIndexChanged.connect(self.on_angle_mode_changed) + params_layout.addRow("Mode:", self.angle_mode) + + # Angle value + self.angle_spin = QtGui.QDoubleSpinBox() + self.angle_spin.setRange(0.001, 360) + self.angle_spin.setValue(360) + self.angle_spin.setDecimals(2) + self.angle_spin.setSuffix(" deg") + self.angle_spin.setEnabled(False) + params_layout.addRow("Total Angle:", self.angle_spin) + + layout.addWidget(params_group) + + # Options section + options_group = QtGui.QGroupBox("Options") + options_layout = QtGui.QVBoxLayout(options_group) + + self.link_checkbox = QtGui.QCheckBox("Create as Links (recommended)") + self.link_checkbox.setChecked(True) + options_layout.addWidget(self.link_checkbox) + + self.hide_original_checkbox = QtGui.QCheckBox("Hide original components") + self.hide_original_checkbox.setChecked(False) + options_layout.addWidget(self.hide_original_checkbox) + + layout.addWidget(options_group) + + layout.addStretch() + + def setup_selection_observer(self): + """Set up observer to track selection changes.""" + + class SelectionObserver: + def __init__(self, panel): + self.panel = panel + + def addSelection(self, doc, obj, sub, pos): + self.panel.on_selection_added(doc, obj, sub) + + def removeSelection(self, doc, obj, sub): + self.panel.on_selection_removed(doc, obj, sub) + + def clearSelection(self, doc): + pass + + self.observer = SelectionObserver(self) + Gui.Selection.addObserver(self.observer) + + def on_selection_added(self, doc_name, obj_name, sub): + """Handle new selection.""" + doc = App.getDocument(doc_name) + if not doc: + return + + obj = doc.getObject(obj_name) + if not obj: + return + + # Check if this is an assembly component + if not self._is_valid_component(obj): + return + + if obj in self.selected_components: + return + + self.selected_components.append(obj) + self._update_table() + + def on_selection_removed(self, doc_name, obj_name, sub): + """Handle selection removal.""" + doc = App.getDocument(doc_name) + if not doc: + return + + obj = doc.getObject(obj_name) + if obj in self.selected_components: + self.selected_components.remove(obj) + self._update_table() + + def _is_valid_component(self, obj): + """Check if object is a valid assembly component.""" + valid_types = [ + "App::Link", + "App::LinkGroup", + "Part::Feature", + "PartDesign::Body", + "App::Part", + ] + return obj.TypeId in valid_types + + def _update_table(self): + """Update the component table.""" + self.component_table.setRowCount(len(self.selected_components)) + + for i, comp in enumerate(self.selected_components): + name_item = QtGui.QTableWidgetItem(comp.Label) + name_item.setFlags(name_item.flags() & ~QtCore.Qt.ItemIsEditable) + self.component_table.setItem(i, 0, name_item) + + remove_btn = QtGui.QPushButton("X") + remove_btn.setMaximumWidth(40) + remove_btn.clicked.connect( + lambda checked, idx=i: self._remove_component(idx) + ) + self.component_table.setCellWidget(i, 1, remove_btn) + + def _remove_component(self, index): + """Remove component from selection.""" + if 0 <= index < len(self.selected_components): + self.selected_components.pop(index) + self._update_table() + + def on_axis_mode_changed(self, index): + """Update axis inputs based on mode.""" + if index == 0: + # Custom axis + self.axis_widget.setEnabled(True) + else: + # Preset axis + self.axis_widget.setEnabled(False) + if index == 1: # X + self.axis_x_spin.setValue(1) + self.axis_y_spin.setValue(0) + self.axis_z_spin.setValue(0) + elif index == 2: # Y + self.axis_x_spin.setValue(0) + self.axis_y_spin.setValue(1) + self.axis_z_spin.setValue(0) + elif index == 3: # Z + self.axis_x_spin.setValue(0) + self.axis_y_spin.setValue(0) + self.axis_z_spin.setValue(1) + + def on_angle_mode_changed(self, index): + """Update angle input based on mode.""" + if index == 0: + # Full circle + self.angle_spin.setValue(360) + self.angle_spin.setEnabled(False) + else: + # Custom angle + self.angle_spin.setEnabled(True) + + def accept(self): + """Create the polar pattern.""" + Gui.Selection.removeObserver(self.observer) + + if not self.selected_components: + App.Console.PrintError("No components selected for pattern\n") + return False + + doc = App.ActiveDocument + if not doc: + return False + + # Get axis + axis = App.Vector( + self.axis_x_spin.value(), + self.axis_y_spin.value(), + self.axis_z_spin.value(), + ) + + if axis.Length < 1e-6: + App.Console.PrintError("Axis vector cannot be zero\n") + return False + + axis.normalize() + + # Get center + center = App.Vector( + self.center_x_spin.value(), + self.center_y_spin.value(), + self.center_z_spin.value(), + ) + + occurrences = self.occurrences_spin.value() + total_angle = self.angle_spin.value() + + # Calculate angle increment + if self.angle_mode.currentIndex() == 0: + # Full circle - don't include last (would overlap first) + angle_step = 360.0 / occurrences + else: + # Custom angle + angle_step = total_angle / (occurrences - 1) if occurrences > 1 else 0 + + use_links = self.link_checkbox.isChecked() + hide_original = self.hide_original_checkbox.isChecked() + + # Find parent assembly + assembly = self._find_parent_assembly(self.selected_components[0]) + + doc.openTransaction("Assembly Polar Pattern") + + try: + created_objects = [] + + for comp in self.selected_components: + # Get base placement + if hasattr(comp, "Placement"): + base_placement = comp.Placement + else: + base_placement = App.Placement() + + # Create pattern instances + for i in range(1, occurrences): + angle = angle_step * i + + # Create rotation around axis through center + rotation = App.Rotation(axis, angle) + + # Calculate new position + # Translate to origin, rotate, translate back + base_pos = base_placement.Base + relative_pos = base_pos - center + rotated_pos = rotation.multVec(relative_pos) + new_pos = rotated_pos + center + + # Combine rotations + new_rotation = rotation.multiply(base_placement.Rotation) + + new_placement = App.Placement(new_pos, new_rotation) + + if use_links: + if comp.TypeId == "App::Link": + link_target = comp.LinkedObject + else: + link_target = comp + + new_obj = doc.addObject("App::Link", f"{comp.Label}_Polar{i}") + new_obj.LinkedObject = link_target + new_obj.Placement = new_placement + else: + new_obj = doc.copyObject(comp, False) + new_obj.Label = f"{comp.Label}_Polar{i}" + new_obj.Placement = new_placement + + if assembly and hasattr(assembly, "addObject"): + assembly.addObject(new_obj) + + created_objects.append(new_obj) + + if hide_original and hasattr(comp, "ViewObject"): + comp.ViewObject.Visibility = False + + doc.commitTransaction() + doc.recompute() + + App.Console.PrintMessage( + f"Created {len(created_objects)} polar pattern instances\n" + ) + + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to create pattern: {e}\n") + return False + + return True + + def reject(self): + """Cancel the operation.""" + Gui.Selection.removeObserver(self.observer) + return True + + def _find_parent_assembly(self, obj): + """Find the parent assembly of an object.""" + doc = obj.Document + + for candidate in doc.Objects: + if candidate.TypeId == "Assembly::AssemblyObject": + if hasattr(candidate, "Group"): + if obj in candidate.Group: + return candidate + for member in candidate.Group: + if hasattr(member, "Group") and obj in member.Group: + return candidate + + return None + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + + +class AssemblyPolarPatternCommand: + """Command to create a polar pattern of assembly components.""" + + def GetResources(self): + return { + "Pixmap": get_icon("assembly_polar_pattern"), + "MenuText": "Assembly Polar Pattern", + "ToolTip": "Create a polar (circular) pattern of assembly components", + } + + def IsActive(self): + return App.ActiveDocument is not None + + def Activated(self): + panel = AssemblyPolarPatternTaskPanel() + Gui.Control.showDialog(panel) + + +# ============================================================================= +# Register Commands +# ============================================================================= + +Gui.addCommand("ZTools_AssemblyLinearPattern", AssemblyLinearPatternCommand()) +Gui.addCommand("ZTools_AssemblyPolarPattern", AssemblyPolarPatternCommand()) diff --git a/ztools/ztools/commands/spreadsheet_commands.py b/ztools/ztools/commands/spreadsheet_commands.py new file mode 100644 index 0000000..d5bf6ec --- /dev/null +++ b/ztools/ztools/commands/spreadsheet_commands.py @@ -0,0 +1,567 @@ +# ztools/commands/spreadsheet_commands.py +# Enhanced spreadsheet formatting tools for FreeCAD +# Provides quick formatting toolbar for cell styling + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore, QtGui + +from ztools.resources.icons import get_icon + +# ============================================================================= +# Helper Functions +# ============================================================================= + + +def get_active_spreadsheet(): + """Get the currently active spreadsheet object and its view. + + Returns: + tuple: (sheet_object, sheet_view) or (None, None) + """ + doc = App.ActiveDocument + if not doc: + return None, None + + # Get MDI area and active subwindow + main_window = Gui.getMainWindow() + mdi = main_window.centralWidget() + if not mdi: + return None, None + + subwindow = mdi.activeSubWindow() + if not subwindow: + return None, None + + # Get widget from subwindow + widget = subwindow.widget() + if not widget: + return None, None + + # Check if it's a spreadsheet view by looking for the table view + # FreeCAD's spreadsheet view contains a QTableView + table_view = None + if hasattr(widget, "findChild"): + table_view = widget.findChild(QtGui.QTableView) + + if not table_view: + # Try if widget itself is the table view + if isinstance(widget, QtGui.QTableView): + table_view = widget + else: + return None, None + + # Get the spreadsheet object from window title + # Window title format varies: "Spreadsheet" or "Spreadsheet - DocName" + title = subwindow.windowTitle() + sheet_name = title.split(" - ")[0].split(" : ")[0].strip() + + # Try to find the sheet object + sheet = doc.getObject(sheet_name) + if sheet and sheet.TypeId == "Spreadsheet::Sheet": + return sheet, table_view + + # Fallback: search for any spreadsheet object + for obj in doc.Objects: + if obj.TypeId == "Spreadsheet::Sheet": + return obj, table_view + + return None, None + + +def get_selected_cells(): + """Get list of selected cell addresses from active spreadsheet. + + Returns: + tuple: (sheet_object, list_of_cell_addresses) or (None, []) + """ + sheet, table_view = get_active_spreadsheet() + if not sheet or not table_view: + return None, [] + + # Get selection model + selection_model = table_view.selectionModel() + if not selection_model: + return sheet, [] + + indexes = selection_model.selectedIndexes() + if not indexes: + return sheet, [] + + cells = [] + for idx in indexes: + col = idx.column() + row = idx.row() + + # Convert to cell address (A1 notation) + # Handle columns beyond Z (AA, AB, etc.) + col_str = "" + temp_col = col + while temp_col >= 0: + col_str = chr(65 + (temp_col % 26)) + col_str + temp_col = temp_col // 26 - 1 + + cell_addr = f"{col_str}{row + 1}" + cells.append(cell_addr) + + return sheet, cells + + +def column_to_index(col_str): + """Convert column string (A, B, ..., Z, AA, AB, ...) to index.""" + result = 0 + for char in col_str: + result = result * 26 + (ord(char) - ord("A") + 1) + return result - 1 + + +# ============================================================================= +# Style Commands (Bold, Italic, Underline) +# ============================================================================= + + +class ZTools_SpreadsheetStyleBold: + """Toggle bold style on selected cells.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_bold"), + "MenuText": "Bold", + "ToolTip": "Toggle bold style on selected cells (Ctrl+B)", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Toggle Bold") + + try: + for cell in cells: + current = sheet.getStyle(cell) or "" + styles = set(s.strip() for s in current.split("|") if s.strip()) + + if "bold" in styles: + styles.discard("bold") + else: + styles.add("bold") + + new_style = "|".join(sorted(styles)) if styles else "" + sheet.setStyle(cell, new_style) + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to toggle bold: {e}\n") + + +class ZTools_SpreadsheetStyleItalic: + """Toggle italic style on selected cells.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_italic"), + "MenuText": "Italic", + "ToolTip": "Toggle italic style on selected cells (Ctrl+I)", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Toggle Italic") + + try: + for cell in cells: + current = sheet.getStyle(cell) or "" + styles = set(s.strip() for s in current.split("|") if s.strip()) + + if "italic" in styles: + styles.discard("italic") + else: + styles.add("italic") + + new_style = "|".join(sorted(styles)) if styles else "" + sheet.setStyle(cell, new_style) + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to toggle italic: {e}\n") + + +class ZTools_SpreadsheetStyleUnderline: + """Toggle underline style on selected cells.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_underline"), + "MenuText": "Underline", + "ToolTip": "Toggle underline style on selected cells (Ctrl+U)", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Toggle Underline") + + try: + for cell in cells: + current = sheet.getStyle(cell) or "" + styles = set(s.strip() for s in current.split("|") if s.strip()) + + if "underline" in styles: + styles.discard("underline") + else: + styles.add("underline") + + new_style = "|".join(sorted(styles)) if styles else "" + sheet.setStyle(cell, new_style) + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to toggle underline: {e}\n") + + +# ============================================================================= +# Alignment Commands +# ============================================================================= + + +class ZTools_SpreadsheetAlignLeft: + """Align cell content to the left.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_align_left"), + "MenuText": "Align Left", + "ToolTip": "Align selected cells to the left", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Align Left") + + try: + for cell in cells: + sheet.setAlignment(cell, "left") + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to align left: {e}\n") + + +class ZTools_SpreadsheetAlignCenter: + """Align cell content to the center.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_align_center"), + "MenuText": "Align Center", + "ToolTip": "Align selected cells to the center", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Align Center") + + try: + for cell in cells: + sheet.setAlignment(cell, "center") + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to align center: {e}\n") + + +class ZTools_SpreadsheetAlignRight: + """Align cell content to the right.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_align_right"), + "MenuText": "Align Right", + "ToolTip": "Align selected cells to the right", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Align Right") + + try: + for cell in cells: + sheet.setAlignment(cell, "right") + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to align right: {e}\n") + + +# ============================================================================= +# Color Commands +# ============================================================================= + + +class ZTools_SpreadsheetBgColor: + """Set background color of selected cells.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_bg_color"), + "MenuText": "Background Color", + "ToolTip": "Set background color of selected cells", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + # Show color picker dialog + color = QtGui.QColorDialog.getColor( + QtCore.Qt.white, Gui.getMainWindow(), "Select Background Color" + ) + + if not color.isValid(): + return + + doc = App.ActiveDocument + doc.openTransaction("Set Background Color") + + try: + # FreeCAD expects RGB as tuple of floats 0-1 + rgb = ( + color.redF(), + color.greenF(), + color.blueF(), + ) + + for cell in cells: + sheet.setBackground(cell, rgb) + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to set background color: {e}\n") + + +class ZTools_SpreadsheetTextColor: + """Set text color of selected cells.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_text_color"), + "MenuText": "Text Color", + "ToolTip": "Set text color of selected cells", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + # Show color picker dialog with default white for dark theme + initial_color = QtGui.QColor(205, 214, 244) # Catppuccin Mocha text color + color = QtGui.QColorDialog.getColor( + initial_color, Gui.getMainWindow(), "Select Text Color" + ) + + if not color.isValid(): + return + + doc = App.ActiveDocument + doc.openTransaction("Set Text Color") + + try: + # FreeCAD expects RGB as tuple of floats 0-1 + rgb = ( + color.redF(), + color.greenF(), + color.blueF(), + ) + + for cell in cells: + sheet.setForeground(cell, rgb) + + doc.commitTransaction() + doc.recompute() + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to set text color: {e}\n") + + +# ============================================================================= +# Utility Commands +# ============================================================================= + + +class ZTools_SpreadsheetQuickAlias: + """Create alias from cell content.""" + + def GetResources(self): + return { + "Pixmap": get_icon("spreadsheet_quick_alias"), + "MenuText": "Quick Alias", + "ToolTip": "Create alias for selected cell based on adjacent label cell", + } + + def IsActive(self): + sheet, cells = get_selected_cells() + return sheet is not None and len(cells) > 0 + + def Activated(self): + sheet, cells = get_selected_cells() + if not sheet or not cells: + App.Console.PrintWarning("No cells selected\n") + return + + doc = App.ActiveDocument + doc.openTransaction("Quick Alias") + + try: + aliases_created = 0 + + for cell in cells: + # Parse cell address + import re + + match = re.match(r"([A-Z]+)(\d+)", cell) + if not match: + continue + + col_str = match.group(1) + row = int(match.group(2)) + + # Get content of cell to the left (label cell) + col_idx = column_to_index(col_str) + if col_idx > 0: + # Convert back to column string + label_col_idx = col_idx - 1 + label_col_str = "" + temp = label_col_idx + while temp >= 0: + label_col_str = chr(65 + (temp % 26)) + label_col_str + temp = temp // 26 - 1 + + label_cell = f"{label_col_str}{row}" + label_content = sheet.getContents(label_cell) + + if label_content: + # Clean the label to make a valid alias + # Must be alphanumeric + underscore, start with letter + alias = "".join( + c if c.isalnum() or c == "_" else "_" + for c in label_content + c if c.isalnum() or c == "_" else "_" for c in label_content + ) + # Ensure it starts with a letter + if alias and not alias[0].isalpha(): + alias = "var_" + alias + # Truncate if too long + alias = alias[:30] + + if alias: + try: + sheet.setAlias(cell, alias) + aliases_created += 1 + except Exception as alias_err: + App.Console.PrintWarning( + f"Could not set alias '{alias}' for {cell}: {alias_err}\n" + ) + + doc.commitTransaction() + doc.recompute() + + if aliases_created > 0: + App.Console.PrintMessage(f"Created {aliases_created} alias(es)\n") + "No aliases created. Select value cells with labels to the left.\n" + ) + + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to create aliases: {e}\n") + + +# ============================================================================= +# Register Commands +# ============================================================================= + +Gui.addCommand("ZTools_SpreadsheetStyleBold", ZTools_SpreadsheetStyleBold()) +Gui.addCommand("ZTools_SpreadsheetStyleItalic", ZTools_SpreadsheetStyleItalic()) +Gui.addCommand("ZTools_SpreadsheetStyleUnderline", ZTools_SpreadsheetStyleUnderline()) +Gui.addCommand("ZTools_SpreadsheetAlignLeft", ZTools_SpreadsheetAlignLeft()) +Gui.addCommand("ZTools_SpreadsheetAlignCenter", ZTools_SpreadsheetAlignCenter()) +Gui.addCommand("ZTools_SpreadsheetAlignRight", ZTools_SpreadsheetAlignRight()) +Gui.addCommand("ZTools_SpreadsheetBgColor", ZTools_SpreadsheetBgColor()) +Gui.addCommand("ZTools_SpreadsheetTextColor", ZTools_SpreadsheetTextColor()) +Gui.addCommand("ZTools_SpreadsheetQuickAlias", ZTools_SpreadsheetQuickAlias()) diff --git a/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc b/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc index 5120d08ac8d6f4448c8390a8947d611d3badca50..7d6bbd20397c51920a1423a71434c498d10a9768 100644 GIT binary patch delta 7577 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 S5pNMYX};I=IRzgHdH)9sW_UCJ delta 1401 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 delta 8364 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 O4}E0g|K9$Pzx8JslrH`N diff --git a/ztools/ztools/resources/icons.py b/ztools/ztools/resources/icons.py index cd711fe..c62db92 100644 --- a/ztools/ztools/resources/icons.py +++ b/ztools/ztools/resources/icons.py @@ -294,6 +294,136 @@ ICON_POCKET_FLIPPED_SVG = f''' ''' +# Assembly Linear Pattern icon - components along a line +ICON_ASSEMBLY_LINEAR_PATTERN_SVG = f''' + + + + + + + + + + + + + + + 1-2-3 +''' + +# Assembly Polar Pattern icon - components around a center +ICON_ASSEMBLY_POLAR_PATTERN_SVG = f''' + + + + + + + + + + + + + + + + + + + + + + + + + +''' + +# ============================================================================= +# Spreadsheet Icons +# ============================================================================= + +# Bold text icon +ICON_SPREADSHEET_BOLD_SVG = f''' + + B +''' + +# Italic text icon +ICON_SPREADSHEET_ITALIC_SVG = f''' + + I +''' + +# Underline text icon +ICON_SPREADSHEET_UNDERLINE_SVG = f''' + + U + +''' + +# Align left icon +ICON_SPREADSHEET_ALIGN_LEFT_SVG = f''' + + + + + +''' + +# Align center icon +ICON_SPREADSHEET_ALIGN_CENTER_SVG = f''' + + + + + +''' + +# Align right icon +ICON_SPREADSHEET_ALIGN_RIGHT_SVG = f''' + + + + + +''' + +# Background color icon (paint bucket) +ICON_SPREADSHEET_BG_COLOR_SVG = f''' + + + + + + + +''' + +# Text color icon (A with color bar) +ICON_SPREADSHEET_TEXT_COLOR_SVG = f''' + + + A + + +''' + +# Quick alias icon (tag/label) +ICON_SPREADSHEET_QUICK_ALIAS_SVG = f''' + + + + + + + + +''' + # ============================================================================= # Icon Registry - Base64 encoded for FreeCAD @@ -331,6 +461,17 @@ def get_icon(name: str) -> str: "rotated_pattern": ICON_ROTATED_PATTERN_SVG, "pocket_enhanced": ICON_POCKET_ENHANCED_SVG, "pocket_flipped": ICON_POCKET_FLIPPED_SVG, + "assembly_linear_pattern": ICON_ASSEMBLY_LINEAR_PATTERN_SVG, + "assembly_polar_pattern": ICON_ASSEMBLY_POLAR_PATTERN_SVG, + "spreadsheet_bold": ICON_SPREADSHEET_BOLD_SVG, + "spreadsheet_italic": ICON_SPREADSHEET_ITALIC_SVG, + "spreadsheet_underline": ICON_SPREADSHEET_UNDERLINE_SVG, + "spreadsheet_align_left": ICON_SPREADSHEET_ALIGN_LEFT_SVG, + "spreadsheet_align_center": ICON_SPREADSHEET_ALIGN_CENTER_SVG, + "spreadsheet_align_right": ICON_SPREADSHEET_ALIGN_RIGHT_SVG, + "spreadsheet_bg_color": ICON_SPREADSHEET_BG_COLOR_SVG, + "spreadsheet_text_color": ICON_SPREADSHEET_TEXT_COLOR_SVG, + "spreadsheet_quick_alias": ICON_SPREADSHEET_QUICK_ALIAS_SVG, } if name not in icons: @@ -377,6 +518,17 @@ def save_icons_to_disk(directory: str): "ztools_rotated_pattern": ICON_ROTATED_PATTERN_SVG, "ztools_pocket_enhanced": ICON_POCKET_ENHANCED_SVG, "ztools_pocket_flipped": ICON_POCKET_FLIPPED_SVG, + "ztools_assembly_linear_pattern": ICON_ASSEMBLY_LINEAR_PATTERN_SVG, + "ztools_assembly_polar_pattern": ICON_ASSEMBLY_POLAR_PATTERN_SVG, + "ztools_spreadsheet_bold": ICON_SPREADSHEET_BOLD_SVG, + "ztools_spreadsheet_italic": ICON_SPREADSHEET_ITALIC_SVG, + "ztools_spreadsheet_underline": ICON_SPREADSHEET_UNDERLINE_SVG, + "ztools_spreadsheet_align_left": ICON_SPREADSHEET_ALIGN_LEFT_SVG, + "ztools_spreadsheet_align_center": ICON_SPREADSHEET_ALIGN_CENTER_SVG, + "ztools_spreadsheet_align_right": ICON_SPREADSHEET_ALIGN_RIGHT_SVG, + "ztools_spreadsheet_bg_color": ICON_SPREADSHEET_BG_COLOR_SVG, + "ztools_spreadsheet_text_color": ICON_SPREADSHEET_TEXT_COLOR_SVG, + "ztools_spreadsheet_quick_alias": ICON_SPREADSHEET_QUICK_ALIAS_SVG, } for name, svg in icons.items(): diff --git a/ztools/ztools/resources/icons/ztools_assembly_linear_pattern.svg b/ztools/ztools/resources/icons/ztools_assembly_linear_pattern.svg new file mode 100644 index 0000000..c4ff109 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_assembly_linear_pattern.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + 1-2-3 + \ No newline at end of file diff --git a/ztools/ztools/resources/icons/ztools_assembly_polar_pattern.svg b/ztools/ztools/resources/icons/ztools_assembly_polar_pattern.svg new file mode 100644 index 0000000..8bcd089 --- /dev/null +++ b/ztools/ztools/resources/icons/ztools_assembly_polar_pattern.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ztools/ztools/resources/theme.py b/ztools/ztools/resources/theme.py index a7c2652..32d2692 100644 --- a/ztools/ztools/resources/theme.py +++ b/ztools/ztools/resources/theme.py @@ -1218,6 +1218,43 @@ SpreadsheetGui--SheetTableView QHeaderView::section {{ padding: 4px; }} +SpreadsheetGui--SheetTableView::item {{ + color: {_text}; + background-color: {_base}; +}} + +SpreadsheetGui--SheetTableView::item:selected {{ + color: {_text}; + background-color: {_surface1}; +}} + +SpreadsheetGui--SheetTableView::item:focus {{ + color: {_text}; +}} + +/* Spreadsheet cell editor */ +SpreadsheetGui--SheetTableView QLineEdit {{ + color: {_text}; + background-color: {_surface0}; + border: 1px solid {_mauve}; + selection-background-color: {_mauve}; + selection-color: {_crust}; +}} + +/* Spreadsheet module - additional selectors for cell content */ +Spreadsheet--SheetModel {{ + color: {_text}; +}} + +SpreadsheetGui--SheetView {{ + background-color: {_base}; + color: {_text}; +}} + +SpreadsheetGui--SheetView::item {{ + color: {_text}; +}} + /* Python Console */ Gui--PythonConsole {{ background-color: {_crust}; @@ -1304,3 +1341,53 @@ def get_stylesheet() -> str: str: Complete QSS stylesheet. """ return generate_stylesheet() + + +def apply_spreadsheet_colors(): + """Apply Catppuccin Mocha colors to FreeCAD Spreadsheet preferences. + + FreeCAD's Spreadsheet module uses internal preference settings for colors + rather than pure QSS styling. This function sets those preferences to + match the Catppuccin Mocha theme. + """ + import FreeCAD as App + + # Get the parameter group for Spreadsheet colors + params = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Spreadsheet") + + # Convert hex color to unsigned int (FreeCAD stores colors as unsigned 32-bit RGBA) + def hex_to_rgba_uint(hex_color: str) -> int: + """Convert hex color (#RRGGBB) to FreeCAD's unsigned int format (0xRRGGBBAA).""" + hex_color = hex_color.lstrip("#") + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + a = 255 # Full opacity + # FreeCAD uses RGBA format as unsigned int + return (r << 24) | (g << 16) | (b << 8) | a + + # Set text/foreground color to Catppuccin Mocha "text" (#cdd6f4) + text_color = hex_to_rgba_uint(MOCHA["text"]) + params.SetUnsigned("TextColor", text_color) + + # Set background colors + bg_color = hex_to_rgba_uint(MOCHA["base"]) + params.SetUnsigned("BackgroundColor", bg_color) + + # Alternate row background + alt_bg_color = hex_to_rgba_uint(MOCHA["surface0"]) + params.SetUnsigned("AltBackgroundColor", alt_bg_color) + + # Alias text color (for cells with aliases) - use teal for distinction + alias_color = hex_to_rgba_uint(MOCHA["teal"]) + params.SetUnsigned("AliasedTextColor", alias_color) + + # Positive number color - green + positive_color = hex_to_rgba_uint(MOCHA["green"]) + params.SetUnsigned("PositiveNumberColor", positive_color) + + # Negative number color - red + negative_color = hex_to_rgba_uint(MOCHA["red"]) + params.SetUnsigned("NegativeNumberColor", negative_color) + + App.Console.PrintLog("ztools: Applied Catppuccin Mocha spreadsheet colors\n")