add spreadsheet commands

This commit is contained in:
Zoe Forbes
2026-01-26 06:34:59 -06:00
parent a66dac7afc
commit 2f03558a33
14 changed files with 2376 additions and 11 deletions

234
PLAN.md
View File

@@ -1,18 +1,37 @@
# ZTools Development Plan # ZTools Development Plan
## Current Status: v0.1.0 (70% complete) ## Current Status: v0.3.0 (80% complete)
### What's Working ### 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 - All 15 datum creation functions with custom ZTools attachment system
- Datum Creator GUI (task panel with Planes/Axes/Points tabs) - Datum Creator GUI (task panel with Planes/Axes/Points tabs)
- OK button creates datum, Cancel dismisses without creating - OK button creates datum, Cancel dismisses without creating
- Rotated Linear Pattern feature (complete) - 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) - Metadata storage system (ZTools_Type, ZTools_Params, ZTools_SourceRefs)
- Spreadsheet linking for parametric control - 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 - Replaced FreeCAD's vanilla attachment system with custom ZTools attachment
- All datums now use `MapMode='Deactivated'` with calculated placements - All datums now use `MapMode='Deactivated'` with calculated placements
- Source references stored in `ZTools_SourceRefs` property for future update capability - 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) ## Phase 1: Complete (Datum Tools)
All datum creation functions now work: 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/datum_commands.py` | Datum Creator/Manager GUI | ~520 |
| `ztools/ztools/commands/pocket_commands.py` | Enhanced Pocket GUI | ~600 | | `ztools/ztools/commands/pocket_commands.py` | Enhanced Pocket GUI | ~600 |
| `ztools/ztools/commands/pattern_commands.py` | Rotated Linear Pattern | ~206 | | `ztools/ztools/commands/pattern_commands.py` | Rotated Linear Pattern | ~206 |
| `ztools/InitGui.py` | Workbench registration | ~200 | | `ztools/ztools/commands/assembly_pattern_commands.py` | Assembly Linear/Polar Patterns | ~580 |
| `ztools/ztools/resources/icons.py` | SVG icon definitions | ~400 | | `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 - [ ] Rename persists after recompute
- [ ] Delete removes datum cleanly - [ ] 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 ## Notes

388
ROADMAP.md Normal file
View File

@@ -0,0 +1,388 @@
# ZTools Roadmap
**Version:** 0.3.0
**Last Updated:** 2026-01-25
**Target Platform:** FreeCAD 1.0+
**License:** LGPL-3.0-or-later
---
## Executive Summary
ZTools is an extended PartDesign workbench replacement for FreeCAD, focused on velocity-driven CAD workflows. It integrates enhanced datum creation, assembly patterning, spreadsheet formatting, and a unified dark theme (Catppuccin Mocha).
**Current State:** 80% complete for v1.0 release
**Active Development Areas:** Datum management, Enhanced Pocket completion, documentation
---
## Table of Contents
1. [Implemented Features](#implemented-features)
2. [Known Gaps & Incomplete Features](#known-gaps--incomplete-features)
3. [FreeCAD Ecosystem Alignment](#freecad-ecosystem-alignment)
4. [Development Roadmap](#development-roadmap)
5. [Technical Architecture](#technical-architecture)
6. [File Reference](#file-reference)
---
## Implemented Features
### 1. Workbench Integration (17 Toolbars)
ZTools consolidates multiple FreeCAD workbenches into a single unified interface:
| Category | Toolbars | Commands |
|----------|----------|----------|
| PartDesign | Structure, Datums, Additive, Subtractive, Transformations, Dress-Up, Boolean | 35+ native commands |
| Sketcher | Sketcher | 4 native commands |
| Assembly | Assembly, Assembly Joints, Assembly Management | 21 native commands |
| Spreadsheet | Spreadsheet | 6 native commands |
| ZTools Custom | ztools Datums, ztools Patterns, ztools Features, ztools Assembly, ztools Spreadsheet | 14 custom commands |
### 2. Datum Creation System (16 Functions)
**Custom Attachment System** - Replaces FreeCAD's unreliable vanilla attachment:
- Calculates placement directly from source geometry
- Stores source references in `ZTools_SourceRefs` (JSON)
- Uses `MapMode='Deactivated'` to prevent interference
- Stores creation parameters in `ZTools_Params` for recalculation
#### Datum Planes (7 modes)
| Mode | Function | Parameters |
|------|----------|------------|
| Offset from Face | `plane_offset_from_face()` | distance (mm) |
| Offset from Plane | `plane_offset_from_plane()` | distance (mm) |
| Midplane | `plane_midplane()` | 2 parallel faces |
| 3 Points | `plane_from_3_points()` | 3 vertices |
| Normal to Edge | `plane_normal_to_edge()` | parameter (0.0-1.0) |
| Angled | `plane_angled()` | angle (degrees) |
| Tangent to Cylinder | `plane_tangent_to_cylinder()` | angle (degrees) |
#### Datum Axes (4 modes)
| Mode | Function | Parameters |
|------|----------|------------|
| 2 Points | `axis_from_2_points()` | 2 vertices |
| From Edge | `axis_from_edge()` | linear edge |
| Cylinder Center | `axis_cylinder_center()` | cylindrical face |
| Plane Intersection | `axis_intersection_planes()` | 2 planes |
#### Datum Points (5 modes)
| Mode | Function | Parameters |
|------|----------|------------|
| At Vertex | `point_at_vertex()` | vertex |
| XYZ Coordinates | `point_at_coordinates()` | x, y, z |
| On Edge | `point_on_edge()` | parameter (0.0-1.0) |
| Face Center | `point_center_of_face()` | face |
| Circle Center | `point_center_of_circle()` | circular edge |
**Datum Creator GUI:**
- Auto-detection of 15+ creation modes based on selection
- Manual mode override
- Spreadsheet linking option
- Custom naming
- Real-time selection observer
### 3. Pattern Tools (3 Commands)
| Command | Description | Status |
|---------|-------------|--------|
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation per instance | Complete |
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Complete |
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Complete |
**Assembly Pattern Features:**
- Multi-component selection via table UI
- Creates as Links (recommended) or copies
- Direction/axis presets or custom vectors
- Spacing modes: Total Length or Fixed Spacing
- Angle modes: Full Circle or Custom Angle
- Auto-detects parent assembly container
### 4. Spreadsheet Formatting (9 Commands)
| Command | Description | Status |
|---------|-------------|--------|
| `ZTools_SpreadsheetStyleBold` | Toggle bold | Complete |
| `ZTools_SpreadsheetStyleItalic` | Toggle italic | Complete |
| `ZTools_SpreadsheetStyleUnderline` | Toggle underline | Complete |
| `ZTools_SpreadsheetAlignLeft` | Left align | Complete |
| `ZTools_SpreadsheetAlignCenter` | Center align | Complete |
| `ZTools_SpreadsheetAlignRight` | Right align | Complete |
| `ZTools_SpreadsheetBgColor` | Background color picker | Complete |
| `ZTools_SpreadsheetTextColor` | Text color picker | Complete |
| `ZTools_SpreadsheetQuickAlias` | Auto-create alias from label | Complete |
### 5. Enhanced Features
| Command | Description | Status |
|---------|-------------|--------|
| `ZTools_EnhancedPocket` | Pocket with "Flip Side to Cut" (SOLIDWORKS-style) | 90% Complete |
**Flip Side to Cut:** Removes material OUTSIDE the sketch profile instead of inside, using Boolean Common operation.
### 6. Theme System (Catppuccin Mocha)
- Complete Qt StyleSheet (QSS) for entire FreeCAD interface
- 26-color palette consistently applied
- 50+ widget types styled
- FreeCAD-specific widgets: PropertyEditor, Python Console, Spreadsheet
- Spreadsheet colors auto-applied on workbench activation
### 7. Icon System (33 Icons)
All icons use Catppuccin Mocha palette:
- Workbench icon
- Datum icons (planes, axes, points - 13 total)
- Pattern icons (3 total)
- Pocket icons (2 total)
- Assembly pattern icons (2 total)
- Spreadsheet formatting icons (9 total)
---
## Known Gaps & Incomplete Features
### Critical (Must Fix)
| Issue | Location | Description | Priority |
|-------|----------|-------------|----------|
| Datum Manager stub | `datum_commands.py:853` | Placeholder only - needs full implementation | High |
| Datum edit recalculation | `datum_viewprovider.py:351,355,359` | Parameter changes don't recalculate placement from source geometry | High |
### Non-Critical (Should Fix)
| Issue | Location | Description | Priority |
|-------|----------|-------------|----------|
| Enhanced Pocket incomplete | `pocket_commands.py` | Taper angle disabled for flipped pockets | Medium |
| Pocket execution logic | `pocket_commands.py` | UI complete but execution needs verification | Medium |
### Future Enhancements (Nice to Have)
| Feature | Description | Priority |
|---------|-------------|----------|
| Curve-driven pattern | Sweep instances along spline | Low |
| Fill pattern | Populate region with instances | Low |
| Variable spacing pattern | Non-uniform spacing | Low |
| Enhanced Pad | Multi-body support, draft angles | Low |
| Body operations | Split, combine, shell improvements | Low |
---
## FreeCAD Ecosystem Alignment
### FreeCAD 1.0 (November 2024) - Current Target
**Key Features ZTools Leverages:**
- **TNP Mitigation:** Topological Naming Problem largely resolved
- **Integrated Assembly Workbench:** Ondsel's assembly system now core
- **Material System Overhaul:** New material handling
- **UI/UX Improvements:** Dark/light themes, selection filters
**ZTools Alignment:**
- Custom attachment system complements TNP fix
- Full integration with native Assembly workbench
- Catppuccin theme extends FreeCAD's theming
### FreeCAD 1.1 (Expected Late 2025)
**Planned Features:**
- New Transform Manipulator
- UI Material Rendering Improvements
- Continued TNP refinement for Sketcher/PartDesign
**ZTools Opportunities:**
- Watch for new Assembly API standardization
- Monitor Sketcher improvements for datum integration
### FreeCAD Strategic Priorities (from Roadmap)
| FreeCAD Priority | ZTools Alignment |
|------------------|------------------|
| Model Stability | Custom attachment system prevents fragile models |
| Assembly Integration | Full native Assembly command exposure |
| Flatten Learning Curve | Unified toolbar consolidation |
| UI Modernization | Catppuccin Mocha theme |
| Streamlined Workflow | Quick formatting toolbars, auto-detection |
### Ondsel Contributions (Note: Ondsel shut down October 2025)
Ondsel's contributions now maintained by FreeCAD community:
- Assembly workbench (ZTools integrates)
- VarSets custom properties (potential future integration)
- Sketcher improvements
---
## Development Roadmap
### Phase 1: v0.3.x - Stability & Completion (Current)
**Timeline:** Q1 2026
| Task | Status | Priority |
|------|--------|----------|
| Complete Datum Manager GUI | Not Started | High |
| Implement datum parameter recalculation | Not Started | High |
| Verify Enhanced Pocket execution | Partial | Medium |
| Add comprehensive test coverage | Not Started | Medium |
| Documentation completion | In Progress | Medium |
### Phase 2: v0.4.0 - Polish & UX
**Timeline:** Q2 2026
| Task | Description |
|------|-------------|
| Keyboard shortcuts | Add hotkeys for common operations |
| Context menus | Right-click menus in 3D view |
| Undo/redo improvements | Better transaction naming |
| Error handling | User-friendly error messages |
| Preferences panel | ZTools configuration UI |
### Phase 3: v0.5.0 - Advanced Features
**Timeline:** Q3 2026
| Task | Description |
|------|-------------|
| Curve-driven pattern | Pattern along splines |
| Enhanced Pad | Draft angles, lip/groove |
| Body operations | Split, combine, shell |
| Datum freeze/update | Control source geometry updates |
### Phase 4: v1.0.0 - Production Release
**Timeline:** Q4 2026
| Task | Description |
|------|-------------|
| Full test suite | Automated testing |
| User documentation | Complete user guide |
| Video tutorials | Getting started series |
| FreeCAD Addon Manager | Official listing |
---
## Technical Architecture
### Directory Structure
```
ztools/
├── Init.py # Startup (non-GUI)
├── InitGui.py # Workbench registration
└── ztools/
├── __init__.py
├── commands/ # GUI commands
│ ├── __init__.py
│ ├── datum_commands.py # Datum Creator/Manager
│ ├── datum_viewprovider.py # Custom ViewProvider
│ ├── pattern_commands.py # Rotated Linear Pattern
│ ├── pocket_commands.py # Enhanced Pocket
│ ├── assembly_pattern_commands.py # Assembly patterns
│ └── spreadsheet_commands.py # Spreadsheet formatting
├── datums/ # Core datum library
│ ├── __init__.py
│ └── core.py # 16 datum functions
└── resources/ # Assets
├── __init__.py
├── icons.py # 33 SVG icons
├── theme.py # Catppuccin QSS
└── icons/ # Generated SVG files
```
### Key Design Patterns
1. **Command Pattern:** All tools follow FreeCAD's `GetResources()`, `Activated()`, `IsActive()` pattern
2. **Task Panel Pattern:** Complex UIs use `QWidget` with selection observers
3. **Feature Python Pattern:** Custom features use `Part::FeaturePython` with ViewProvider
4. **Metadata System:** JSON properties for tracking ZTools objects
### Metadata Properties
All ZTools objects have:
- `ZTools_Type`: Feature type identifier
- `ZTools_Params`: JSON creation parameters
- `ZTools_SourceRefs`: JSON source geometry references
---
## File Reference
| File | Purpose | Lines |
|------|---------|-------|
| `InitGui.py` | Workbench registration, toolbars, menus | ~330 |
| `datums/core.py` | 16 datum creation functions | ~1300 |
| `commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 |
| `commands/datum_viewprovider.py` | Custom ViewProvider, edit panel | ~400 |
| `commands/pattern_commands.py` | Rotated Linear Pattern | ~206 |
| `commands/pocket_commands.py` | Enhanced Pocket | ~600 |
| `commands/assembly_pattern_commands.py` | Assembly patterns | ~580 |
| `commands/spreadsheet_commands.py` | Spreadsheet formatting | ~480 |
| `resources/icons.py` | 33 SVG icon definitions | ~540 |
| `resources/theme.py` | Catppuccin Mocha QSS | ~1500 |
**Total:** ~6,400+ lines of code
---
## Statistics Summary
| Category | Count |
|----------|-------|
| Command Files | 6 |
| Command Classes | 24+ |
| Datum Creation Functions | 16 |
| Icons Defined | 33 |
| Toolbars Registered | 17 |
| Menu Hierarchies | 7 major |
| Native Commands Exposed | 66 |
| Custom ZTools Commands | 14 |
| Theme Colors | 26 |
| Styled Widget Types | 50+ |
---
## Contributing
ZTools follows FreeCAD's contribution guidelines. Key areas needing help:
1. **Testing:** Manual testing on different platforms
2. **Documentation:** User guides and tutorials
3. **Translations:** Internationalization support
4. **Bug Reports:** Issue tracking and reproduction
---
## License
LGPL-3.0-or-later
Compatible with FreeCAD's licensing model.
---
## Changelog
### v0.3.0 (2026-01-25)
- Added zSpreadsheet module with 9 formatting commands
- Added Spreadsheet workbench integration (6 native commands)
- Added 9 spreadsheet formatting icons
### v0.2.0 (2026-01-25)
- Added Assembly workbench integration (21 native commands)
- Added Assembly Linear Pattern tool
- Added Assembly Polar Pattern tool
- Added assembly pattern icons
### v0.1.0 (2026-01-24)
- Initial release
- Custom attachment system for datums
- 16 datum creation functions
- Datum Creator GUI with auto-detection
- Rotated Linear Pattern
- Enhanced Pocket (partial)
- Catppuccin Mocha theme
- 21 initial icons

View File

@@ -60,10 +60,30 @@ class ZToolsWorkbench(Gui.Workbench):
if hasattr(sketcher_wb, "Initialize"): if hasattr(sketcher_wb, "Initialize"):
sketcher_wb.Initialize() sketcher_wb.Initialize()
except Exception as e: # Initialize Assembly workbench if available (FreeCAD 1.0+)
App.Console.PrintWarning(f"Could not initialize PartDesign/Sketcher: {e}\n") 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 # PartDesign Structure Tools
@@ -176,6 +196,74 @@ class ZToolsWorkbench(Gui.Workbench):
"ZTools_EnhancedPocket", "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 # Append Toolbars
# ===================================================================== # =====================================================================
@@ -189,9 +277,15 @@ class ZToolsWorkbench(Gui.Workbench):
self.appendToolbar("Transformations", self.transformation_tools) self.appendToolbar("Transformations", self.transformation_tools)
self.appendToolbar("Dress-Up", self.dressup_tools) self.appendToolbar("Dress-Up", self.dressup_tools)
self.appendToolbar("Boolean", self.boolean_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 Datums", self.ztools_datum_tools)
self.appendToolbar("ztools Patterns", self.ztools_pattern_tools) self.appendToolbar("ztools Patterns", self.ztools_pattern_tools)
self.appendToolbar("ztools Features", self.ztools_pocket_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 # Append Menus
@@ -209,17 +303,32 @@ class ZToolsWorkbench(Gui.Workbench):
self.appendMenu(["PartDesign", "Transformations"], self.transformation_tools) self.appendMenu(["PartDesign", "Transformations"], self.transformation_tools)
self.appendMenu(["PartDesign", "Dress-Up"], self.dressup_tools) self.appendMenu(["PartDesign", "Dress-Up"], self.dressup_tools)
self.appendMenu(["PartDesign", "Boolean"], self.boolean_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( self.appendMenu(
"ztools", "ztools",
self.ztools_datum_tools self.ztools_datum_tools
+ self.ztools_pattern_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") App.Console.PrintMessage("ztools workbench initialized\n")
def Activated(self): def Activated(self):
"""Called when workbench is activated.""" """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") App.Console.PrintMessage("ztools workbench activated\n")
def Deactivated(self): def Deactivated(self):

View File

@@ -1,9 +1,18 @@
# ztools/commands - GUI commands # 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__ = [ __all__ = [
"datum_commands", "datum_commands",
"datum_viewprovider", "datum_viewprovider",
"pattern_commands", "pattern_commands",
"pocket_commands", "pocket_commands",
"assembly_pattern_commands",
"spreadsheet_commands",
] ]

View File

@@ -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())

View File

@@ -0,0 +1,567 @@
# ztools/commands/spreadsheet_commands.py
# Enhanced spreadsheet formatting tools for FreeCAD
# Provides quick formatting toolbar for cell styling
import FreeCAD as App
import FreeCADGui as Gui
from PySide import QtCore, QtGui
from ztools.resources.icons import get_icon
# =============================================================================
# Helper Functions
# =============================================================================
def get_active_spreadsheet():
"""Get the currently active spreadsheet object and its view.
Returns:
tuple: (sheet_object, sheet_view) or (None, None)
"""
doc = App.ActiveDocument
if not doc:
return None, None
# Get MDI area and active subwindow
main_window = Gui.getMainWindow()
mdi = main_window.centralWidget()
if not mdi:
return None, None
subwindow = mdi.activeSubWindow()
if not subwindow:
return None, None
# Get widget from subwindow
widget = subwindow.widget()
if not widget:
return None, None
# Check if it's a spreadsheet view by looking for the table view
# FreeCAD's spreadsheet view contains a QTableView
table_view = None
if hasattr(widget, "findChild"):
table_view = widget.findChild(QtGui.QTableView)
if not table_view:
# Try if widget itself is the table view
if isinstance(widget, QtGui.QTableView):
table_view = widget
else:
return None, None
# Get the spreadsheet object from window title
# Window title format varies: "Spreadsheet" or "Spreadsheet - DocName"
title = subwindow.windowTitle()
sheet_name = title.split(" - ")[0].split(" : ")[0].strip()
# Try to find the sheet object
sheet = doc.getObject(sheet_name)
if sheet and sheet.TypeId == "Spreadsheet::Sheet":
return sheet, table_view
# Fallback: search for any spreadsheet object
for obj in doc.Objects:
if obj.TypeId == "Spreadsheet::Sheet":
return obj, table_view
return None, None
def get_selected_cells():
"""Get list of selected cell addresses from active spreadsheet.
Returns:
tuple: (sheet_object, list_of_cell_addresses) or (None, [])
"""
sheet, table_view = get_active_spreadsheet()
if not sheet or not table_view:
return None, []
# Get selection model
selection_model = table_view.selectionModel()
if not selection_model:
return sheet, []
indexes = selection_model.selectedIndexes()
if not indexes:
return sheet, []
cells = []
for idx in indexes:
col = idx.column()
row = idx.row()
# Convert to cell address (A1 notation)
# Handle columns beyond Z (AA, AB, etc.)
col_str = ""
temp_col = col
while temp_col >= 0:
col_str = chr(65 + (temp_col % 26)) + col_str
temp_col = temp_col // 26 - 1
cell_addr = f"{col_str}{row + 1}"
cells.append(cell_addr)
return sheet, cells
def column_to_index(col_str):
"""Convert column string (A, B, ..., Z, AA, AB, ...) to index."""
result = 0
for char in col_str:
result = result * 26 + (ord(char) - ord("A") + 1)
return result - 1
# =============================================================================
# Style Commands (Bold, Italic, Underline)
# =============================================================================
class ZTools_SpreadsheetStyleBold:
"""Toggle bold style on selected cells."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_bold"),
"MenuText": "Bold",
"ToolTip": "Toggle bold style on selected cells (Ctrl+B)",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Toggle Bold")
try:
for cell in cells:
current = sheet.getStyle(cell) or ""
styles = set(s.strip() for s in current.split("|") if s.strip())
if "bold" in styles:
styles.discard("bold")
else:
styles.add("bold")
new_style = "|".join(sorted(styles)) if styles else ""
sheet.setStyle(cell, new_style)
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to toggle bold: {e}\n")
class ZTools_SpreadsheetStyleItalic:
"""Toggle italic style on selected cells."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_italic"),
"MenuText": "Italic",
"ToolTip": "Toggle italic style on selected cells (Ctrl+I)",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Toggle Italic")
try:
for cell in cells:
current = sheet.getStyle(cell) or ""
styles = set(s.strip() for s in current.split("|") if s.strip())
if "italic" in styles:
styles.discard("italic")
else:
styles.add("italic")
new_style = "|".join(sorted(styles)) if styles else ""
sheet.setStyle(cell, new_style)
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to toggle italic: {e}\n")
class ZTools_SpreadsheetStyleUnderline:
"""Toggle underline style on selected cells."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_underline"),
"MenuText": "Underline",
"ToolTip": "Toggle underline style on selected cells (Ctrl+U)",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Toggle Underline")
try:
for cell in cells:
current = sheet.getStyle(cell) or ""
styles = set(s.strip() for s in current.split("|") if s.strip())
if "underline" in styles:
styles.discard("underline")
else:
styles.add("underline")
new_style = "|".join(sorted(styles)) if styles else ""
sheet.setStyle(cell, new_style)
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to toggle underline: {e}\n")
# =============================================================================
# Alignment Commands
# =============================================================================
class ZTools_SpreadsheetAlignLeft:
"""Align cell content to the left."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_align_left"),
"MenuText": "Align Left",
"ToolTip": "Align selected cells to the left",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Align Left")
try:
for cell in cells:
sheet.setAlignment(cell, "left")
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to align left: {e}\n")
class ZTools_SpreadsheetAlignCenter:
"""Align cell content to the center."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_align_center"),
"MenuText": "Align Center",
"ToolTip": "Align selected cells to the center",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Align Center")
try:
for cell in cells:
sheet.setAlignment(cell, "center")
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to align center: {e}\n")
class ZTools_SpreadsheetAlignRight:
"""Align cell content to the right."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_align_right"),
"MenuText": "Align Right",
"ToolTip": "Align selected cells to the right",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Align Right")
try:
for cell in cells:
sheet.setAlignment(cell, "right")
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to align right: {e}\n")
# =============================================================================
# Color Commands
# =============================================================================
class ZTools_SpreadsheetBgColor:
"""Set background color of selected cells."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_bg_color"),
"MenuText": "Background Color",
"ToolTip": "Set background color of selected cells",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
# Show color picker dialog
color = QtGui.QColorDialog.getColor(
QtCore.Qt.white, Gui.getMainWindow(), "Select Background Color"
)
if not color.isValid():
return
doc = App.ActiveDocument
doc.openTransaction("Set Background Color")
try:
# FreeCAD expects RGB as tuple of floats 0-1
rgb = (
color.redF(),
color.greenF(),
color.blueF(),
)
for cell in cells:
sheet.setBackground(cell, rgb)
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to set background color: {e}\n")
class ZTools_SpreadsheetTextColor:
"""Set text color of selected cells."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_text_color"),
"MenuText": "Text Color",
"ToolTip": "Set text color of selected cells",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
# Show color picker dialog with default white for dark theme
initial_color = QtGui.QColor(205, 214, 244) # Catppuccin Mocha text color
color = QtGui.QColorDialog.getColor(
initial_color, Gui.getMainWindow(), "Select Text Color"
)
if not color.isValid():
return
doc = App.ActiveDocument
doc.openTransaction("Set Text Color")
try:
# FreeCAD expects RGB as tuple of floats 0-1
rgb = (
color.redF(),
color.greenF(),
color.blueF(),
)
for cell in cells:
sheet.setForeground(cell, rgb)
doc.commitTransaction()
doc.recompute()
except Exception as e:
doc.abortTransaction()
App.Console.PrintError(f"Failed to set text color: {e}\n")
# =============================================================================
# Utility Commands
# =============================================================================
class ZTools_SpreadsheetQuickAlias:
"""Create alias from cell content."""
def GetResources(self):
return {
"Pixmap": get_icon("spreadsheet_quick_alias"),
"MenuText": "Quick Alias",
"ToolTip": "Create alias for selected cell based on adjacent label cell",
}
def IsActive(self):
sheet, cells = get_selected_cells()
return sheet is not None and len(cells) > 0
def Activated(self):
sheet, cells = get_selected_cells()
if not sheet or not cells:
App.Console.PrintWarning("No cells selected\n")
return
doc = App.ActiveDocument
doc.openTransaction("Quick Alias")
try:
aliases_created = 0
for cell in cells:
# Parse cell address
import re
match = re.match(r"([A-Z]+)(\d+)", cell)
if not match:
continue
col_str = match.group(1)
row = int(match.group(2))
# Get content of cell to the left (label cell)
col_idx = column_to_index(col_str)
if col_idx > 0:
# Convert back to column string
label_col_idx = col_idx - 1
label_col_str = ""
temp = label_col_idx
while temp >= 0:
label_col_str = chr(65 + (temp % 26)) + label_col_str
temp = temp // 26 - 1
label_cell = f"{label_col_str}{row}"
label_content = sheet.getContents(label_cell)
if label_content:
# Clean the label to make a valid alias
# Must be alphanumeric + underscore, start with letter
alias = "".join(
c if c.isalnum() or c == "_" else "_"
for c in label_content
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())

View File

@@ -294,6 +294,136 @@ ICON_POCKET_FLIPPED_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0
<path d="M28 16 L24 14 L24 18 Z" fill="{MOCHA["peach"]}"/> <path d="M28 16 L24 14 L24 18 Z" fill="{MOCHA["peach"]}"/>
</svg>''' </svg>'''
# Assembly Linear Pattern icon - components along a line
ICON_ASSEMBLY_LINEAR_PATTERN_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<!-- Direction arrow -->
<line x1="4" y1="16" x2="26" y2="16" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" stroke-dasharray="3,2"/>
<path d="M24 13 L28 16 L24 19" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Component 1 (original) -->
<rect x="4" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
<circle cx="7" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
<!-- Component 2 -->
<rect x="13" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
<circle cx="16" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
<!-- Component 3 -->
<rect x="22" y="10" width="6" height="6" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
<circle cx="25" cy="13" r="1.5" fill="{MOCHA["teal"]}"/>
<!-- Count indicator -->
<text x="16" y="26" font-family="monospace" font-size="7" fill="{MOCHA["text"]}" text-anchor="middle">1-2-3</text>
</svg>'''
# Assembly Polar Pattern icon - components around a center
ICON_ASSEMBLY_POLAR_PATTERN_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<!-- Center axis indicator -->
<circle cx="16" cy="16" r="2" fill="{MOCHA["peach"]}"/>
<circle cx="16" cy="16" r="10" fill="none" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="3,2"/>
<!-- Rotation arrow -->
<path d="M23 8 A9 9 0 0 1 26 14" stroke="{MOCHA["green"]}" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path d="M25 11 L26 14 L23 14" stroke="{MOCHA["green"]}" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
<!-- Component 1 (top) -->
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["sapphire"]}" stroke="{MOCHA["blue"]}" stroke-width="1"/>
<!-- Component 2 (right) -->
<g transform="translate(16,16) rotate(72) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["mauve"]}" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
</g>
<!-- Component 3 (bottom-right) -->
<g transform="translate(16,16) rotate(144) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["pink"]}" stroke="{MOCHA["flamingo"]}" stroke-width="1"/>
</g>
<!-- Component 4 (bottom-left) -->
<g transform="translate(16,16) rotate(216) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["peach"]}" stroke="{MOCHA["maroon"]}" stroke-width="1"/>
</g>
<!-- Component 5 (left) -->
<g transform="translate(16,16) rotate(288) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="{MOCHA["yellow"]}" stroke="{MOCHA["peach"]}" stroke-width="1"/>
</g>
</svg>'''
# =============================================================================
# Spreadsheet Icons
# =============================================================================
# Bold text icon
ICON_SPREADSHEET_BOLD_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<text x="16" y="23" font-family="sans-serif" font-size="18" font-weight="bold" fill="{MOCHA["text"]}" text-anchor="middle">B</text>
</svg>'''
# Italic text icon
ICON_SPREADSHEET_ITALIC_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<text x="16" y="23" font-family="serif" font-size="18" font-style="italic" fill="{MOCHA["text"]}" text-anchor="middle">I</text>
</svg>'''
# Underline text icon
ICON_SPREADSHEET_UNDERLINE_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<text x="16" y="20" font-family="sans-serif" font-size="16" fill="{MOCHA["text"]}" text-anchor="middle">U</text>
<line x1="10" y1="24" x2="22" y2="24" stroke="{MOCHA["text"]}" stroke-width="2"/>
</svg>'''
# Align left icon
ICON_SPREADSHEET_ALIGN_LEFT_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="6" y1="14" x2="20" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="6" y1="19" x2="24" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="6" y1="24" x2="16" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
</svg>'''
# Align center icon
ICON_SPREADSHEET_ALIGN_CENTER_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="14" x2="22" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="19" x2="24" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="24" x2="20" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
</svg>'''
# Align right icon
ICON_SPREADSHEET_ALIGN_RIGHT_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<line x1="6" y1="9" x2="26" y2="9" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="12" y1="14" x2="26" y2="14" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="19" x2="26" y2="19" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="24" x2="26" y2="24" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
</svg>'''
# Background color icon (paint bucket)
ICON_SPREADSHEET_BG_COLOR_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<!-- Paint bucket -->
<path d="M10 12 L16 6 L22 12 L22 20 C22 22 20 24 16 24 C12 24 10 22 10 20 Z" fill="{MOCHA["yellow"]}" stroke="{MOCHA["peach"]}" stroke-width="1.5"/>
<!-- Handle -->
<path d="M16 6 L16 3" stroke="{MOCHA["overlay1"]}" stroke-width="2" stroke-linecap="round"/>
<!-- Paint drip -->
<ellipse cx="25" cy="22" rx="2" ry="3" fill="{MOCHA["yellow"]}"/>
</svg>'''
# Text color icon (A with color bar)
ICON_SPREADSHEET_TEXT_COLOR_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<!-- Letter A -->
<text x="16" y="20" font-family="sans-serif" font-size="16" font-weight="bold" fill="{MOCHA["text"]}" text-anchor="middle">A</text>
<!-- Color bar -->
<rect x="8" y="24" width="16" height="3" rx="1" fill="{MOCHA["red"]}"/>
</svg>'''
# Quick alias icon (tag/label)
ICON_SPREADSHEET_QUICK_ALIAS_SVG = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="{MOCHA["surface0"]}"/>
<!-- Tag shape -->
<path d="M6 10 L18 10 L24 16 L18 22 L6 22 Z" fill="{MOCHA["teal"]}" stroke="{MOCHA["green"]}" stroke-width="1.5"/>
<!-- Tag hole -->
<circle cx="10" cy="16" r="2" fill="{MOCHA["surface0"]}"/>
<!-- Equals sign (alias) -->
<line x1="20" y1="12" x2="26" y2="12" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
<line x1="20" y1="17" x2="26" y2="17" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
</svg>'''
# ============================================================================= # =============================================================================
# Icon Registry - Base64 encoded for FreeCAD # Icon Registry - Base64 encoded for FreeCAD
@@ -331,6 +461,17 @@ def get_icon(name: str) -> str:
"rotated_pattern": ICON_ROTATED_PATTERN_SVG, "rotated_pattern": ICON_ROTATED_PATTERN_SVG,
"pocket_enhanced": ICON_POCKET_ENHANCED_SVG, "pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
"pocket_flipped": ICON_POCKET_FLIPPED_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: if name not in icons:
@@ -377,6 +518,17 @@ def save_icons_to_disk(directory: str):
"ztools_rotated_pattern": ICON_ROTATED_PATTERN_SVG, "ztools_rotated_pattern": ICON_ROTATED_PATTERN_SVG,
"ztools_pocket_enhanced": ICON_POCKET_ENHANCED_SVG, "ztools_pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
"ztools_pocket_flipped": ICON_POCKET_FLIPPED_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(): for name, svg in icons.items():

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Direction arrow -->
<line x1="4" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1.5" stroke-dasharray="3,2"/>
<path d="M24 13 L28 16 L24 19" stroke="#7f849c" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Component 1 (original) -->
<rect x="4" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
<circle cx="7" cy="13" r="1.5" fill="#94e2d5"/>
<!-- Component 2 -->
<rect x="13" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
<circle cx="16" cy="13" r="1.5" fill="#94e2d5"/>
<!-- Component 3 -->
<rect x="22" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
<circle cx="25" cy="13" r="1.5" fill="#94e2d5"/>
<!-- Count indicator -->
<text x="16" y="26" font-family="monospace" font-size="7" fill="#cdd6f4" text-anchor="middle">1-2-3</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- Center axis indicator -->
<circle cx="16" cy="16" r="2" fill="#fab387"/>
<circle cx="16" cy="16" r="10" fill="none" stroke="#7f849c" stroke-width="1" stroke-dasharray="3,2"/>
<!-- Rotation arrow -->
<path d="M23 8 A9 9 0 0 1 26 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<path d="M25 11 L26 14 L23 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
<!-- Component 1 (top) -->
<rect x="13" y="4" width="6" height="5" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
<!-- Component 2 (right) -->
<g transform="translate(16,16) rotate(72) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="#cba6f7" stroke="#b4befe" stroke-width="1"/>
</g>
<!-- Component 3 (bottom-right) -->
<g transform="translate(16,16) rotate(144) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f5c2e7" stroke="#f2cdcd" stroke-width="1"/>
</g>
<!-- Component 4 (bottom-left) -->
<g transform="translate(16,16) rotate(216) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="#fab387" stroke="#eba0ac" stroke-width="1"/>
</g>
<!-- Component 5 (left) -->
<g transform="translate(16,16) rotate(288) translate(-16,-16)">
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f9e2af" stroke="#fab387" stroke-width="1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1218,6 +1218,43 @@ SpreadsheetGui--SheetTableView QHeaderView::section {{
padding: 4px; 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 */ /* Python Console */
Gui--PythonConsole {{ Gui--PythonConsole {{
background-color: {_crust}; background-color: {_crust};
@@ -1304,3 +1341,53 @@ def get_stylesheet() -> str:
str: Complete QSS stylesheet. str: Complete QSS stylesheet.
""" """
return generate_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")