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 17471f6..0aa032a 100644 Binary files a/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc and b/ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc differ diff --git a/ztools/ztools/commands/__pycache__/assembly_pattern_commands.cpython-312.pyc b/ztools/ztools/commands/__pycache__/assembly_pattern_commands.cpython-312.pyc new file mode 100644 index 0000000..bc84e91 Binary files /dev/null and b/ztools/ztools/commands/__pycache__/assembly_pattern_commands.cpython-312.pyc differ diff --git a/ztools/ztools/commands/assembly_pattern_commands.py b/ztools/ztools/commands/assembly_pattern_commands.py new file mode 100644 index 0000000..d3b3790 --- /dev/null +++ b/ztools/ztools/commands/assembly_pattern_commands.py @@ -0,0 +1,787 @@ +# ztools/commands/assembly_pattern_commands.py +# Assembly patterning tools for FreeCAD 1.0+ Assembly workbench +# Creates linear and polar patterns of assembly components + +import math + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore, QtGui + +from ztools.resources.icons import get_icon + +# ============================================================================= +# Assembly Linear Pattern +# ============================================================================= + + +class AssemblyLinearPatternTaskPanel: + """Task panel for creating linear patterns of assembly components.""" + + def __init__(self): + self.form = QtGui.QWidget() + self.setup_ui() + self.setup_selection_observer() + self.selected_components = [] + + def setup_ui(self): + layout = QtGui.QVBoxLayout(self.form) + + # Component selection section + component_group = QtGui.QGroupBox("Components") + component_layout = QtGui.QVBoxLayout(component_group) + + # Selection table + self.component_table = QtGui.QTableWidget() + self.component_table.setColumnCount(2) + self.component_table.setHorizontalHeaderLabels(["Component", "Remove"]) + self.component_table.horizontalHeader().setStretchLastSection(False) + self.component_table.horizontalHeader().setSectionResizeMode( + 0, QtGui.QHeaderView.Stretch + ) + self.component_table.horizontalHeader().setSectionResizeMode( + 1, QtGui.QHeaderView.Fixed + ) + self.component_table.setColumnWidth(1, 60) + self.component_table.setMaximumHeight(120) + component_layout.addWidget(self.component_table) + + # Selection hint + hint_label = QtGui.QLabel("Select assembly components in the 3D view") + hint_label.setStyleSheet("color: gray; font-style: italic;") + component_layout.addWidget(hint_label) + + layout.addWidget(component_group) + + # Pattern parameters section + params_group = QtGui.QGroupBox("Pattern Parameters") + params_layout = QtGui.QFormLayout(params_group) + + # Direction + direction_layout = QtGui.QHBoxLayout() + self.dir_x_spin = QtGui.QDoubleSpinBox() + self.dir_x_spin.setRange(-1000, 1000) + self.dir_x_spin.setValue(1) + self.dir_x_spin.setDecimals(3) + self.dir_y_spin = QtGui.QDoubleSpinBox() + self.dir_y_spin.setRange(-1000, 1000) + self.dir_y_spin.setValue(0) + self.dir_y_spin.setDecimals(3) + self.dir_z_spin = QtGui.QDoubleSpinBox() + self.dir_z_spin.setRange(-1000, 1000) + self.dir_z_spin.setValue(0) + self.dir_z_spin.setDecimals(3) + direction_layout.addWidget(QtGui.QLabel("X:")) + direction_layout.addWidget(self.dir_x_spin) + direction_layout.addWidget(QtGui.QLabel("Y:")) + direction_layout.addWidget(self.dir_y_spin) + direction_layout.addWidget(QtGui.QLabel("Z:")) + direction_layout.addWidget(self.dir_z_spin) + params_layout.addRow("Direction:", direction_layout) + + # Occurrences + self.occurrences_spin = QtGui.QSpinBox() + self.occurrences_spin.setRange(2, 100) + self.occurrences_spin.setValue(3) + params_layout.addRow("Occurrences:", self.occurrences_spin) + + # Spacing mode + self.spacing_mode = QtGui.QComboBox() + self.spacing_mode.addItems(["Total Length", "Spacing"]) + self.spacing_mode.currentIndexChanged.connect(self.on_spacing_mode_changed) + params_layout.addRow("Mode:", self.spacing_mode) + + # Length/Spacing value + self.length_spin = QtGui.QDoubleSpinBox() + self.length_spin.setRange(0.001, 100000) + self.length_spin.setValue(100) + self.length_spin.setDecimals(3) + self.length_spin.setSuffix(" mm") + self.length_label = QtGui.QLabel("Total Length:") + params_layout.addRow(self.length_label, self.length_spin) + + layout.addWidget(params_group) + + # Options section + options_group = QtGui.QGroupBox("Options") + options_layout = QtGui.QVBoxLayout(options_group) + + self.link_checkbox = QtGui.QCheckBox("Create as Links (recommended)") + self.link_checkbox.setChecked(True) + self.link_checkbox.setToolTip( + "Links reference the original component, reducing file size" + ) + options_layout.addWidget(self.link_checkbox) + + self.hide_original_checkbox = QtGui.QCheckBox("Hide original components") + self.hide_original_checkbox.setChecked(False) + options_layout.addWidget(self.hide_original_checkbox) + + layout.addWidget(options_group) + + layout.addStretch() + + def setup_selection_observer(self): + """Set up observer to track selection changes.""" + + class SelectionObserver: + def __init__(self, panel): + self.panel = panel + + def addSelection(self, doc, obj, sub, pos): + self.panel.on_selection_added(doc, obj, sub) + + def removeSelection(self, doc, obj, sub): + self.panel.on_selection_removed(doc, obj, sub) + + def clearSelection(self, doc): + pass + + self.observer = SelectionObserver(self) + Gui.Selection.addObserver(self.observer) + + def on_selection_added(self, doc_name, obj_name, sub): + """Handle new selection.""" + doc = App.getDocument(doc_name) + if not doc: + return + + obj = doc.getObject(obj_name) + if not obj: + return + + # Check if this is an assembly component (App::Link or Part) + if not self._is_valid_component(obj): + return + + # Avoid duplicates + if obj in self.selected_components: + return + + self.selected_components.append(obj) + self._update_table() + + def on_selection_removed(self, doc_name, obj_name, sub): + """Handle selection removal.""" + doc = App.getDocument(doc_name) + if not doc: + return + + obj = doc.getObject(obj_name) + if obj in self.selected_components: + self.selected_components.remove(obj) + self._update_table() + + def _is_valid_component(self, obj): + """Check if object is a valid assembly component.""" + valid_types = [ + "App::Link", + "App::LinkGroup", + "Part::Feature", + "PartDesign::Body", + "App::Part", + ] + return obj.TypeId in valid_types + + def _update_table(self): + """Update the component table.""" + self.component_table.setRowCount(len(self.selected_components)) + + for i, comp in enumerate(self.selected_components): + # Component name + name_item = QtGui.QTableWidgetItem(comp.Label) + name_item.setFlags(name_item.flags() & ~QtCore.Qt.ItemIsEditable) + self.component_table.setItem(i, 0, name_item) + + # Remove button + remove_btn = QtGui.QPushButton("X") + remove_btn.setMaximumWidth(40) + remove_btn.clicked.connect( + lambda checked, idx=i: self._remove_component(idx) + ) + self.component_table.setCellWidget(i, 1, remove_btn) + + def _remove_component(self, index): + """Remove component from selection.""" + if 0 <= index < len(self.selected_components): + self.selected_components.pop(index) + self._update_table() + + def on_spacing_mode_changed(self, index): + """Update label based on spacing mode.""" + if index == 0: + self.length_label.setText("Total Length:") + else: + self.length_label.setText("Spacing:") + + def accept(self): + """Create the linear pattern.""" + Gui.Selection.removeObserver(self.observer) + + if not self.selected_components: + App.Console.PrintError("No components selected for pattern\n") + return False + + doc = App.ActiveDocument + if not doc: + return False + + # Get parameters + direction = App.Vector( + self.dir_x_spin.value(), + self.dir_y_spin.value(), + self.dir_z_spin.value(), + ) + + if direction.Length < 1e-6: + App.Console.PrintError("Direction vector cannot be zero\n") + return False + + direction.normalize() + + occurrences = self.occurrences_spin.value() + length_value = self.length_spin.value() + + if self.spacing_mode.currentIndex() == 0: + # Total length mode + spacing = length_value / (occurrences - 1) if occurrences > 1 else 0 + else: + # Spacing mode + spacing = length_value + + use_links = self.link_checkbox.isChecked() + hide_original = self.hide_original_checkbox.isChecked() + + # Find parent assembly + assembly = self._find_parent_assembly(self.selected_components[0]) + + doc.openTransaction("Assembly Linear Pattern") + + try: + created_objects = [] + + for comp in self.selected_components: + # Get base placement + if hasattr(comp, "Placement"): + base_placement = comp.Placement + else: + base_placement = App.Placement() + + # Create pattern instances (skip i=0 as that's the original) + for i in range(1, occurrences): + offset = direction * spacing * i + new_placement = App.Placement( + base_placement.Base + offset, + base_placement.Rotation, + ) + + if use_links: + # Create link to original + if comp.TypeId == "App::Link": + # Link to the linked object + link_target = comp.LinkedObject + else: + link_target = comp + + new_obj = doc.addObject("App::Link", f"{comp.Label}_Pattern{i}") + new_obj.LinkedObject = link_target + new_obj.Placement = new_placement + else: + # Create copy + new_obj = doc.copyObject(comp, False) + new_obj.Label = f"{comp.Label}_Pattern{i}" + new_obj.Placement = new_placement + + # Add to assembly if found + if assembly and hasattr(assembly, "addObject"): + assembly.addObject(new_obj) + + created_objects.append(new_obj) + + # Hide original if requested + if hide_original and hasattr(comp, "ViewObject"): + comp.ViewObject.Visibility = False + + doc.commitTransaction() + doc.recompute() + + App.Console.PrintMessage( + f"Created {len(created_objects)} pattern instances\n" + ) + + except Exception as e: + doc.abortTransaction() + App.Console.PrintError(f"Failed to create pattern: {e}\n") + return False + + return True + + def reject(self): + """Cancel the operation.""" + Gui.Selection.removeObserver(self.observer) + return True + + def _find_parent_assembly(self, obj): + """Find the parent assembly of an object.""" + doc = obj.Document + + for candidate in doc.Objects: + if candidate.TypeId == "Assembly::AssemblyObject": + # Check if obj is in this assembly's group + if hasattr(candidate, "Group"): + if obj in candidate.Group: + return candidate + # Check nested + for member in candidate.Group: + if hasattr(member, "Group") and obj in member.Group: + return candidate + + return None + + def getStandardButtons(self): + return 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 5120d08..7d6bbd2 100644 Binary files a/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc and b/ztools/ztools/resources/__pycache__/icons.cpython-312.pyc differ diff --git a/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc b/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc index b5f2b30..98f6975 100644 Binary files a/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc and b/ztools/ztools/resources/__pycache__/theme.cpython-312.pyc differ 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")