add spreadsheet commands
This commit is contained in:
234
PLAN.md
234
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
|
||||
|
||||
388
ROADMAP.md
Normal file
388
ROADMAP.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# ZTools Roadmap
|
||||
|
||||
**Version:** 0.3.0
|
||||
**Last Updated:** 2026-01-25
|
||||
**Target Platform:** FreeCAD 1.0+
|
||||
**License:** LGPL-3.0-or-later
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
ZTools is an extended PartDesign workbench replacement for FreeCAD, focused on velocity-driven CAD workflows. It integrates enhanced datum creation, assembly patterning, spreadsheet formatting, and a unified dark theme (Catppuccin Mocha).
|
||||
|
||||
**Current State:** 80% complete for v1.0 release
|
||||
**Active Development Areas:** Datum management, Enhanced Pocket completion, documentation
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Implemented Features](#implemented-features)
|
||||
2. [Known Gaps & Incomplete Features](#known-gaps--incomplete-features)
|
||||
3. [FreeCAD Ecosystem Alignment](#freecad-ecosystem-alignment)
|
||||
4. [Development Roadmap](#development-roadmap)
|
||||
5. [Technical Architecture](#technical-architecture)
|
||||
6. [File Reference](#file-reference)
|
||||
|
||||
---
|
||||
|
||||
## Implemented Features
|
||||
|
||||
### 1. Workbench Integration (17 Toolbars)
|
||||
|
||||
ZTools consolidates multiple FreeCAD workbenches into a single unified interface:
|
||||
|
||||
| Category | Toolbars | Commands |
|
||||
|----------|----------|----------|
|
||||
| PartDesign | Structure, Datums, Additive, Subtractive, Transformations, Dress-Up, Boolean | 35+ native commands |
|
||||
| Sketcher | Sketcher | 4 native commands |
|
||||
| Assembly | Assembly, Assembly Joints, Assembly Management | 21 native commands |
|
||||
| Spreadsheet | Spreadsheet | 6 native commands |
|
||||
| ZTools Custom | ztools Datums, ztools Patterns, ztools Features, ztools Assembly, ztools Spreadsheet | 14 custom commands |
|
||||
|
||||
### 2. Datum Creation System (16 Functions)
|
||||
|
||||
**Custom Attachment System** - Replaces FreeCAD's unreliable vanilla attachment:
|
||||
- Calculates placement directly from source geometry
|
||||
- Stores source references in `ZTools_SourceRefs` (JSON)
|
||||
- Uses `MapMode='Deactivated'` to prevent interference
|
||||
- Stores creation parameters in `ZTools_Params` for recalculation
|
||||
|
||||
#### Datum Planes (7 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| Offset from Face | `plane_offset_from_face()` | distance (mm) |
|
||||
| Offset from Plane | `plane_offset_from_plane()` | distance (mm) |
|
||||
| Midplane | `plane_midplane()` | 2 parallel faces |
|
||||
| 3 Points | `plane_from_3_points()` | 3 vertices |
|
||||
| Normal to Edge | `plane_normal_to_edge()` | parameter (0.0-1.0) |
|
||||
| Angled | `plane_angled()` | angle (degrees) |
|
||||
| Tangent to Cylinder | `plane_tangent_to_cylinder()` | angle (degrees) |
|
||||
|
||||
#### Datum Axes (4 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| 2 Points | `axis_from_2_points()` | 2 vertices |
|
||||
| From Edge | `axis_from_edge()` | linear edge |
|
||||
| Cylinder Center | `axis_cylinder_center()` | cylindrical face |
|
||||
| Plane Intersection | `axis_intersection_planes()` | 2 planes |
|
||||
|
||||
#### Datum Points (5 modes)
|
||||
| Mode | Function | Parameters |
|
||||
|------|----------|------------|
|
||||
| At Vertex | `point_at_vertex()` | vertex |
|
||||
| XYZ Coordinates | `point_at_coordinates()` | x, y, z |
|
||||
| On Edge | `point_on_edge()` | parameter (0.0-1.0) |
|
||||
| Face Center | `point_center_of_face()` | face |
|
||||
| Circle Center | `point_center_of_circle()` | circular edge |
|
||||
|
||||
**Datum Creator GUI:**
|
||||
- Auto-detection of 15+ creation modes based on selection
|
||||
- Manual mode override
|
||||
- Spreadsheet linking option
|
||||
- Custom naming
|
||||
- Real-time selection observer
|
||||
|
||||
### 3. Pattern Tools (3 Commands)
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_RotatedLinearPattern` | Linear pattern with incremental rotation per instance | Complete |
|
||||
| `ZTools_AssemblyLinearPattern` | Pattern assembly components linearly | Complete |
|
||||
| `ZTools_AssemblyPolarPattern` | Pattern assembly components around axis | Complete |
|
||||
|
||||
**Assembly Pattern Features:**
|
||||
- Multi-component selection via table UI
|
||||
- Creates as Links (recommended) or copies
|
||||
- Direction/axis presets or custom vectors
|
||||
- Spacing modes: Total Length or Fixed Spacing
|
||||
- Angle modes: Full Circle or Custom Angle
|
||||
- Auto-detects parent assembly container
|
||||
|
||||
### 4. Spreadsheet Formatting (9 Commands)
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_SpreadsheetStyleBold` | Toggle bold | Complete |
|
||||
| `ZTools_SpreadsheetStyleItalic` | Toggle italic | Complete |
|
||||
| `ZTools_SpreadsheetStyleUnderline` | Toggle underline | Complete |
|
||||
| `ZTools_SpreadsheetAlignLeft` | Left align | Complete |
|
||||
| `ZTools_SpreadsheetAlignCenter` | Center align | Complete |
|
||||
| `ZTools_SpreadsheetAlignRight` | Right align | Complete |
|
||||
| `ZTools_SpreadsheetBgColor` | Background color picker | Complete |
|
||||
| `ZTools_SpreadsheetTextColor` | Text color picker | Complete |
|
||||
| `ZTools_SpreadsheetQuickAlias` | Auto-create alias from label | Complete |
|
||||
|
||||
### 5. Enhanced Features
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `ZTools_EnhancedPocket` | Pocket with "Flip Side to Cut" (SOLIDWORKS-style) | 90% Complete |
|
||||
|
||||
**Flip Side to Cut:** Removes material OUTSIDE the sketch profile instead of inside, using Boolean Common operation.
|
||||
|
||||
### 6. Theme System (Catppuccin Mocha)
|
||||
|
||||
- Complete Qt StyleSheet (QSS) for entire FreeCAD interface
|
||||
- 26-color palette consistently applied
|
||||
- 50+ widget types styled
|
||||
- FreeCAD-specific widgets: PropertyEditor, Python Console, Spreadsheet
|
||||
- Spreadsheet colors auto-applied on workbench activation
|
||||
|
||||
### 7. Icon System (33 Icons)
|
||||
|
||||
All icons use Catppuccin Mocha palette:
|
||||
- Workbench icon
|
||||
- Datum icons (planes, axes, points - 13 total)
|
||||
- Pattern icons (3 total)
|
||||
- Pocket icons (2 total)
|
||||
- Assembly pattern icons (2 total)
|
||||
- Spreadsheet formatting icons (9 total)
|
||||
|
||||
---
|
||||
|
||||
## Known Gaps & Incomplete Features
|
||||
|
||||
### Critical (Must Fix)
|
||||
|
||||
| Issue | Location | Description | Priority |
|
||||
|-------|----------|-------------|----------|
|
||||
| Datum Manager stub | `datum_commands.py:853` | Placeholder only - needs full implementation | High |
|
||||
| Datum edit recalculation | `datum_viewprovider.py:351,355,359` | Parameter changes don't recalculate placement from source geometry | High |
|
||||
|
||||
### Non-Critical (Should Fix)
|
||||
|
||||
| Issue | Location | Description | Priority |
|
||||
|-------|----------|-------------|----------|
|
||||
| Enhanced Pocket incomplete | `pocket_commands.py` | Taper angle disabled for flipped pockets | Medium |
|
||||
| Pocket execution logic | `pocket_commands.py` | UI complete but execution needs verification | Medium |
|
||||
|
||||
### Future Enhancements (Nice to Have)
|
||||
|
||||
| Feature | Description | Priority |
|
||||
|---------|-------------|----------|
|
||||
| Curve-driven pattern | Sweep instances along spline | Low |
|
||||
| Fill pattern | Populate region with instances | Low |
|
||||
| Variable spacing pattern | Non-uniform spacing | Low |
|
||||
| Enhanced Pad | Multi-body support, draft angles | Low |
|
||||
| Body operations | Split, combine, shell improvements | Low |
|
||||
|
||||
---
|
||||
|
||||
## FreeCAD Ecosystem Alignment
|
||||
|
||||
### FreeCAD 1.0 (November 2024) - Current Target
|
||||
|
||||
**Key Features ZTools Leverages:**
|
||||
- **TNP Mitigation:** Topological Naming Problem largely resolved
|
||||
- **Integrated Assembly Workbench:** Ondsel's assembly system now core
|
||||
- **Material System Overhaul:** New material handling
|
||||
- **UI/UX Improvements:** Dark/light themes, selection filters
|
||||
|
||||
**ZTools Alignment:**
|
||||
- Custom attachment system complements TNP fix
|
||||
- Full integration with native Assembly workbench
|
||||
- Catppuccin theme extends FreeCAD's theming
|
||||
|
||||
### FreeCAD 1.1 (Expected Late 2025)
|
||||
|
||||
**Planned Features:**
|
||||
- New Transform Manipulator
|
||||
- UI Material Rendering Improvements
|
||||
- Continued TNP refinement for Sketcher/PartDesign
|
||||
|
||||
**ZTools Opportunities:**
|
||||
- Watch for new Assembly API standardization
|
||||
- Monitor Sketcher improvements for datum integration
|
||||
|
||||
### FreeCAD Strategic Priorities (from Roadmap)
|
||||
|
||||
| FreeCAD Priority | ZTools Alignment |
|
||||
|------------------|------------------|
|
||||
| Model Stability | Custom attachment system prevents fragile models |
|
||||
| Assembly Integration | Full native Assembly command exposure |
|
||||
| Flatten Learning Curve | Unified toolbar consolidation |
|
||||
| UI Modernization | Catppuccin Mocha theme |
|
||||
| Streamlined Workflow | Quick formatting toolbars, auto-detection |
|
||||
|
||||
### Ondsel Contributions (Note: Ondsel shut down October 2025)
|
||||
|
||||
Ondsel's contributions now maintained by FreeCAD community:
|
||||
- Assembly workbench (ZTools integrates)
|
||||
- VarSets custom properties (potential future integration)
|
||||
- Sketcher improvements
|
||||
|
||||
---
|
||||
|
||||
## Development Roadmap
|
||||
|
||||
### Phase 1: v0.3.x - Stability & Completion (Current)
|
||||
|
||||
**Timeline:** Q1 2026
|
||||
|
||||
| Task | Status | Priority |
|
||||
|------|--------|----------|
|
||||
| Complete Datum Manager GUI | Not Started | High |
|
||||
| Implement datum parameter recalculation | Not Started | High |
|
||||
| Verify Enhanced Pocket execution | Partial | Medium |
|
||||
| Add comprehensive test coverage | Not Started | Medium |
|
||||
| Documentation completion | In Progress | Medium |
|
||||
|
||||
### Phase 2: v0.4.0 - Polish & UX
|
||||
|
||||
**Timeline:** Q2 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Keyboard shortcuts | Add hotkeys for common operations |
|
||||
| Context menus | Right-click menus in 3D view |
|
||||
| Undo/redo improvements | Better transaction naming |
|
||||
| Error handling | User-friendly error messages |
|
||||
| Preferences panel | ZTools configuration UI |
|
||||
|
||||
### Phase 3: v0.5.0 - Advanced Features
|
||||
|
||||
**Timeline:** Q3 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Curve-driven pattern | Pattern along splines |
|
||||
| Enhanced Pad | Draft angles, lip/groove |
|
||||
| Body operations | Split, combine, shell |
|
||||
| Datum freeze/update | Control source geometry updates |
|
||||
|
||||
### Phase 4: v1.0.0 - Production Release
|
||||
|
||||
**Timeline:** Q4 2026
|
||||
|
||||
| Task | Description |
|
||||
|------|-------------|
|
||||
| Full test suite | Automated testing |
|
||||
| User documentation | Complete user guide |
|
||||
| Video tutorials | Getting started series |
|
||||
| FreeCAD Addon Manager | Official listing |
|
||||
|
||||
---
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ztools/
|
||||
├── Init.py # Startup (non-GUI)
|
||||
├── InitGui.py # Workbench registration
|
||||
└── ztools/
|
||||
├── __init__.py
|
||||
├── commands/ # GUI commands
|
||||
│ ├── __init__.py
|
||||
│ ├── datum_commands.py # Datum Creator/Manager
|
||||
│ ├── datum_viewprovider.py # Custom ViewProvider
|
||||
│ ├── pattern_commands.py # Rotated Linear Pattern
|
||||
│ ├── pocket_commands.py # Enhanced Pocket
|
||||
│ ├── assembly_pattern_commands.py # Assembly patterns
|
||||
│ └── spreadsheet_commands.py # Spreadsheet formatting
|
||||
├── datums/ # Core datum library
|
||||
│ ├── __init__.py
|
||||
│ └── core.py # 16 datum functions
|
||||
└── resources/ # Assets
|
||||
├── __init__.py
|
||||
├── icons.py # 33 SVG icons
|
||||
├── theme.py # Catppuccin QSS
|
||||
└── icons/ # Generated SVG files
|
||||
```
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
1. **Command Pattern:** All tools follow FreeCAD's `GetResources()`, `Activated()`, `IsActive()` pattern
|
||||
2. **Task Panel Pattern:** Complex UIs use `QWidget` with selection observers
|
||||
3. **Feature Python Pattern:** Custom features use `Part::FeaturePython` with ViewProvider
|
||||
4. **Metadata System:** JSON properties for tracking ZTools objects
|
||||
|
||||
### Metadata Properties
|
||||
|
||||
All ZTools objects have:
|
||||
- `ZTools_Type`: Feature type identifier
|
||||
- `ZTools_Params`: JSON creation parameters
|
||||
- `ZTools_SourceRefs`: JSON source geometry references
|
||||
|
||||
---
|
||||
|
||||
## File Reference
|
||||
|
||||
| File | Purpose | Lines |
|
||||
|------|---------|-------|
|
||||
| `InitGui.py` | Workbench registration, toolbars, menus | ~330 |
|
||||
| `datums/core.py` | 16 datum creation functions | ~1300 |
|
||||
| `commands/datum_commands.py` | Datum Creator/Manager GUI | ~520 |
|
||||
| `commands/datum_viewprovider.py` | Custom ViewProvider, edit panel | ~400 |
|
||||
| `commands/pattern_commands.py` | Rotated Linear Pattern | ~206 |
|
||||
| `commands/pocket_commands.py` | Enhanced Pocket | ~600 |
|
||||
| `commands/assembly_pattern_commands.py` | Assembly patterns | ~580 |
|
||||
| `commands/spreadsheet_commands.py` | Spreadsheet formatting | ~480 |
|
||||
| `resources/icons.py` | 33 SVG icon definitions | ~540 |
|
||||
| `resources/theme.py` | Catppuccin Mocha QSS | ~1500 |
|
||||
|
||||
**Total:** ~6,400+ lines of code
|
||||
|
||||
---
|
||||
|
||||
## Statistics Summary
|
||||
|
||||
| Category | Count |
|
||||
|----------|-------|
|
||||
| Command Files | 6 |
|
||||
| Command Classes | 24+ |
|
||||
| Datum Creation Functions | 16 |
|
||||
| Icons Defined | 33 |
|
||||
| Toolbars Registered | 17 |
|
||||
| Menu Hierarchies | 7 major |
|
||||
| Native Commands Exposed | 66 |
|
||||
| Custom ZTools Commands | 14 |
|
||||
| Theme Colors | 26 |
|
||||
| Styled Widget Types | 50+ |
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
ZTools follows FreeCAD's contribution guidelines. Key areas needing help:
|
||||
|
||||
1. **Testing:** Manual testing on different platforms
|
||||
2. **Documentation:** User guides and tutorials
|
||||
3. **Translations:** Internationalization support
|
||||
4. **Bug Reports:** Issue tracking and reproduction
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3.0-or-later
|
||||
|
||||
Compatible with FreeCAD's licensing model.
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.3.0 (2026-01-25)
|
||||
- Added zSpreadsheet module with 9 formatting commands
|
||||
- Added Spreadsheet workbench integration (6 native commands)
|
||||
- Added 9 spreadsheet formatting icons
|
||||
|
||||
### v0.2.0 (2026-01-25)
|
||||
- Added Assembly workbench integration (21 native commands)
|
||||
- Added Assembly Linear Pattern tool
|
||||
- Added Assembly Polar Pattern tool
|
||||
- Added assembly pattern icons
|
||||
|
||||
### v0.1.0 (2026-01-24)
|
||||
- Initial release
|
||||
- Custom attachment system for datums
|
||||
- 16 datum creation functions
|
||||
- Datum Creator GUI with auto-detection
|
||||
- Rotated Linear Pattern
|
||||
- Enhanced Pocket (partial)
|
||||
- Catppuccin Mocha theme
|
||||
- 21 initial icons
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
787
ztools/ztools/commands/assembly_pattern_commands.py
Normal file
787
ztools/ztools/commands/assembly_pattern_commands.py
Normal 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())
|
||||
567
ztools/ztools/commands/spreadsheet_commands.py
Normal file
567
ztools/ztools/commands/spreadsheet_commands.py
Normal file
@@ -0,0 +1,567 @@
|
||||
# ztools/commands/spreadsheet_commands.py
|
||||
# Enhanced spreadsheet formatting tools for FreeCAD
|
||||
# Provides quick formatting toolbar for cell styling
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_active_spreadsheet():
|
||||
"""Get the currently active spreadsheet object and its view.
|
||||
|
||||
Returns:
|
||||
tuple: (sheet_object, sheet_view) or (None, None)
|
||||
"""
|
||||
doc = App.ActiveDocument
|
||||
if not doc:
|
||||
return None, None
|
||||
|
||||
# Get MDI area and active subwindow
|
||||
main_window = Gui.getMainWindow()
|
||||
mdi = main_window.centralWidget()
|
||||
if not mdi:
|
||||
return None, None
|
||||
|
||||
subwindow = mdi.activeSubWindow()
|
||||
if not subwindow:
|
||||
return None, None
|
||||
|
||||
# Get widget from subwindow
|
||||
widget = subwindow.widget()
|
||||
if not widget:
|
||||
return None, None
|
||||
|
||||
# Check if it's a spreadsheet view by looking for the table view
|
||||
# FreeCAD's spreadsheet view contains a QTableView
|
||||
table_view = None
|
||||
if hasattr(widget, "findChild"):
|
||||
table_view = widget.findChild(QtGui.QTableView)
|
||||
|
||||
if not table_view:
|
||||
# Try if widget itself is the table view
|
||||
if isinstance(widget, QtGui.QTableView):
|
||||
table_view = widget
|
||||
else:
|
||||
return None, None
|
||||
|
||||
# Get the spreadsheet object from window title
|
||||
# Window title format varies: "Spreadsheet" or "Spreadsheet - DocName"
|
||||
title = subwindow.windowTitle()
|
||||
sheet_name = title.split(" - ")[0].split(" : ")[0].strip()
|
||||
|
||||
# Try to find the sheet object
|
||||
sheet = doc.getObject(sheet_name)
|
||||
if sheet and sheet.TypeId == "Spreadsheet::Sheet":
|
||||
return sheet, table_view
|
||||
|
||||
# Fallback: search for any spreadsheet object
|
||||
for obj in doc.Objects:
|
||||
if obj.TypeId == "Spreadsheet::Sheet":
|
||||
return obj, table_view
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def get_selected_cells():
|
||||
"""Get list of selected cell addresses from active spreadsheet.
|
||||
|
||||
Returns:
|
||||
tuple: (sheet_object, list_of_cell_addresses) or (None, [])
|
||||
"""
|
||||
sheet, table_view = get_active_spreadsheet()
|
||||
if not sheet or not table_view:
|
||||
return None, []
|
||||
|
||||
# Get selection model
|
||||
selection_model = table_view.selectionModel()
|
||||
if not selection_model:
|
||||
return sheet, []
|
||||
|
||||
indexes = selection_model.selectedIndexes()
|
||||
if not indexes:
|
||||
return sheet, []
|
||||
|
||||
cells = []
|
||||
for idx in indexes:
|
||||
col = idx.column()
|
||||
row = idx.row()
|
||||
|
||||
# Convert to cell address (A1 notation)
|
||||
# Handle columns beyond Z (AA, AB, etc.)
|
||||
col_str = ""
|
||||
temp_col = col
|
||||
while temp_col >= 0:
|
||||
col_str = chr(65 + (temp_col % 26)) + col_str
|
||||
temp_col = temp_col // 26 - 1
|
||||
|
||||
cell_addr = f"{col_str}{row + 1}"
|
||||
cells.append(cell_addr)
|
||||
|
||||
return sheet, cells
|
||||
|
||||
|
||||
def column_to_index(col_str):
|
||||
"""Convert column string (A, B, ..., Z, AA, AB, ...) to index."""
|
||||
result = 0
|
||||
for char in col_str:
|
||||
result = result * 26 + (ord(char) - ord("A") + 1)
|
||||
return result - 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Style Commands (Bold, Italic, Underline)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleBold:
|
||||
"""Toggle bold style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_bold"),
|
||||
"MenuText": "Bold",
|
||||
"ToolTip": "Toggle bold style on selected cells (Ctrl+B)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Bold")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "bold" in styles:
|
||||
styles.discard("bold")
|
||||
else:
|
||||
styles.add("bold")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle bold: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleItalic:
|
||||
"""Toggle italic style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_italic"),
|
||||
"MenuText": "Italic",
|
||||
"ToolTip": "Toggle italic style on selected cells (Ctrl+I)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Italic")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "italic" in styles:
|
||||
styles.discard("italic")
|
||||
else:
|
||||
styles.add("italic")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle italic: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetStyleUnderline:
|
||||
"""Toggle underline style on selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_underline"),
|
||||
"MenuText": "Underline",
|
||||
"ToolTip": "Toggle underline style on selected cells (Ctrl+U)",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Toggle Underline")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
current = sheet.getStyle(cell) or ""
|
||||
styles = set(s.strip() for s in current.split("|") if s.strip())
|
||||
|
||||
if "underline" in styles:
|
||||
styles.discard("underline")
|
||||
else:
|
||||
styles.add("underline")
|
||||
|
||||
new_style = "|".join(sorted(styles)) if styles else ""
|
||||
sheet.setStyle(cell, new_style)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to toggle underline: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Alignment Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignLeft:
|
||||
"""Align cell content to the left."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_left"),
|
||||
"MenuText": "Align Left",
|
||||
"ToolTip": "Align selected cells to the left",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Left")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "left")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align left: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignCenter:
|
||||
"""Align cell content to the center."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_center"),
|
||||
"MenuText": "Align Center",
|
||||
"ToolTip": "Align selected cells to the center",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Center")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "center")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align center: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetAlignRight:
|
||||
"""Align cell content to the right."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_align_right"),
|
||||
"MenuText": "Align Right",
|
||||
"ToolTip": "Align selected cells to the right",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Align Right")
|
||||
|
||||
try:
|
||||
for cell in cells:
|
||||
sheet.setAlignment(cell, "right")
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to align right: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Color Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetBgColor:
|
||||
"""Set background color of selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_bg_color"),
|
||||
"MenuText": "Background Color",
|
||||
"ToolTip": "Set background color of selected cells",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
# Show color picker dialog
|
||||
color = QtGui.QColorDialog.getColor(
|
||||
QtCore.Qt.white, Gui.getMainWindow(), "Select Background Color"
|
||||
)
|
||||
|
||||
if not color.isValid():
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Set Background Color")
|
||||
|
||||
try:
|
||||
# FreeCAD expects RGB as tuple of floats 0-1
|
||||
rgb = (
|
||||
color.redF(),
|
||||
color.greenF(),
|
||||
color.blueF(),
|
||||
)
|
||||
|
||||
for cell in cells:
|
||||
sheet.setBackground(cell, rgb)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to set background color: {e}\n")
|
||||
|
||||
|
||||
class ZTools_SpreadsheetTextColor:
|
||||
"""Set text color of selected cells."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_text_color"),
|
||||
"MenuText": "Text Color",
|
||||
"ToolTip": "Set text color of selected cells",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
# Show color picker dialog with default white for dark theme
|
||||
initial_color = QtGui.QColor(205, 214, 244) # Catppuccin Mocha text color
|
||||
color = QtGui.QColorDialog.getColor(
|
||||
initial_color, Gui.getMainWindow(), "Select Text Color"
|
||||
)
|
||||
|
||||
if not color.isValid():
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Set Text Color")
|
||||
|
||||
try:
|
||||
# FreeCAD expects RGB as tuple of floats 0-1
|
||||
rgb = (
|
||||
color.redF(),
|
||||
color.greenF(),
|
||||
color.blueF(),
|
||||
)
|
||||
|
||||
for cell in cells:
|
||||
sheet.setForeground(cell, rgb)
|
||||
|
||||
doc.commitTransaction()
|
||||
doc.recompute()
|
||||
except Exception as e:
|
||||
doc.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to set text color: {e}\n")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Utility Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ZTools_SpreadsheetQuickAlias:
|
||||
"""Create alias from cell content."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("spreadsheet_quick_alias"),
|
||||
"MenuText": "Quick Alias",
|
||||
"ToolTip": "Create alias for selected cell based on adjacent label cell",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
return sheet is not None and len(cells) > 0
|
||||
|
||||
def Activated(self):
|
||||
sheet, cells = get_selected_cells()
|
||||
if not sheet or not cells:
|
||||
App.Console.PrintWarning("No cells selected\n")
|
||||
return
|
||||
|
||||
doc = App.ActiveDocument
|
||||
doc.openTransaction("Quick Alias")
|
||||
|
||||
try:
|
||||
aliases_created = 0
|
||||
|
||||
for cell in cells:
|
||||
# Parse cell address
|
||||
import re
|
||||
|
||||
match = re.match(r"([A-Z]+)(\d+)", cell)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
col_str = match.group(1)
|
||||
row = int(match.group(2))
|
||||
|
||||
# Get content of cell to the left (label cell)
|
||||
col_idx = column_to_index(col_str)
|
||||
if col_idx > 0:
|
||||
# Convert back to column string
|
||||
label_col_idx = col_idx - 1
|
||||
label_col_str = ""
|
||||
temp = label_col_idx
|
||||
while temp >= 0:
|
||||
label_col_str = chr(65 + (temp % 26)) + label_col_str
|
||||
temp = temp // 26 - 1
|
||||
|
||||
label_cell = f"{label_col_str}{row}"
|
||||
label_content = sheet.getContents(label_cell)
|
||||
|
||||
if label_content:
|
||||
# Clean the label to make a valid alias
|
||||
# Must be alphanumeric + underscore, start with letter
|
||||
alias = "".join(
|
||||
c if c.isalnum() or c == "_" else "_"
|
||||
for c in label_content
|
||||
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())
|
||||
Binary file not shown.
Binary file not shown.
@@ -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"]}"/>
|
||||
</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
|
||||
@@ -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():
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<!-- Direction arrow -->
|
||||
<line x1="4" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1.5" stroke-dasharray="3,2"/>
|
||||
<path d="M24 13 L28 16 L24 19" stroke="#7f849c" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (original) -->
|
||||
<rect x="4" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="7" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Component 2 -->
|
||||
<rect x="13" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="16" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Component 3 -->
|
||||
<rect x="22" y="10" width="6" height="6" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<circle cx="25" cy="13" r="1.5" fill="#94e2d5"/>
|
||||
<!-- Count indicator -->
|
||||
<text x="16" y="26" font-family="monospace" font-size="7" fill="#cdd6f4" text-anchor="middle">1-2-3</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<!-- Center axis indicator -->
|
||||
<circle cx="16" cy="16" r="2" fill="#fab387"/>
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="#7f849c" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M23 8 A9 9 0 0 1 26 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M25 11 L26 14 L23 14" stroke="#a6e3a1" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
<!-- Component 1 (top) -->
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#74c7ec" stroke="#89b4fa" stroke-width="1"/>
|
||||
<!-- Component 2 (right) -->
|
||||
<g transform="translate(16,16) rotate(72) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#cba6f7" stroke="#b4befe" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 3 (bottom-right) -->
|
||||
<g transform="translate(16,16) rotate(144) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f5c2e7" stroke="#f2cdcd" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 4 (bottom-left) -->
|
||||
<g transform="translate(16,16) rotate(216) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#fab387" stroke="#eba0ac" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Component 5 (left) -->
|
||||
<g transform="translate(16,16) rotate(288) translate(-16,-16)">
|
||||
<rect x="13" y="4" width="6" height="5" rx="1" fill="#f9e2af" stroke="#fab387" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user