first commit
109
CatppuccinMocha/CatppuccinMocha.cfg
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<FCParameters>
|
||||
<FCParamGroup Name="Root">
|
||||
<FCParamGroup Name="BaseApp">
|
||||
<FCParamGroup Name="Preferences">
|
||||
<FCParamGroup Name="Editor">
|
||||
<FCUInt Name="Text" Value="3453416703"/>
|
||||
<FCUInt Name="Bookmark" Value="3032415999"/>
|
||||
<FCUInt Name="Breakpoint" Value="4086016255"/>
|
||||
<FCUInt Name="Keyword" Value="3416717311"/>
|
||||
<FCUInt Name="Comment" Value="2139095295"/>
|
||||
<FCUInt Name="Block comment" Value="2139095295"/>
|
||||
<FCUInt Name="Number" Value="4206069759"/>
|
||||
<FCUInt Name="String" Value="2799935999"/>
|
||||
<FCUInt Name="Character" Value="4073902335"/>
|
||||
<FCUInt Name="Class name" Value="2310339327"/>
|
||||
<FCUInt Name="Define name" Value="2310339327"/>
|
||||
<FCUInt Name="Operator" Value="2312199935"/>
|
||||
<FCUInt Name="Python output" Value="2796290303"/>
|
||||
<FCUInt Name="Python error" Value="4086016255"/>
|
||||
<FCUInt Name="Current line highlight" Value="1162304255"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="OutputWindow">
|
||||
<FCUInt Name="colorText" Value="3453416703"/>
|
||||
<FCUInt Name="colorLogging" Value="2497893887"/>
|
||||
<FCUInt Name="colorWarning" Value="4192382975"/>
|
||||
<FCUInt Name="colorError" Value="4086016255"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="View">
|
||||
<FCUInt Name="BackgroundColor" Value="505294591"/>
|
||||
<FCUInt Name="BackgroundColor2" Value="286333951"/>
|
||||
<FCUInt Name="BackgroundColor3" Value="404235775"/>
|
||||
<FCUInt Name="BackgroundColor4" Value="825378047"/>
|
||||
<FCBool Name="Simple" Value="0"/>
|
||||
<FCBool Name="Gradient" Value="1"/>
|
||||
<FCBool Name="UseBackgroundColorMid" Value="0"/>
|
||||
<FCUInt Name="HighlightColor" Value="3416717311"/>
|
||||
<FCUInt Name="SelectionColor" Value="3032415999"/>
|
||||
<FCUInt Name="PreselectColor" Value="2497893887"/>
|
||||
<FCUInt Name="DefaultShapeColor" Value="1482387711"/>
|
||||
<FCBool Name="RandomColor" Value="0"/>
|
||||
<FCUInt Name="DefaultShapeLineColor" Value="2470768383"/>
|
||||
<FCUInt Name="DefaultShapeVertexColor" Value="2470768383"/>
|
||||
<FCUInt Name="BoundingBoxColor" Value="1819509759"/>
|
||||
<FCUInt Name="AnnotationTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="SketchEdgeColor" Value="3453416703"/>
|
||||
<FCUInt Name="SketchVertexColor" Value="3453416703"/>
|
||||
<FCUInt Name="EditedEdgeColor" Value="3416717311"/>
|
||||
<FCUInt Name="EditedVertexColor" Value="4123402495"/>
|
||||
<FCUInt Name="ConstructionColor" Value="4206069759"/>
|
||||
<FCUInt Name="ExternalColor" Value="4192382975"/>
|
||||
<FCUInt Name="FullyConstrainedColor" Value="2799935999"/>
|
||||
<FCUInt Name="InternalAlignedGeoColor" Value="1959907071"/>
|
||||
<FCUInt Name="FullyConstraintElementColor" Value="2799935999"/>
|
||||
<FCUInt Name="FullyConstraintConstructionElementColor" Value="2497893887"/>
|
||||
<FCUInt Name="FullyConstraintInternalAlignmentColor" Value="2312199935"/>
|
||||
<FCUInt Name="FullyConstraintConstructionPointColor" Value="2799935999"/>
|
||||
<FCUInt Name="ConstrainedIcoColor" Value="2310339327"/>
|
||||
<FCUInt Name="NonDrivingConstrDimColor" Value="2139095295"/>
|
||||
<FCUInt Name="ConstrainedDimColor" Value="3416717311"/>
|
||||
<FCUInt Name="ExprBasedConstrDimColor" Value="4206069759"/>
|
||||
<FCUInt Name="DeactivatedConstrDimColor" Value="1819509759"/>
|
||||
<FCUInt Name="CursorTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="CursorCrosshairColor" Value="3416717311"/>
|
||||
<FCUInt Name="CreateLineColor" Value="2799935999"/>
|
||||
<FCUInt Name="ShadowLightColor" Value="2470768128"/>
|
||||
<FCUInt Name="ShadowGroundColor" Value="286333952"/>
|
||||
<FCUInt Name="HiddenLineColor" Value="825378047"/>
|
||||
<FCUInt Name="HiddenLineFaceColor" Value="505294591"/>
|
||||
<FCUInt Name="HiddenLineBackground" Value="505294591"/>
|
||||
<FCBool Name="EnableBacklight" Value="1"/>
|
||||
<FCUInt Name="BacklightColor" Value="1162304255"/>
|
||||
<FCFloat Name="BacklightIntensity" Value="0.30"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="TreeView">
|
||||
<FCUInt Name="TreeEditColor" Value="3416717311"/>
|
||||
<FCUInt Name="TreeActiveColor" Value="2799935999"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="MainWindow">
|
||||
<FCText Name="StyleSheet">CatppuccinMocha.qss</FCText>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Mod">
|
||||
<FCParamGroup Name="Start">
|
||||
<FCUInt Name="BackgroundColor1" Value="404235775"/>
|
||||
<FCUInt Name="BackgroundTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="PageColor" Value="505294591"/>
|
||||
<FCUInt Name="PageTextColor" Value="3453416703"/>
|
||||
<FCUInt Name="BoxColor" Value="825378047"/>
|
||||
<FCUInt Name="LinkColor" Value="2310339327"/>
|
||||
<FCUInt Name="BackgroundColor2" Value="286333951"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Part">
|
||||
<FCUInt Name="VertexColor" Value="3032415999"/>
|
||||
<FCUInt Name="EdgeColor" Value="2310339327"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="PartDesign">
|
||||
<FCUInt Name="DefaultDatumColor" Value="3416717311"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Draft">
|
||||
<FCUInt Name="snapcolor" Value="2799935999"/>
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="Sketcher">
|
||||
<FCUInt Name="GridLineColor" Value="1162304255"/>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParamGroup>
|
||||
</FCParameters>
|
||||
1249
CatppuccinMocha/CatppuccinMocha.qss
Normal file
89
Makefile
Normal file
@@ -0,0 +1,89 @@
|
||||
# Makefile for ZTools FreeCAD Workbench
|
||||
# Installs to local flatpak FreeCAD instance
|
||||
|
||||
WORKBENCH_NAME := ZTools
|
||||
FLATPAK_APP := org.freecad.FreeCAD
|
||||
|
||||
# FreeCAD Mod directory for flatpak
|
||||
FLATPAK_MOD_DIR := $(HOME)/.var/app/$(FLATPAK_APP)/data/FreeCAD/Mod
|
||||
INSTALL_DIR := $(FLATPAK_MOD_DIR)/$(WORKBENCH_NAME)
|
||||
|
||||
.PHONY: all install uninstall clean check-flatpak list dev-install dev-uninstall run help
|
||||
|
||||
all: install
|
||||
|
||||
# Check if flatpak FreeCAD is installed
|
||||
check-flatpak:
|
||||
@if ! flatpak list | grep -q $(FLATPAK_APP); then \
|
||||
echo "Error: FreeCAD flatpak ($(FLATPAK_APP)) is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Install the workbench
|
||||
install: check-flatpak
|
||||
@echo "Installing $(WORKBENCH_NAME) workbench to flatpak FreeCAD..."
|
||||
@mkdir -p $(INSTALL_DIR)/ztools/commands
|
||||
@mkdir -p $(INSTALL_DIR)/ztools/datums
|
||||
@mkdir -p $(INSTALL_DIR)/ztools/resources/icons
|
||||
@cp -v package.xml $(INSTALL_DIR)/
|
||||
@cp -v ztools/InitGui.py $(INSTALL_DIR)/
|
||||
@cp -v ztools/ztools/*.py $(INSTALL_DIR)/ztools/
|
||||
@cp -v ztools/ztools/commands/*.py $(INSTALL_DIR)/ztools/commands/
|
||||
@cp -v ztools/ztools/datums/*.py $(INSTALL_DIR)/ztools/datums/
|
||||
@cp -v ztools/ztools/resources/*.py $(INSTALL_DIR)/ztools/resources/
|
||||
@cp -v ztools/ztools/resources/icons/*.svg $(INSTALL_DIR)/ztools/resources/icons/
|
||||
@echo "Installation complete!"
|
||||
@echo "Restart FreeCAD to load the workbench."
|
||||
|
||||
# Uninstall the workbench
|
||||
uninstall:
|
||||
@echo "Uninstalling $(WORKBENCH_NAME) workbench..."
|
||||
@rm -rf $(INSTALL_DIR)
|
||||
@echo "Uninstallation complete!"
|
||||
|
||||
# Clean build artifacts (if any)
|
||||
clean:
|
||||
@find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
@echo "Cleaned build artifacts."
|
||||
|
||||
# List installed files
|
||||
list:
|
||||
@echo "Installed files in $(INSTALL_DIR):"
|
||||
@find $(INSTALL_DIR) -type f 2>/dev/null || echo "Workbench not installed."
|
||||
|
||||
# Development: install with symlink for live editing
|
||||
dev-install: check-flatpak
|
||||
@echo "Installing $(WORKBENCH_NAME) workbench (dev mode with symlink)..."
|
||||
@mkdir -p $(FLATPAK_MOD_DIR)
|
||||
@rm -rf $(INSTALL_DIR)
|
||||
@ln -sfv $(CURDIR) $(INSTALL_DIR)
|
||||
@echo "Dev installation complete (symlinked)!"
|
||||
@echo "Changes to source files will be reflected immediately (restart FreeCAD to reload)."
|
||||
|
||||
# Remove dev symlink
|
||||
dev-uninstall:
|
||||
@echo "Removing dev symlink..."
|
||||
@rm -f $(INSTALL_DIR)
|
||||
@echo "Dev uninstallation complete!"
|
||||
|
||||
# Run FreeCAD flatpak
|
||||
run:
|
||||
@echo "Starting FreeCAD flatpak..."
|
||||
@flatpak run $(FLATPAK_APP)
|
||||
|
||||
# Show help
|
||||
help:
|
||||
@echo "ZTools Workbench Makefile"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " install - Install workbench to flatpak FreeCAD"
|
||||
@echo " uninstall - Remove workbench from flatpak FreeCAD"
|
||||
@echo " dev-install - Install as symlink for development"
|
||||
@echo " dev-uninstall- Remove development symlink"
|
||||
@echo " clean - Remove Python cache files"
|
||||
@echo " list - List installed files"
|
||||
@echo " run - Start FreeCAD flatpak"
|
||||
@echo " help - Show this help message"
|
||||
@echo ""
|
||||
@echo "Install directory: $(INSTALL_DIR)"
|
||||
58
TODO_ATTACHMENT_WORK.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Datum Attachment Work - In Progress
|
||||
|
||||
## Context
|
||||
Implementing proper FreeCAD attachment for datum objects to avoid "deactivated attachment mode" warnings.
|
||||
The pattern is adding `source_object` and `source_subname` parameters to each datum function and using `_setup_datum_attachment()` with appropriate MapModes.
|
||||
|
||||
## Completed Functions (in core.py)
|
||||
|
||||
### Planes
|
||||
- `plane_offset_from_face` - MapMode='FlatFace'
|
||||
- `plane_midplane` - MapMode='TwoFace'
|
||||
- `plane_from_3_points` - MapMode='ThreePointPlane'
|
||||
- `plane_normal_to_edge` - MapMode='NormalToPath'
|
||||
- `plane_angled` - MapMode='FlatFace' with rotation offset
|
||||
- `plane_tangent_to_cylinder` - MapMode='Tangent'
|
||||
|
||||
### Axes
|
||||
- `axis_from_2_points` - MapMode='TwoPointLine'
|
||||
- `axis_from_edge` - MapMode='ObjectXY'
|
||||
- `axis_cylinder_center` - MapMode='ObjectZ'
|
||||
- `axis_intersection_planes` - MapMode='TwoFace'
|
||||
|
||||
### Points
|
||||
- `point_at_vertex` - MapMode='Vertex'
|
||||
|
||||
## Remaining Functions to Update (in core.py)
|
||||
|
||||
- `point_at_coordinates` - No attachment needed (explicit coordinates), but could use 'Translate' mode
|
||||
- `point_on_edge` - Use MapMode='OnEdge' with MapPathParameter for position
|
||||
- `point_center_of_face` - Use MapMode='CenterOfCurvature' or similar
|
||||
- `point_center_of_circle` - Use MapMode='CenterOfCurvature'
|
||||
|
||||
## After core.py Updates
|
||||
|
||||
Update `datum_commands.py` to pass source references to the remaining point functions:
|
||||
- `create_point_at_vertex` - already done
|
||||
- `create_point_on_edge` - needs update
|
||||
- `create_point_center_face` - needs update
|
||||
- `create_point_center_circle` - needs update
|
||||
|
||||
## Pattern for Updates
|
||||
|
||||
1. Add parameters to function signature:
|
||||
```python
|
||||
source_object: Optional[App.DocumentObject] = None,
|
||||
source_subname: Optional[str] = None,
|
||||
```
|
||||
|
||||
2. In the body section, use attachment instead of placement:
|
||||
```python
|
||||
if source_object and source_subname:
|
||||
support = [(source_object, source_subname)]
|
||||
_setup_datum_attachment(point, support, "MapMode")
|
||||
else:
|
||||
_setup_datum_placement(point, App.Placement(...))
|
||||
```
|
||||
|
||||
3. Update datum_commands.py to extract and pass source references from selection.
|
||||
32
package.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
|
||||
|
||||
<name>ZTools</name>
|
||||
|
||||
<description>Extended PartDesign workbench with velocity-focused tools, advanced datum creation, and Catppuccin Mocha theme.</description>
|
||||
|
||||
<version>0.1.0</version>
|
||||
|
||||
<date>2026-01-24</date>
|
||||
|
||||
<license file="LICENSE">LGPL-3.0-or-later</license>
|
||||
|
||||
<content>
|
||||
<workbench>
|
||||
<name>ZTools</name>
|
||||
<classname>ZToolsWorkbench</classname>
|
||||
<subdirectory>./ztools</subdirectory>
|
||||
</workbench>
|
||||
<preferencepack>
|
||||
<name>CatppuccinMocha</name>
|
||||
<description>Catppuccin Mocha dark theme - soothing pastel colors for the high-spirited</description>
|
||||
<subdirectory>./CatppuccinMocha</subdirectory>
|
||||
<tag>color</tag>
|
||||
<tag>dark</tag>
|
||||
<tag>catppuccin</tag>
|
||||
<tag>mocha</tag>
|
||||
<tag>theme</tag>
|
||||
</preferencepack>
|
||||
</content>
|
||||
|
||||
</package>
|
||||
591
partdesign.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# FreeCAD 1.0.2 PartDesign Workbench Command Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The PartDesign Workbench uses a **feature-based parametric methodology** where a component is represented by a Body container. Features are cumulative—each builds on the result of preceding features. Most features are based on parametric sketches and are either additive (adding material) or subtractive (removing material).
|
||||
|
||||
FreeCAD 1.0 introduced significant improvements including **Topological Naming Problem (TNP) mitigation**, making parametric models more stable when earlier features are modified.
|
||||
|
||||
---
|
||||
|
||||
## Structure & Containers
|
||||
|
||||
### Body
|
||||
The fundamental container for PartDesign features. Defines a local coordinate system and contains all features that define a single solid component.
|
||||
|
||||
```python
|
||||
body = doc.addObject('PartDesign::Body', 'Body')
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `Tip` — The feature representing the current state of the body
|
||||
- `BaseFeature` — Optional external solid to build upon
|
||||
- `Origin` — Contains reference planes (XY, XZ, YZ) and axes (X, Y, Z)
|
||||
|
||||
### Part Container
|
||||
Groups multiple Bodies for organization. Not a PartDesign-specific object but commonly used.
|
||||
|
||||
```python
|
||||
part = doc.addObject('App::Part', 'Part')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sketch Tools
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **Create Sketch** | Creates a new sketch on a selected face or datum plane |
|
||||
| **Attach Sketch** | Attaches a sketch to geometry from the active body |
|
||||
| **Edit Sketch** | Opens selected sketch for editing |
|
||||
| **Validate Sketch** | Verifies tolerance of points and adjusts them |
|
||||
| **Check Geometry** | Checks geometry for errors |
|
||||
|
||||
```python
|
||||
# Create sketch attached to XY plane
|
||||
sketch = body.newObject('Sketcher::SketchObject', 'Sketch')
|
||||
sketch.AttachmentSupport = [(body.getObject('Origin').getObject('XY_Plane'), '')]
|
||||
sketch.MapMode = 'FlatFace'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Geometry (Datums)
|
||||
|
||||
### Datum Plane
|
||||
Creates a reference plane for sketch attachment or as a mirror/pattern reference.
|
||||
|
||||
```python
|
||||
plane = body.newObject('PartDesign::Plane', 'DatumPlane')
|
||||
plane.AttachmentSupport = [(face_reference, '')]
|
||||
plane.MapMode = 'FlatFace'
|
||||
plane.Offset = App.Vector(0, 0, 10) # Offset along normal
|
||||
```
|
||||
|
||||
### Datum Line
|
||||
Creates a reference axis for revolutions, grooves, or patterns.
|
||||
|
||||
```python
|
||||
line = body.newObject('PartDesign::Line', 'DatumLine')
|
||||
line.AttachmentSupport = [(edge_reference, '')]
|
||||
line.MapMode = 'ObjectXY'
|
||||
```
|
||||
|
||||
### Datum Point
|
||||
Creates a reference point for geometry attachment.
|
||||
|
||||
```python
|
||||
point = body.newObject('PartDesign::Point', 'DatumPoint')
|
||||
point.AttachmentSupport = [(vertex_reference, '')]
|
||||
```
|
||||
|
||||
### Local Coordinate System
|
||||
Creates a local coordinate system (LCS) attached to datum geometry.
|
||||
|
||||
```python
|
||||
lcs = body.newObject('PartDesign::CoordinateSystem', 'LocalCS')
|
||||
```
|
||||
|
||||
### Shape Binder
|
||||
References geometry from a single parent object.
|
||||
|
||||
```python
|
||||
binder = body.newObject('PartDesign::ShapeBinder', 'ShapeBinder')
|
||||
binder.Support = [(external_object, ['Face1'])]
|
||||
```
|
||||
|
||||
### SubShapeBinder
|
||||
References geometry from one or more parent objects (more flexible than ShapeBinder).
|
||||
|
||||
```python
|
||||
subbinder = body.newObject('PartDesign::SubShapeBinder', 'SubShapeBinder')
|
||||
subbinder.Support = [(obj1, ['Face1']), (obj2, ['Edge2'])]
|
||||
```
|
||||
|
||||
### Clone
|
||||
Creates a clone of a selected body.
|
||||
|
||||
```python
|
||||
clone = doc.addObject('PartDesign::FeatureBase', 'Clone')
|
||||
clone.BaseFeature = source_body
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additive Features (Add Material)
|
||||
|
||||
### Pad
|
||||
Extrudes a sketch profile to create a solid.
|
||||
|
||||
```python
|
||||
pad = body.newObject('PartDesign::Pad', 'Pad')
|
||||
pad.Profile = sketch
|
||||
pad.Length = 20.0
|
||||
pad.Type = 0 # 0=Dimension, 1=UpToLast, 2=UpToFirst, 3=UpToFace, 4=TwoLengths, 5=UpToShape
|
||||
pad.Reversed = False
|
||||
pad.Midplane = False
|
||||
pad.Symmetric = False
|
||||
pad.Length2 = 10.0 # For TwoLengths type
|
||||
pad.UseCustomVector = False
|
||||
pad.Direction = App.Vector(0, 0, 1)
|
||||
pad.TaperAngle = 0.0 # Draft angle (new in 1.0)
|
||||
pad.TaperAngle2 = 0.0
|
||||
```
|
||||
|
||||
**Type Options:**
|
||||
| Value | Mode | Description |
|
||||
|-------|------|-------------|
|
||||
| 0 | Dimension | Fixed length |
|
||||
| 1 | UpToLast | Extends to last face in direction |
|
||||
| 2 | UpToFirst | Extends to first face encountered |
|
||||
| 3 | UpToFace | Extends to selected face |
|
||||
| 4 | TwoLengths | Extends in both directions |
|
||||
| 5 | UpToShape | Extends to selected shape (new in 1.0) |
|
||||
|
||||
### Revolution
|
||||
Creates a solid by revolving a sketch around an axis.
|
||||
|
||||
```python
|
||||
revolution = body.newObject('PartDesign::Revolution', 'Revolution')
|
||||
revolution.Profile = sketch
|
||||
revolution.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
revolution.Angle = 360.0
|
||||
revolution.Midplane = False
|
||||
revolution.Reversed = False
|
||||
```
|
||||
|
||||
### Additive Loft
|
||||
Creates a solid by transitioning between two or more sketch profiles.
|
||||
|
||||
```python
|
||||
loft = body.newObject('PartDesign::AdditiveLoft', 'AdditiveLoft')
|
||||
loft.Profile = sketch1
|
||||
loft.Sections = [sketch2, sketch3]
|
||||
loft.Ruled = False
|
||||
loft.Closed = False
|
||||
```
|
||||
|
||||
### Additive Pipe (Sweep)
|
||||
Creates a solid by sweeping a profile along a path.
|
||||
|
||||
```python
|
||||
pipe = body.newObject('PartDesign::AdditivePipe', 'AdditivePipe')
|
||||
pipe.Profile = profile_sketch
|
||||
pipe.Spine = path_sketch # or (object, ['Edge1', 'Edge2'])
|
||||
pipe.Transition = 0 # 0=Transformed, 1=RightCorner, 2=RoundCorner
|
||||
pipe.Mode = 0 # 0=Standard, 1=Fixed, 2=Frenet, 3=Auxiliary
|
||||
pipe.Auxiliary = None # Auxiliary spine for Mode=3
|
||||
```
|
||||
|
||||
### Additive Helix
|
||||
Creates a solid by sweeping a sketch along a helix.
|
||||
|
||||
```python
|
||||
helix = body.newObject('PartDesign::AdditiveHelix', 'AdditiveHelix')
|
||||
helix.Profile = sketch
|
||||
helix.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
helix.Pitch = 5.0
|
||||
helix.Height = 30.0
|
||||
helix.Turns = 6.0
|
||||
helix.Mode = 0 # 0=pitch-height, 1=pitch-turns, 2=height-turns
|
||||
helix.LeftHanded = False
|
||||
helix.Reversed = False
|
||||
helix.Angle = 0.0 # Taper angle
|
||||
helix.Growth = 0.0 # Radius growth per turn
|
||||
```
|
||||
|
||||
### Additive Primitives
|
||||
Direct primitive creation without sketches.
|
||||
|
||||
```python
|
||||
# Box
|
||||
box = body.newObject('PartDesign::AdditiveBox', 'Box')
|
||||
box.Length = 10.0
|
||||
box.Width = 10.0
|
||||
box.Height = 10.0
|
||||
|
||||
# Cylinder
|
||||
cyl = body.newObject('PartDesign::AdditiveCylinder', 'Cylinder')
|
||||
cyl.Radius = 5.0
|
||||
cyl.Height = 20.0
|
||||
cyl.Angle = 360.0
|
||||
|
||||
# Sphere
|
||||
sphere = body.newObject('PartDesign::AdditiveSphere', 'Sphere')
|
||||
sphere.Radius = 10.0
|
||||
sphere.Angle1 = -90.0
|
||||
sphere.Angle2 = 90.0
|
||||
sphere.Angle3 = 360.0
|
||||
|
||||
# Cone
|
||||
cone = body.newObject('PartDesign::AdditiveCone', 'Cone')
|
||||
cone.Radius1 = 10.0
|
||||
cone.Radius2 = 5.0
|
||||
cone.Height = 15.0
|
||||
cone.Angle = 360.0
|
||||
|
||||
# Ellipsoid
|
||||
ellipsoid = body.newObject('PartDesign::AdditiveEllipsoid', 'Ellipsoid')
|
||||
ellipsoid.Radius1 = 10.0
|
||||
ellipsoid.Radius2 = 5.0
|
||||
ellipsoid.Radius3 = 8.0
|
||||
|
||||
# Torus
|
||||
torus = body.newObject('PartDesign::AdditiveTorus', 'Torus')
|
||||
torus.Radius1 = 20.0
|
||||
torus.Radius2 = 5.0
|
||||
|
||||
# Prism
|
||||
prism = body.newObject('PartDesign::AdditivePrism', 'Prism')
|
||||
prism.Polygon = 6
|
||||
prism.Circumradius = 10.0
|
||||
prism.Height = 20.0
|
||||
|
||||
# Wedge
|
||||
wedge = body.newObject('PartDesign::AdditiveWedge', 'Wedge')
|
||||
wedge.Xmin = 0.0
|
||||
wedge.Xmax = 10.0
|
||||
wedge.Ymin = 0.0
|
||||
wedge.Ymax = 10.0
|
||||
wedge.Zmin = 0.0
|
||||
wedge.Zmax = 10.0
|
||||
wedge.X2min = 2.0
|
||||
wedge.X2max = 8.0
|
||||
wedge.Z2min = 2.0
|
||||
wedge.Z2max = 8.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Subtractive Features (Remove Material)
|
||||
|
||||
### Pocket
|
||||
Cuts material by extruding a sketch inward.
|
||||
|
||||
```python
|
||||
pocket = body.newObject('PartDesign::Pocket', 'Pocket')
|
||||
pocket.Profile = sketch
|
||||
pocket.Length = 15.0
|
||||
pocket.Type = 0 # Same options as Pad, plus 1=ThroughAll
|
||||
pocket.Reversed = False
|
||||
pocket.Midplane = False
|
||||
pocket.Symmetric = False
|
||||
pocket.TaperAngle = 0.0
|
||||
```
|
||||
|
||||
### Hole
|
||||
Creates parametric holes with threading options.
|
||||
|
||||
```python
|
||||
hole = body.newObject('PartDesign::Hole', 'Hole')
|
||||
hole.Profile = sketch # Sketch with center points
|
||||
hole.Diameter = 6.0
|
||||
hole.Depth = 15.0
|
||||
hole.DepthType = 0 # 0=Dimension, 1=ThroughAll
|
||||
hole.Threaded = True
|
||||
hole.ThreadType = 0 # 0=None, 1=ISOMetricCoarse, 2=ISOMetricFine, 3=UNC, 4=UNF, 5=NPT, etc.
|
||||
hole.ThreadSize = 'M6'
|
||||
hole.ThreadFit = 0 # 0=Standard, 1=Close
|
||||
hole.ThreadDirection = 0 # 0=Right, 1=Left
|
||||
hole.HoleCutType = 0 # 0=None, 1=Counterbore, 2=Countersink
|
||||
hole.HoleCutDiameter = 10.0
|
||||
hole.HoleCutDepth = 3.0
|
||||
hole.HoleCutCountersinkAngle = 90.0
|
||||
hole.DrillPoint = 0 # 0=Flat, 1=Angled
|
||||
hole.DrillPointAngle = 118.0
|
||||
hole.DrillForDepth = False
|
||||
```
|
||||
|
||||
**Thread Types:**
|
||||
- ISO Metric Coarse/Fine
|
||||
- UNC/UNF (Unified National)
|
||||
- NPT/NPTF (National Pipe Thread)
|
||||
- BSW/BSF (British Standard)
|
||||
- UTS (Unified Thread Standard)
|
||||
|
||||
### Groove
|
||||
Creates a cut by revolving a sketch around an axis (subtractive revolution).
|
||||
|
||||
```python
|
||||
groove = body.newObject('PartDesign::Groove', 'Groove')
|
||||
groove.Profile = sketch
|
||||
groove.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
groove.Angle = 360.0
|
||||
groove.Midplane = False
|
||||
groove.Reversed = False
|
||||
```
|
||||
|
||||
### Subtractive Loft
|
||||
Cuts by transitioning between profiles.
|
||||
|
||||
```python
|
||||
subloft = body.newObject('PartDesign::SubtractiveLoft', 'SubtractiveLoft')
|
||||
subloft.Profile = sketch1
|
||||
subloft.Sections = [sketch2]
|
||||
```
|
||||
|
||||
### Subtractive Pipe
|
||||
Cuts by sweeping a profile along a path.
|
||||
|
||||
```python
|
||||
subpipe = body.newObject('PartDesign::SubtractivePipe', 'SubtractivePipe')
|
||||
subpipe.Profile = profile_sketch
|
||||
subpipe.Spine = path_sketch
|
||||
```
|
||||
|
||||
### Subtractive Helix
|
||||
Cuts by sweeping along a helix (e.g., for threads).
|
||||
|
||||
```python
|
||||
subhelix = body.newObject('PartDesign::SubtractiveHelix', 'SubtractiveHelix')
|
||||
subhelix.Profile = thread_profile_sketch
|
||||
subhelix.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
subhelix.Pitch = 1.0
|
||||
subhelix.Height = 10.0
|
||||
```
|
||||
|
||||
### Subtractive Primitives
|
||||
Same primitives as additive, but subtract material:
|
||||
- `PartDesign::SubtractiveBox`
|
||||
- `PartDesign::SubtractiveCylinder`
|
||||
- `PartDesign::SubtractiveSphere`
|
||||
- `PartDesign::SubtractiveCone`
|
||||
- `PartDesign::SubtractiveEllipsoid`
|
||||
- `PartDesign::SubtractiveTorus`
|
||||
- `PartDesign::SubtractivePrism`
|
||||
- `PartDesign::SubtractiveWedge`
|
||||
|
||||
---
|
||||
|
||||
## Transformation Features (Patterns)
|
||||
|
||||
### Mirrored
|
||||
Creates a mirror copy of features across a plane.
|
||||
|
||||
```python
|
||||
mirrored = body.newObject('PartDesign::Mirrored', 'Mirrored')
|
||||
mirrored.Originals = [pad, pocket]
|
||||
mirrored.MirrorPlane = (body.getObject('Origin').getObject('XZ_Plane'), [''])
|
||||
```
|
||||
|
||||
### Linear Pattern
|
||||
Creates copies in a linear arrangement.
|
||||
|
||||
```python
|
||||
linear = body.newObject('PartDesign::LinearPattern', 'LinearPattern')
|
||||
linear.Originals = [pocket]
|
||||
linear.Direction = (body.getObject('Origin').getObject('X_Axis'), [''])
|
||||
linear.Length = 100.0
|
||||
linear.Occurrences = 5
|
||||
linear.Mode = 0 # 0=OverallLength, 1=Offset
|
||||
```
|
||||
|
||||
### Polar Pattern
|
||||
Creates copies in a circular arrangement.
|
||||
|
||||
```python
|
||||
polar = body.newObject('PartDesign::PolarPattern', 'PolarPattern')
|
||||
polar.Originals = [pocket]
|
||||
polar.Axis = (body.getObject('Origin').getObject('Z_Axis'), [''])
|
||||
polar.Angle = 360.0
|
||||
polar.Occurrences = 6
|
||||
polar.Mode = 0 # 0=OverallAngle, 1=Offset
|
||||
```
|
||||
|
||||
### MultiTransform
|
||||
Combines multiple transformations (mirrored, linear, polar, scaled).
|
||||
|
||||
```python
|
||||
multi = body.newObject('PartDesign::MultiTransform', 'MultiTransform')
|
||||
multi.Originals = [pocket]
|
||||
|
||||
# Add transformations (created within MultiTransform)
|
||||
# Typically done via GUI or by setting Transformations property
|
||||
multi.Transformations = [mirrored_transform, linear_transform]
|
||||
```
|
||||
|
||||
### Scaled
|
||||
Scales features (only available within MultiTransform).
|
||||
|
||||
```python
|
||||
# Only accessible as part of MultiTransform
|
||||
scaled = body.newObject('PartDesign::Scaled', 'Scaled')
|
||||
scaled.Factor = 0.5
|
||||
scaled.Occurrences = 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dress-Up Features (Edge/Face Treatment)
|
||||
|
||||
### Fillet
|
||||
Rounds edges with a specified radius.
|
||||
|
||||
```python
|
||||
fillet = body.newObject('PartDesign::Fillet', 'Fillet')
|
||||
fillet.Base = (pad, ['Edge1', 'Edge5', 'Edge9'])
|
||||
fillet.Radius = 2.0
|
||||
```
|
||||
|
||||
### Chamfer
|
||||
Bevels edges.
|
||||
|
||||
```python
|
||||
chamfer = body.newObject('PartDesign::Chamfer', 'Chamfer')
|
||||
chamfer.Base = (pad, ['Edge2', 'Edge6'])
|
||||
chamfer.ChamferType = 'Equal Distance' # or 'Two Distances' or 'Distance and Angle'
|
||||
chamfer.Size = 1.5
|
||||
chamfer.Size2 = 2.0 # For asymmetric
|
||||
chamfer.Angle = 45.0 # For 'Distance and Angle'
|
||||
```
|
||||
|
||||
### Draft
|
||||
Applies angular draft to faces (for mold release).
|
||||
|
||||
```python
|
||||
draft = body.newObject('PartDesign::Draft', 'Draft')
|
||||
draft.Base = (pad, ['Face2', 'Face4'])
|
||||
draft.Angle = 3.0 # Degrees
|
||||
draft.NeutralPlane = (body.getObject('Origin').getObject('XY_Plane'), [''])
|
||||
draft.PullDirection = App.Vector(0, 0, 1)
|
||||
draft.Reversed = False
|
||||
```
|
||||
|
||||
### Thickness
|
||||
Creates a shell by hollowing out a solid, keeping selected faces open.
|
||||
|
||||
```python
|
||||
thickness = body.newObject('PartDesign::Thickness', 'Thickness')
|
||||
thickness.Base = (pad, ['Face6']) # Faces to remove (open)
|
||||
thickness.Value = 2.0 # Wall thickness
|
||||
thickness.Mode = 0 # 0=Skin, 1=Pipe, 2=RectoVerso
|
||||
thickness.Join = 0 # 0=Arc, 1=Intersection
|
||||
thickness.Reversed = False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Boolean Operations
|
||||
|
||||
### Boolean
|
||||
Imports bodies and applies boolean operations.
|
||||
|
||||
```python
|
||||
boolean = body.newObject('PartDesign::Boolean', 'Boolean')
|
||||
boolean.Type = 0 # 0=Fuse, 1=Cut, 2=Common (intersection)
|
||||
boolean.Bodies = [other_body1, other_body2]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Menu Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **Set Tip** | Sets selected feature as the body's current state (tip) |
|
||||
| **Move object to other body** | Transfers feature to a different body |
|
||||
| **Move object after other object** | Reorders features in the tree |
|
||||
| **Appearance** | Sets color and transparency |
|
||||
| **Color per face** | Assigns different colors to individual faces |
|
||||
|
||||
```python
|
||||
# Set tip programmatically
|
||||
body.Tip = pocket
|
||||
|
||||
# Move feature order
|
||||
doc.moveObject(feature, body, after_feature)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Tools
|
||||
|
||||
### Sprocket
|
||||
Creates a sprocket profile for chain drives.
|
||||
|
||||
```python
|
||||
# Available via Gui.runCommand('PartDesign_Sprocket')
|
||||
```
|
||||
|
||||
### Involute Gear
|
||||
Creates an involute gear profile.
|
||||
|
||||
```python
|
||||
# Available via Gui.runCommand('PartDesign_InvoluteGear')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Properties (All Features)
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Label` | String | User-visible name |
|
||||
| `Placement` | Placement | Position and orientation |
|
||||
| `BaseFeature` | Link | Feature this builds upon |
|
||||
| `Shape` | Shape | Resulting geometry |
|
||||
|
||||
---
|
||||
|
||||
## Expression Binding
|
||||
|
||||
All dimensional properties can be driven by expressions:
|
||||
|
||||
```python
|
||||
pad.setExpression('Length', 'Spreadsheet.plate_height')
|
||||
fillet.setExpression('Radius', 'Spreadsheet.fillet_r * 0.5')
|
||||
hole.setExpression('Diameter', '<<Parameters>>.hole_dia')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always work within a Body** — PartDesign features require a body container
|
||||
2. **Use fully constrained sketches** — Prevents unexpected behavior when parameters change
|
||||
3. **Leverage datum geometry** — Creates stable references that survive TNP issues
|
||||
4. **Name constraints** — Enables expression-based parametric design
|
||||
5. **Use spreadsheets** — Centralizes parameters for easy modification
|
||||
6. **Set meaningful Labels** — Internal Names are auto-generated; Labels are user-friendly
|
||||
7. **Check isSolid()** — Before subtractive operations, verify the body has solid geometry
|
||||
|
||||
```python
|
||||
if not body.isSolid():
|
||||
raise ValueError("Body must contain solid geometry for subtractive features")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FreeCAD 1.0 Changes
|
||||
|
||||
| Change | Description |
|
||||
|--------|-------------|
|
||||
| **TNP Mitigation** | Topological naming more stable |
|
||||
| **UpToShape** | New Pad/Pocket type extending to arbitrary shapes |
|
||||
| **Draft Angle** | Taper angles on Pad/Pocket |
|
||||
| **Improved Hole** | More thread types, better UI |
|
||||
| **Assembly Integration** | Native assembly workbench |
|
||||
| **Arch → BIM** | Workbench rename |
|
||||
| **Path → CAM** | Workbench rename |
|
||||
|
||||
---
|
||||
|
||||
## Python Module Access
|
||||
|
||||
```python
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
import Sketcher
|
||||
import PartDesign
|
||||
import PartDesignGui
|
||||
|
||||
# Access feature classes
|
||||
print(dir(PartDesign))
|
||||
# ['Additive', 'AdditiveBox', 'AdditiveCone', 'AdditiveCylinder', ...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document version: FreeCAD 1.0.2 / January 2026*
|
||||
*Reference: FreeCAD Wiki, GitHub FreeCAD-documentation, FreeCAD Forum*
|
||||
12
ztools/Init.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# ztools Addon Initialization
|
||||
# This file runs at FreeCAD startup (before GUI)
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
# The Catppuccin Mocha theme is now provided as a Preference Pack.
|
||||
# It will be automatically available in:
|
||||
# Edit > Preferences > General > Preference packs > CatppuccinMocha
|
||||
#
|
||||
# No manual installation is required - FreeCAD's addon system handles it.
|
||||
|
||||
App.Console.PrintLog("ztools addon loaded\n")
|
||||
233
ztools/InitGui.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# ztools Workbench for FreeCAD 1.0+
|
||||
# Extended PartDesign replacement with velocity-focused tools
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
|
||||
|
||||
class ZToolsWorkbench(Gui.Workbench):
|
||||
"""Extended PartDesign workbench with velocity-focused tools."""
|
||||
|
||||
MenuText = "ztools"
|
||||
ToolTip = "Extended PartDesign replacement for faster CAD workflows"
|
||||
|
||||
# Catppuccin Mocha themed icon
|
||||
Icon = """
|
||||
/* XPM */
|
||||
static char * ztools_xpm[] = {
|
||||
"16 16 5 1",
|
||||
" c None",
|
||||
". c #313244",
|
||||
"+ c #cba6f7",
|
||||
"@ c #94e2d5",
|
||||
"# c #45475a",
|
||||
" ",
|
||||
" ############ ",
|
||||
" #..........# ",
|
||||
" #.++++++++.# ",
|
||||
" #.+......+.# ",
|
||||
" #.....+++..# ",
|
||||
" #....++....# ",
|
||||
" #...++.....# ",
|
||||
" #..++......# ",
|
||||
" #.++.......# ",
|
||||
" #.++++++++@# ",
|
||||
" #..........# ",
|
||||
" ############ ",
|
||||
" ",
|
||||
" ",
|
||||
" "};
|
||||
"""
|
||||
|
||||
def Initialize(self):
|
||||
"""Called on workbench first activation."""
|
||||
# Load PartDesign and Sketcher workbenches to register their commands
|
||||
# We need to actually activate them briefly to ensure commands are registered
|
||||
try:
|
||||
# Get list of available workbenches
|
||||
wb_list = Gui.listWorkbenches()
|
||||
|
||||
# Initialize PartDesign workbench if available
|
||||
if "PartDesignWorkbench" in wb_list:
|
||||
pd_wb = Gui.getWorkbench("PartDesignWorkbench")
|
||||
# Call Initialize if not already done
|
||||
if hasattr(pd_wb, "Initialize"):
|
||||
pd_wb.Initialize()
|
||||
|
||||
# Initialize Sketcher workbench if available
|
||||
if "SketcherWorkbench" in wb_list:
|
||||
sketcher_wb = Gui.getWorkbench("SketcherWorkbench")
|
||||
if hasattr(sketcher_wb, "Initialize"):
|
||||
sketcher_wb.Initialize()
|
||||
|
||||
except Exception as e:
|
||||
App.Console.PrintWarning(f"Could not initialize PartDesign/Sketcher: {e}\n")
|
||||
|
||||
from ztools.commands import datum_commands, pattern_commands, pocket_commands
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Structure Tools
|
||||
# =====================================================================
|
||||
self.structure_tools = [
|
||||
"PartDesign_Body",
|
||||
"PartDesign_NewSketch",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Reference Geometry (Datums)
|
||||
# =====================================================================
|
||||
self.partdesign_datum_tools = [
|
||||
"PartDesign_Plane",
|
||||
"PartDesign_Line",
|
||||
"PartDesign_Point",
|
||||
"PartDesign_CoordinateSystem",
|
||||
"PartDesign_ShapeBinder",
|
||||
"PartDesign_SubShapeBinder",
|
||||
"PartDesign_Clone",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Additive Features
|
||||
# =====================================================================
|
||||
self.additive_tools = [
|
||||
"PartDesign_Pad",
|
||||
"PartDesign_Revolution",
|
||||
"PartDesign_AdditiveLoft",
|
||||
"PartDesign_AdditivePipe",
|
||||
"PartDesign_AdditiveHelix",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Additive Primitives (compound command with dropdown)
|
||||
# =====================================================================
|
||||
self.additive_primitives = [
|
||||
"PartDesign_CompPrimitiveAdditive",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Subtractive Features
|
||||
# =====================================================================
|
||||
self.subtractive_tools = [
|
||||
"PartDesign_Pocket",
|
||||
"PartDesign_Hole",
|
||||
"PartDesign_Groove",
|
||||
"PartDesign_SubtractiveLoft",
|
||||
"PartDesign_SubtractivePipe",
|
||||
"PartDesign_SubtractiveHelix",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Subtractive Primitives (compound command with dropdown)
|
||||
# =====================================================================
|
||||
self.subtractive_primitives = [
|
||||
"PartDesign_CompPrimitiveSubtractive",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Transformation Features (Patterns)
|
||||
# =====================================================================
|
||||
self.transformation_tools = [
|
||||
"PartDesign_Mirrored",
|
||||
"PartDesign_LinearPattern",
|
||||
"PartDesign_PolarPattern",
|
||||
"PartDesign_MultiTransform",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Dress-Up Features
|
||||
# =====================================================================
|
||||
self.dressup_tools = [
|
||||
"PartDesign_Fillet",
|
||||
"PartDesign_Chamfer",
|
||||
"PartDesign_Draft",
|
||||
"PartDesign_Thickness",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# PartDesign Boolean Operations
|
||||
# =====================================================================
|
||||
self.boolean_tools = [
|
||||
"PartDesign_Boolean",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Sketcher Tools (commonly used with PartDesign)
|
||||
# =====================================================================
|
||||
self.sketcher_tools = [
|
||||
"Sketcher_NewSketch",
|
||||
"Sketcher_EditSketch",
|
||||
"Sketcher_MapSketch",
|
||||
"Sketcher_ValidateSketch",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# ZTools Custom Tools
|
||||
# =====================================================================
|
||||
self.ztools_datum_tools = [
|
||||
"ZTools_DatumCreator",
|
||||
"ZTools_DatumManager",
|
||||
]
|
||||
|
||||
self.ztools_pattern_tools = [
|
||||
"ZTools_RotatedLinearPattern",
|
||||
]
|
||||
|
||||
self.ztools_pocket_tools = [
|
||||
"ZTools_EnhancedPocket",
|
||||
]
|
||||
|
||||
# =====================================================================
|
||||
# Append Toolbars
|
||||
# =====================================================================
|
||||
self.appendToolbar("Structure", self.structure_tools)
|
||||
self.appendToolbar("Sketcher", self.sketcher_tools)
|
||||
self.appendToolbar("Datums", self.partdesign_datum_tools)
|
||||
self.appendToolbar("Additive", self.additive_tools + self.additive_primitives)
|
||||
self.appendToolbar(
|
||||
"Subtractive", self.subtractive_tools + self.subtractive_primitives
|
||||
)
|
||||
self.appendToolbar("Transformations", self.transformation_tools)
|
||||
self.appendToolbar("Dress-Up", self.dressup_tools)
|
||||
self.appendToolbar("Boolean", self.boolean_tools)
|
||||
self.appendToolbar("ztools Datums", self.ztools_datum_tools)
|
||||
self.appendToolbar("ztools Patterns", self.ztools_pattern_tools)
|
||||
self.appendToolbar("ztools Features", self.ztools_pocket_tools)
|
||||
|
||||
# =====================================================================
|
||||
# Append Menus
|
||||
# =====================================================================
|
||||
self.appendMenu("Structure", self.structure_tools)
|
||||
self.appendMenu("Sketch", self.sketcher_tools)
|
||||
self.appendMenu(["PartDesign", "Datums"], self.partdesign_datum_tools)
|
||||
self.appendMenu(
|
||||
["PartDesign", "Additive"], self.additive_tools + self.additive_primitives
|
||||
)
|
||||
self.appendMenu(
|
||||
["PartDesign", "Subtractive"],
|
||||
self.subtractive_tools + self.subtractive_primitives,
|
||||
)
|
||||
self.appendMenu(["PartDesign", "Transformations"], self.transformation_tools)
|
||||
self.appendMenu(["PartDesign", "Dress-Up"], self.dressup_tools)
|
||||
self.appendMenu(["PartDesign", "Boolean"], self.boolean_tools)
|
||||
self.appendMenu(
|
||||
"ztools",
|
||||
self.ztools_datum_tools
|
||||
+ self.ztools_pattern_tools
|
||||
+ self.ztools_pocket_tools,
|
||||
)
|
||||
|
||||
App.Console.PrintMessage("ztools workbench initialized\n")
|
||||
|
||||
def Activated(self):
|
||||
"""Called when workbench is activated."""
|
||||
App.Console.PrintMessage("ztools workbench activated\n")
|
||||
|
||||
def Deactivated(self):
|
||||
"""Called when workbench is deactivated."""
|
||||
pass
|
||||
|
||||
def GetClassName(self):
|
||||
return "Gui::PythonWorkbench"
|
||||
|
||||
|
||||
Gui.addWorkbench(ZToolsWorkbench())
|
||||
123
ztools/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ztools - Extended PartDesign for FreeCAD
|
||||
|
||||
Velocity-focused CAD tools extending FreeCAD 1.0+ PartDesign workbench.
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Copy the `ztools` folder to your FreeCAD Mod directory:
|
||||
- **Linux**: `~/.local/share/FreeCAD/Mod/ztools/`
|
||||
- **Windows**: `%APPDATA%\FreeCAD\Mod\ztools\`
|
||||
- **macOS**: `~/Library/Application Support/FreeCAD/Mod/ztools/`
|
||||
|
||||
2. Restart FreeCAD
|
||||
|
||||
3. Select **ztools** from the workbench dropdown
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
ztools/
|
||||
├── InitGui.py # Workbench registration
|
||||
├── ztools/
|
||||
│ ├── __init__.py
|
||||
│ ├── datums/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── core.py # Datum creation functions
|
||||
│ └── commands/
|
||||
│ ├── __init__.py
|
||||
│ └── datum_commands.py # GUI commands
|
||||
├── setup.cfg
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Module 1: Datum Tools
|
||||
|
||||
### Datum Creator (GUI)
|
||||
|
||||
Unified task panel for creating:
|
||||
|
||||
**Planes**
|
||||
- Offset from Face
|
||||
- Midplane (2 parallel faces)
|
||||
- 3 Points
|
||||
- Normal to Edge (at parameter)
|
||||
- Angled from Face (about edge)
|
||||
- Tangent to Cylinder
|
||||
|
||||
**Axes**
|
||||
- 2 Points
|
||||
- From Edge
|
||||
- Cylinder Center
|
||||
- Plane Intersection
|
||||
|
||||
**Points**
|
||||
- At Vertex
|
||||
- XYZ Coordinates
|
||||
- On Edge (at parameter)
|
||||
- Face Center
|
||||
- Circle Center
|
||||
|
||||
### Options
|
||||
|
||||
- **Link to Spreadsheet**: Creates aliases in Spreadsheet for parametric control
|
||||
- **Add to Active Body**: Creates PartDesign datums vs Part geometry
|
||||
- **Custom Name**: Override auto-naming (e.g., `ZPlane_Offset_001`)
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from ztools.datums import (
|
||||
plane_offset_from_face,
|
||||
plane_midplane,
|
||||
axis_cylinder_center,
|
||||
point_at_coordinates,
|
||||
)
|
||||
|
||||
doc = App.ActiveDocument
|
||||
body = doc.getObject('Body')
|
||||
|
||||
# Offset plane from selected face
|
||||
face = body.Shape.Faces[0]
|
||||
plane = plane_offset_from_face(face, 15.0, body=body, link_spreadsheet=True)
|
||||
|
||||
# Midplane between two faces
|
||||
mid = plane_midplane(face1, face2, name="MidPlane_Custom")
|
||||
|
||||
# Axis at cylinder center
|
||||
cyl_face = body.Shape.Faces[2]
|
||||
axis = axis_cylinder_center(cyl_face, body=body)
|
||||
|
||||
# Point at coordinates with spreadsheet link
|
||||
pt = point_at_coordinates(50, 25, 0, link_spreadsheet=True)
|
||||
```
|
||||
|
||||
### Metadata
|
||||
|
||||
All ztools datums store creation metadata in custom properties:
|
||||
|
||||
- `ZTools_Type`: Creation method (e.g., "offset_from_face")
|
||||
- `ZTools_Params`: JSON-encoded parameters
|
||||
|
||||
Access via:
|
||||
```python
|
||||
plane = doc.getObject('ZPlane_Offset_001')
|
||||
print(plane.ZTools_Type) # "offset_from_face"
|
||||
print(plane.ZTools_Params) # {"distance": 15.0, ...}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] **Module 2**: Enhanced Pad/Pocket (multi-body, draft angles, lip/groove)
|
||||
- [ ] **Module 3**: Body operations (split, combine, shell improvements)
|
||||
- [ ] **Module 4**: Pattern tools (curve-driven, fill patterns)
|
||||
- [ ] **Datum Manager**: Panel to list/toggle/rename all datums
|
||||
|
||||
## License
|
||||
|
||||
LGPL-2.1 (same as FreeCAD)
|
||||
|
||||
## Contributing
|
||||
|
||||
Kindred Systems LLC - Kansas City
|
||||
BIN
ztools/__pycache__/Init.cpython-313.pyc
Normal file
BIN
ztools/__pycache__/InitGui.cpython-313.pyc
Normal file
11
ztools/setup.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
[metadata]
|
||||
name = ztools
|
||||
version = 0.1.0
|
||||
description = Extended PartDesign workbench for FreeCAD with velocity-focused tools
|
||||
author = Kindred Systems LLC
|
||||
license = LGPL-2.1
|
||||
url = https://github.com/kindredsystems/ztools
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
python_requires = >=3.8
|
||||
2
ztools/ztools/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# ztools - Extended PartDesign for FreeCAD
|
||||
__version__ = "0.1.0"
|
||||
BIN
ztools/ztools/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ztools/ztools/__pycache__/__init__.cpython-313.pyc
Normal file
4
ztools/ztools/commands/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# ztools/commands - GUI commands
|
||||
from . import datum_commands, pattern_commands, pocket_commands
|
||||
|
||||
__all__ = ["datum_commands", "pattern_commands", "pocket_commands"]
|
||||
BIN
ztools/ztools/commands/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ztools/ztools/commands/__pycache__/__init__.cpython-313.pyc
Normal file
650
ztools/ztools/commands/datum_commands.py
Normal file
@@ -0,0 +1,650 @@
|
||||
# ztools/commands/datum_commands.py
|
||||
# GUI commands and task panel for datum creation
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
class DatumCreatorTaskPanel:
|
||||
"""Unified task panel for creating datum planes, axes, and points."""
|
||||
|
||||
PLANE_MODES = [
|
||||
("Offset from Face", "offset_face"),
|
||||
("Midplane (2 Faces)", "midplane"),
|
||||
("3 Points", "3_points"),
|
||||
("Normal to Edge", "normal_edge"),
|
||||
("Angled from Face", "angled"),
|
||||
("Tangent to Cylinder", "tangent_cyl"),
|
||||
]
|
||||
|
||||
AXIS_MODES = [
|
||||
("2 Points", "axis_2pt"),
|
||||
("From Edge", "axis_edge"),
|
||||
("Cylinder Center", "axis_cyl"),
|
||||
("Plane Intersection", "axis_intersect"),
|
||||
]
|
||||
|
||||
POINT_MODES = [
|
||||
("At Vertex", "point_vertex"),
|
||||
("XYZ Coordinates", "point_xyz"),
|
||||
("On Edge", "point_edge"),
|
||||
("Face Center", "point_face"),
|
||||
("Circle Center", "point_circle"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle("ztools Datum Creator")
|
||||
self.setup_ui()
|
||||
self.selection_callback = None
|
||||
self.selected_items = []
|
||||
self.setup_selection_observer()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Datum type tabs
|
||||
self.tabs = QtGui.QTabWidget()
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
# Plane tab
|
||||
plane_widget = QtGui.QWidget()
|
||||
plane_layout = QtGui.QVBoxLayout(plane_widget)
|
||||
|
||||
self.plane_mode = QtGui.QComboBox()
|
||||
for label, _ in self.PLANE_MODES:
|
||||
self.plane_mode.addItem(label)
|
||||
self.plane_mode.currentIndexChanged.connect(self.on_plane_mode_changed)
|
||||
plane_layout.addWidget(QtGui.QLabel("Mode:"))
|
||||
plane_layout.addWidget(self.plane_mode)
|
||||
|
||||
# Plane parameters group
|
||||
self.plane_params = QtGui.QGroupBox("Parameters")
|
||||
self.plane_params_layout = QtGui.QFormLayout(self.plane_params)
|
||||
|
||||
self.plane_offset_spin = QtGui.QDoubleSpinBox()
|
||||
self.plane_offset_spin.setRange(-10000, 10000)
|
||||
self.plane_offset_spin.setValue(10)
|
||||
self.plane_offset_spin.setSuffix(" mm")
|
||||
|
||||
self.plane_angle_spin = QtGui.QDoubleSpinBox()
|
||||
self.plane_angle_spin.setRange(-360, 360)
|
||||
self.plane_angle_spin.setValue(45)
|
||||
self.plane_angle_spin.setSuffix(" °")
|
||||
|
||||
self.plane_param_spin = QtGui.QDoubleSpinBox()
|
||||
self.plane_param_spin.setRange(0, 1)
|
||||
self.plane_param_spin.setValue(0.5)
|
||||
self.plane_param_spin.setSingleStep(0.1)
|
||||
|
||||
plane_layout.addWidget(self.plane_params)
|
||||
|
||||
# Plane selection display
|
||||
self.plane_selection_label = QtGui.QLabel("Selection: None")
|
||||
self.plane_selection_label.setWordWrap(True)
|
||||
plane_layout.addWidget(self.plane_selection_label)
|
||||
|
||||
self.tabs.addTab(plane_widget, "Planes")
|
||||
|
||||
# Axis tab
|
||||
axis_widget = QtGui.QWidget()
|
||||
axis_layout = QtGui.QVBoxLayout(axis_widget)
|
||||
|
||||
self.axis_mode = QtGui.QComboBox()
|
||||
for label, _ in self.AXIS_MODES:
|
||||
self.axis_mode.addItem(label)
|
||||
self.axis_mode.currentIndexChanged.connect(self.on_axis_mode_changed)
|
||||
axis_layout.addWidget(QtGui.QLabel("Mode:"))
|
||||
axis_layout.addWidget(self.axis_mode)
|
||||
|
||||
self.axis_selection_label = QtGui.QLabel("Selection: None")
|
||||
self.axis_selection_label.setWordWrap(True)
|
||||
axis_layout.addWidget(self.axis_selection_label)
|
||||
|
||||
axis_layout.addStretch()
|
||||
self.tabs.addTab(axis_widget, "Axes")
|
||||
|
||||
# Point tab
|
||||
point_widget = QtGui.QWidget()
|
||||
point_layout = QtGui.QVBoxLayout(point_widget)
|
||||
|
||||
self.point_mode = QtGui.QComboBox()
|
||||
for label, _ in self.POINT_MODES:
|
||||
self.point_mode.addItem(label)
|
||||
self.point_mode.currentIndexChanged.connect(self.on_point_mode_changed)
|
||||
point_layout.addWidget(QtGui.QLabel("Mode:"))
|
||||
point_layout.addWidget(self.point_mode)
|
||||
|
||||
# Point XYZ inputs
|
||||
self.point_xyz_group = QtGui.QGroupBox("Coordinates")
|
||||
xyz_layout = QtGui.QFormLayout(self.point_xyz_group)
|
||||
|
||||
self.point_x_spin = QtGui.QDoubleSpinBox()
|
||||
self.point_x_spin.setRange(-10000, 10000)
|
||||
self.point_x_spin.setSuffix(" mm")
|
||||
|
||||
self.point_y_spin = QtGui.QDoubleSpinBox()
|
||||
self.point_y_spin.setRange(-10000, 10000)
|
||||
self.point_y_spin.setSuffix(" mm")
|
||||
|
||||
self.point_z_spin = QtGui.QDoubleSpinBox()
|
||||
self.point_z_spin.setRange(-10000, 10000)
|
||||
self.point_z_spin.setSuffix(" mm")
|
||||
|
||||
xyz_layout.addRow("X:", self.point_x_spin)
|
||||
xyz_layout.addRow("Y:", self.point_y_spin)
|
||||
xyz_layout.addRow("Z:", self.point_z_spin)
|
||||
|
||||
self.point_xyz_group.setVisible(False)
|
||||
point_layout.addWidget(self.point_xyz_group)
|
||||
|
||||
# Point parameter (for edge)
|
||||
self.point_param_spin = QtGui.QDoubleSpinBox()
|
||||
self.point_param_spin.setRange(0, 1)
|
||||
self.point_param_spin.setValue(0.5)
|
||||
self.point_param_spin.setSingleStep(0.1)
|
||||
|
||||
self.point_selection_label = QtGui.QLabel("Selection: None")
|
||||
self.point_selection_label.setWordWrap(True)
|
||||
point_layout.addWidget(self.point_selection_label)
|
||||
|
||||
point_layout.addStretch()
|
||||
self.tabs.addTab(point_widget, "Points")
|
||||
|
||||
# Common options
|
||||
options_group = QtGui.QGroupBox("Options")
|
||||
options_layout = QtGui.QVBoxLayout(options_group)
|
||||
|
||||
self.link_spreadsheet_cb = QtGui.QCheckBox("Link to Spreadsheet")
|
||||
options_layout.addWidget(self.link_spreadsheet_cb)
|
||||
|
||||
self.use_body_cb = QtGui.QCheckBox("Add to Active Body")
|
||||
self.use_body_cb.setChecked(True)
|
||||
options_layout.addWidget(self.use_body_cb)
|
||||
|
||||
# Custom name
|
||||
name_layout = QtGui.QHBoxLayout()
|
||||
self.custom_name_cb = QtGui.QCheckBox("Custom Name:")
|
||||
self.custom_name_edit = QtGui.QLineEdit()
|
||||
self.custom_name_edit.setEnabled(False)
|
||||
self.custom_name_cb.toggled.connect(self.custom_name_edit.setEnabled)
|
||||
name_layout.addWidget(self.custom_name_cb)
|
||||
name_layout.addWidget(self.custom_name_edit)
|
||||
options_layout.addLayout(name_layout)
|
||||
|
||||
layout.addWidget(options_group)
|
||||
|
||||
# Create button
|
||||
self.create_btn = QtGui.QPushButton("Create Datum")
|
||||
self.create_btn.clicked.connect(self.on_create)
|
||||
layout.addWidget(self.create_btn)
|
||||
|
||||
# Initialize UI state
|
||||
self.on_plane_mode_changed(0)
|
||||
self.tabs.currentChanged.connect(self.on_tab_changed)
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Setup selection observer to track user selections."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def clearSelection(self, doc):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_selection_changed(self):
|
||||
"""Update UI when selection changes."""
|
||||
sel = Gui.Selection.getSelectionEx()
|
||||
self.selected_items = sel
|
||||
|
||||
# Build selection description
|
||||
desc = []
|
||||
for s in sel:
|
||||
if s.SubElementNames:
|
||||
for sub in s.SubElementNames:
|
||||
desc.append(f"{s.ObjectName}.{sub}")
|
||||
else:
|
||||
desc.append(s.ObjectName)
|
||||
|
||||
text = ", ".join(desc) if desc else "None"
|
||||
|
||||
# Update appropriate label
|
||||
tab = self.tabs.currentIndex()
|
||||
if tab == 0:
|
||||
self.plane_selection_label.setText(f"Selection: {text}")
|
||||
elif tab == 1:
|
||||
self.axis_selection_label.setText(f"Selection: {text}")
|
||||
elif tab == 2:
|
||||
self.point_selection_label.setText(f"Selection: {text}")
|
||||
|
||||
def on_tab_changed(self, index):
|
||||
self.on_selection_changed()
|
||||
|
||||
def on_plane_mode_changed(self, index):
|
||||
"""Update plane parameter UI based on mode."""
|
||||
# Clear existing params
|
||||
while self.plane_params_layout.rowCount() > 0:
|
||||
self.plane_params_layout.removeRow(0)
|
||||
|
||||
mode = self.PLANE_MODES[index][1]
|
||||
|
||||
if mode == "offset_face":
|
||||
self.plane_params_layout.addRow("Offset:", self.plane_offset_spin)
|
||||
elif mode == "angled":
|
||||
self.plane_params_layout.addRow("Angle:", self.plane_angle_spin)
|
||||
elif mode == "normal_edge":
|
||||
self.plane_params_layout.addRow("Position (0-1):", self.plane_param_spin)
|
||||
elif mode == "tangent_cyl":
|
||||
self.plane_params_layout.addRow("Angle:", self.plane_angle_spin)
|
||||
|
||||
def on_axis_mode_changed(self, index):
|
||||
pass # Axes don't have extra parameters currently
|
||||
|
||||
def on_point_mode_changed(self, index):
|
||||
mode = self.POINT_MODES[index][1]
|
||||
self.point_xyz_group.setVisible(mode == "point_xyz")
|
||||
|
||||
def get_body(self):
|
||||
"""Get active body if checkbox is checked."""
|
||||
if not self.use_body_cb.isChecked():
|
||||
return None
|
||||
|
||||
# Try to get active body
|
||||
if hasattr(Gui, "ActiveDocument") and Gui.ActiveDocument:
|
||||
active_view = Gui.ActiveDocument.ActiveView
|
||||
if hasattr(active_view, "getActiveObject"):
|
||||
body = active_view.getActiveObject("pdbody")
|
||||
if body:
|
||||
return body
|
||||
|
||||
# Fallback: find a body in document
|
||||
doc = App.ActiveDocument
|
||||
for obj in doc.Objects:
|
||||
if obj.TypeId == "PartDesign::Body":
|
||||
return obj
|
||||
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
"""Get custom name or None for auto-naming."""
|
||||
if self.custom_name_cb.isChecked() and self.custom_name_edit.text():
|
||||
return self.custom_name_edit.text()
|
||||
return None
|
||||
|
||||
def get_selected_geometry(self, geo_type):
|
||||
"""Extract geometry of specified type from selection.
|
||||
|
||||
Returns:
|
||||
List of tuples: (shape, source_object, subname)
|
||||
"""
|
||||
results = []
|
||||
for sel in self.selected_items:
|
||||
obj = sel.Object
|
||||
if not hasattr(obj, "Shape"):
|
||||
continue
|
||||
|
||||
if sel.SubElementNames:
|
||||
for sub in sel.SubElementNames:
|
||||
# Only process valid sub-element names (Face#, Edge#, Vertex#)
|
||||
# Skip invalid names like "Plane" from datum objects
|
||||
if not (
|
||||
sub.startswith("Face")
|
||||
or sub.startswith("Edge")
|
||||
or sub.startswith("Vertex")
|
||||
):
|
||||
# Try to use the whole object's shape instead
|
||||
shape = obj.Shape
|
||||
if geo_type == "face" and shape.Faces:
|
||||
# Use the first face of the object (e.g., datum plane)
|
||||
results.append((shape.Faces[0], obj, "Face1"))
|
||||
elif geo_type == "edge" and shape.Edges:
|
||||
results.append((shape.Edges[0], obj, "Edge1"))
|
||||
elif geo_type == "vertex" and shape.Vertexes:
|
||||
results.append((shape.Vertexes[0], obj, "Vertex1"))
|
||||
continue
|
||||
|
||||
try:
|
||||
shape = obj.Shape.getElement(sub)
|
||||
if geo_type == "face" and isinstance(shape, Part.Face):
|
||||
results.append((shape, obj, sub))
|
||||
elif geo_type == "edge" and isinstance(shape, Part.Edge):
|
||||
results.append((shape, obj, sub))
|
||||
elif geo_type == "vertex" and isinstance(shape, Part.Vertex):
|
||||
results.append((shape, obj, sub))
|
||||
except Exception:
|
||||
# If getElement fails, try to use the whole shape
|
||||
shape = obj.Shape
|
||||
if geo_type == "face" and shape.Faces:
|
||||
results.append((shape.Faces[0], obj, "Face1"))
|
||||
elif geo_type == "edge" and shape.Edges:
|
||||
results.append((shape.Edges[0], obj, "Edge1"))
|
||||
elif geo_type == "vertex" and shape.Vertexes:
|
||||
results.append((shape.Vertexes[0], obj, "Vertex1"))
|
||||
else:
|
||||
# No sub-element selected, use the whole object's shape
|
||||
shape = obj.Shape
|
||||
if geo_type == "face" and shape.Faces:
|
||||
results.append((shape.Faces[0], obj, "Face1"))
|
||||
elif geo_type == "edge" and shape.Edges:
|
||||
results.append((shape.Edges[0], obj, "Edge1"))
|
||||
elif geo_type == "vertex" and shape.Vertexes:
|
||||
results.append((shape.Vertexes[0], obj, "Vertex1"))
|
||||
return results
|
||||
|
||||
def on_create(self):
|
||||
"""Create the datum based on current settings."""
|
||||
from ztools.datums import core
|
||||
|
||||
tab = self.tabs.currentIndex()
|
||||
body = self.get_body()
|
||||
name = self.get_name()
|
||||
link_ss = self.link_spreadsheet_cb.isChecked()
|
||||
|
||||
try:
|
||||
if tab == 0: # Planes
|
||||
self.create_plane(core, body, name, link_ss)
|
||||
elif tab == 1: # Axes
|
||||
self.create_axis(core, body, name)
|
||||
elif tab == 2: # Points
|
||||
self.create_point(core, body, name, link_ss)
|
||||
|
||||
App.Console.PrintMessage("Datum created successfully\n")
|
||||
|
||||
except Exception as e:
|
||||
App.Console.PrintError(f"Failed to create datum: {e}\n")
|
||||
QtGui.QMessageBox.warning(self.form, "Error", str(e))
|
||||
|
||||
def create_plane(self, core, body, name, link_ss):
|
||||
mode = self.PLANE_MODES[self.plane_mode.currentIndex()][1]
|
||||
|
||||
if mode == "offset_face":
|
||||
faces = self.get_selected_geometry("face")
|
||||
if not faces:
|
||||
raise ValueError("Select a face")
|
||||
face, src_obj, src_sub = faces[0]
|
||||
core.plane_offset_from_face(
|
||||
face,
|
||||
self.plane_offset_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "midplane":
|
||||
faces = self.get_selected_geometry("face")
|
||||
if len(faces) < 2:
|
||||
raise ValueError("Select 2 faces")
|
||||
face1, src_obj1, src_sub1 = faces[0]
|
||||
face2, src_obj2, src_sub2 = faces[1]
|
||||
core.plane_midplane(
|
||||
face1,
|
||||
face2,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object1=src_obj1,
|
||||
source_subname1=src_sub1,
|
||||
source_object2=src_obj2,
|
||||
source_subname2=src_sub2,
|
||||
)
|
||||
|
||||
elif mode == "3_points":
|
||||
verts = self.get_selected_geometry("vertex")
|
||||
if len(verts) < 3:
|
||||
raise ValueError("Select 3 vertices")
|
||||
v1, src_obj1, src_sub1 = verts[0]
|
||||
v2, src_obj2, src_sub2 = verts[1]
|
||||
v3, src_obj3, src_sub3 = verts[2]
|
||||
core.plane_from_3_points(
|
||||
v1.Point,
|
||||
v2.Point,
|
||||
v3.Point,
|
||||
name=name,
|
||||
body=body,
|
||||
source_refs=[
|
||||
(src_obj1, src_sub1),
|
||||
(src_obj2, src_sub2),
|
||||
(src_obj3, src_sub3),
|
||||
],
|
||||
)
|
||||
|
||||
elif mode == "normal_edge":
|
||||
edges = self.get_selected_geometry("edge")
|
||||
if not edges:
|
||||
raise ValueError("Select an edge")
|
||||
edge, src_obj, src_sub = edges[0]
|
||||
core.plane_normal_to_edge(
|
||||
edge,
|
||||
parameter=self.plane_param_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "angled":
|
||||
faces = self.get_selected_geometry("face")
|
||||
edges = self.get_selected_geometry("edge")
|
||||
if not faces or not edges:
|
||||
raise ValueError("Select a face and an edge")
|
||||
face, face_obj, face_sub = faces[0]
|
||||
edge, edge_obj, edge_sub = edges[0]
|
||||
core.plane_angled(
|
||||
face,
|
||||
edge,
|
||||
self.plane_angle_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_face_obj=face_obj,
|
||||
source_face_sub=face_sub,
|
||||
source_edge_obj=edge_obj,
|
||||
source_edge_sub=edge_sub,
|
||||
)
|
||||
|
||||
elif mode == "tangent_cyl":
|
||||
faces = self.get_selected_geometry("face")
|
||||
if not faces:
|
||||
raise ValueError("Select a cylindrical face")
|
||||
face, src_obj, src_sub = faces[0]
|
||||
core.plane_tangent_to_cylinder(
|
||||
face,
|
||||
angle=self.plane_angle_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
def create_axis(self, core, body, name):
|
||||
mode = self.AXIS_MODES[self.axis_mode.currentIndex()][1]
|
||||
|
||||
if mode == "axis_2pt":
|
||||
verts = self.get_selected_geometry("vertex")
|
||||
if len(verts) < 2:
|
||||
raise ValueError("Select 2 vertices")
|
||||
v1, obj1, sub1 = verts[0]
|
||||
v2, obj2, sub2 = verts[1]
|
||||
core.axis_from_2_points(
|
||||
v1.Point,
|
||||
v2.Point,
|
||||
name=name,
|
||||
body=body,
|
||||
source_refs=[(obj1, sub1), (obj2, sub2)],
|
||||
)
|
||||
|
||||
elif mode == "axis_edge":
|
||||
edges = self.get_selected_geometry("edge")
|
||||
if not edges:
|
||||
raise ValueError("Select a linear edge")
|
||||
edge, src_obj, src_sub = edges[0]
|
||||
core.axis_from_edge(
|
||||
edge,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "axis_cyl":
|
||||
faces = self.get_selected_geometry("face")
|
||||
if not faces:
|
||||
raise ValueError("Select a cylindrical face")
|
||||
face, src_obj, src_sub = faces[0]
|
||||
core.axis_cylinder_center(
|
||||
face,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "axis_intersect":
|
||||
# Need 2 plane objects selected
|
||||
if len(self.selected_items) < 2:
|
||||
raise ValueError("Select 2 datum planes")
|
||||
core.axis_intersection_planes(
|
||||
self.selected_items[0].Object,
|
||||
self.selected_items[1].Object,
|
||||
name=name,
|
||||
body=body,
|
||||
)
|
||||
|
||||
def create_point(self, core, body, name, link_ss):
|
||||
mode = self.POINT_MODES[self.point_mode.currentIndex()][1]
|
||||
|
||||
if mode == "point_vertex":
|
||||
verts = self.get_selected_geometry("vertex")
|
||||
if not verts:
|
||||
raise ValueError("Select a vertex")
|
||||
vert, src_obj, src_sub = verts[0]
|
||||
core.point_at_vertex(
|
||||
vert,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "point_xyz":
|
||||
core.point_at_coordinates(
|
||||
self.point_x_spin.value(),
|
||||
self.point_y_spin.value(),
|
||||
self.point_z_spin.value(),
|
||||
name=name,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
)
|
||||
|
||||
elif mode == "point_edge":
|
||||
edges = self.get_selected_geometry("edge")
|
||||
if not edges:
|
||||
raise ValueError("Select an edge")
|
||||
edge, src_obj, src_sub = edges[0]
|
||||
core.point_on_edge(
|
||||
edge,
|
||||
parameter=self.point_param_spin.value(),
|
||||
name=name,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
body=body,
|
||||
link_spreadsheet=link_ss,
|
||||
)
|
||||
|
||||
elif mode == "point_face":
|
||||
faces = self.get_selected_geometry("face")
|
||||
if not faces:
|
||||
raise ValueError("Select a face")
|
||||
face, src_obj, src_sub = faces[0]
|
||||
core.point_center_of_face(
|
||||
face,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
elif mode == "point_circle":
|
||||
edges = self.get_selected_geometry("edge")
|
||||
if not edges:
|
||||
raise ValueError("Select a circular edge")
|
||||
edge, src_obj, src_sub = edges[0]
|
||||
core.point_center_of_circle(
|
||||
edge,
|
||||
name=name,
|
||||
body=body,
|
||||
source_object=src_obj,
|
||||
source_subname=src_sub,
|
||||
)
|
||||
|
||||
def accept(self):
|
||||
"""Called when OK is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Called when Cancel is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class ZTools_DatumCreator:
|
||||
"""Command to open datum creator task panel."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("datum_creator"),
|
||||
"MenuText": "Datum Creator",
|
||||
"ToolTip": "Create datum planes, axes, and points with advanced options",
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
panel = DatumCreatorTaskPanel()
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
class ZTools_DatumManager:
|
||||
"""Command to open datum manager panel."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("datum_manager"),
|
||||
"MenuText": "Datum Manager",
|
||||
"ToolTip": "List, toggle visibility, and rename datums in document",
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
# TODO: Implement datum manager panel
|
||||
App.Console.PrintMessage("Datum Manager - Coming soon\n")
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
# Register commands
|
||||
Gui.addCommand("ZTools_DatumCreator", ZTools_DatumCreator())
|
||||
Gui.addCommand("ZTools_DatumManager", ZTools_DatumManager())
|
||||
206
ztools/ztools/commands/pattern_commands.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# ztools/commands/pattern_commands.py
|
||||
# Rotated Linear Pattern command
|
||||
# Creates a linear pattern with incremental rotation for each instance
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
|
||||
class RotatedLinearPatternFeature:
|
||||
"""Feature object for rotated linear pattern."""
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.Proxy = self
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyLink", "Source", "Base", "Source object to pattern"
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"Direction",
|
||||
"Pattern",
|
||||
"Direction of the linear pattern",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance", "Length", "Pattern", "Total length of the pattern"
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyInteger",
|
||||
"Occurrences",
|
||||
"Pattern",
|
||||
"Number of occurrences (including original)",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"RotationAxis",
|
||||
"Rotation",
|
||||
"Axis of rotation for each instance",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyAngle",
|
||||
"RotationAngle",
|
||||
"Rotation",
|
||||
"Rotation angle increment per instance",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyVector",
|
||||
"RotationCenter",
|
||||
"Rotation",
|
||||
"Center point for rotation (relative to each instance)",
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"CumulativeRotation",
|
||||
"Rotation",
|
||||
"If true, rotation accumulates with each instance",
|
||||
)
|
||||
|
||||
# Set defaults
|
||||
obj.Direction = App.Vector(1, 0, 0)
|
||||
obj.Length = 100.0
|
||||
obj.Occurrences = 3
|
||||
obj.RotationAxis = App.Vector(0, 0, 1)
|
||||
obj.RotationAngle = 15.0
|
||||
obj.RotationCenter = App.Vector(0, 0, 0)
|
||||
obj.CumulativeRotation = True
|
||||
|
||||
# Store metadata for ztools tracking
|
||||
obj.addProperty(
|
||||
"App::PropertyString",
|
||||
"ZTools_Type",
|
||||
"ZTools",
|
||||
"ZTools feature type",
|
||||
)
|
||||
obj.ZTools_Type = "RotatedLinearPattern"
|
||||
|
||||
def execute(self, obj):
|
||||
"""Recompute the feature."""
|
||||
if not obj.Source or not hasattr(obj.Source, "Shape"):
|
||||
return
|
||||
|
||||
source_shape = obj.Source.Shape
|
||||
if source_shape.isNull():
|
||||
return
|
||||
|
||||
occurrences = max(1, obj.Occurrences)
|
||||
if occurrences == 1:
|
||||
obj.Shape = source_shape.copy()
|
||||
return
|
||||
|
||||
# Calculate spacing
|
||||
direction = App.Vector(obj.Direction)
|
||||
if direction.Length < 1e-6:
|
||||
direction = App.Vector(1, 0, 0)
|
||||
direction.normalize()
|
||||
|
||||
spacing = float(obj.Length) / (occurrences - 1) if occurrences > 1 else 0
|
||||
|
||||
shapes = []
|
||||
for i in range(occurrences):
|
||||
# Create translation
|
||||
offset = direction * spacing * i
|
||||
translated = source_shape.copy()
|
||||
translated.translate(offset)
|
||||
|
||||
# Apply rotation
|
||||
if abs(float(obj.RotationAngle)) > 1e-6:
|
||||
if obj.CumulativeRotation:
|
||||
angle = float(obj.RotationAngle) * i
|
||||
else:
|
||||
angle = float(obj.RotationAngle)
|
||||
|
||||
# Rotation center is relative to the translated position
|
||||
center = App.Vector(obj.RotationCenter) + offset
|
||||
axis = App.Vector(obj.RotationAxis)
|
||||
if axis.Length < 1e-6:
|
||||
axis = App.Vector(0, 0, 1)
|
||||
axis.normalize()
|
||||
|
||||
translated.rotate(center, axis, angle)
|
||||
|
||||
shapes.append(translated)
|
||||
|
||||
if shapes:
|
||||
obj.Shape = Part.makeCompound(shapes)
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
"""Handle property changes."""
|
||||
pass
|
||||
|
||||
|
||||
class RotatedLinearPatternViewProvider:
|
||||
"""View provider for rotated linear pattern."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
|
||||
def attach(self, vobj):
|
||||
self.Object = vobj.Object
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
pass
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
pass
|
||||
|
||||
def getIcon(self):
|
||||
return get_icon("rotated_pattern")
|
||||
|
||||
def __getstate__(self):
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
return None
|
||||
|
||||
|
||||
class RotatedLinearPatternCommand:
|
||||
"""Command to create a rotated linear pattern."""
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": get_icon("rotated_pattern"),
|
||||
"MenuText": "Rotated Linear Pattern",
|
||||
"ToolTip": "Create a linear pattern with rotation for each instance",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
"""Command is active when there's a document and selection."""
|
||||
if App.ActiveDocument is None:
|
||||
return False
|
||||
sel = Gui.Selection.getSelection()
|
||||
return len(sel) == 1
|
||||
|
||||
def Activated(self):
|
||||
"""Execute the command."""
|
||||
sel = Gui.Selection.getSelection()
|
||||
if not sel:
|
||||
App.Console.PrintError("Please select an object first\n")
|
||||
return
|
||||
|
||||
source = sel[0]
|
||||
|
||||
# Create the feature
|
||||
doc = App.ActiveDocument
|
||||
obj = doc.addObject("Part::FeaturePython", "RotatedLinearPattern")
|
||||
RotatedLinearPatternFeature(obj)
|
||||
RotatedLinearPatternViewProvider(obj.ViewObject)
|
||||
|
||||
obj.Source = source
|
||||
obj.Label = f"RotatedPattern_{source.Label}"
|
||||
|
||||
# Hide source object
|
||||
if hasattr(source, "ViewObject"):
|
||||
source.ViewObject.Visibility = False
|
||||
|
||||
doc.recompute()
|
||||
|
||||
App.Console.PrintMessage(
|
||||
f"Created rotated linear pattern from {source.Label}\n"
|
||||
)
|
||||
|
||||
|
||||
# Register the command
|
||||
Gui.addCommand("ZTools_RotatedLinearPattern", RotatedLinearPatternCommand())
|
||||
601
ztools/ztools/commands/pocket_commands.py
Normal file
@@ -0,0 +1,601 @@
|
||||
# ztools/commands/pocket_commands.py
|
||||
# Enhanced Pocket feature with "Flip side to cut" option
|
||||
#
|
||||
# This provides an enhanced pocket workflow that includes the ability to
|
||||
# cut material OUTSIDE the sketch profile rather than inside (like SOLIDWORKS
|
||||
# "Flip side to cut" feature).
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
|
||||
class EnhancedPocketTaskPanel:
|
||||
"""Task panel for creating enhanced pocket features with flip option."""
|
||||
|
||||
# Pocket type modes matching FreeCAD's PartDesign::Pocket
|
||||
POCKET_TYPES = [
|
||||
("Dimension", 0),
|
||||
("Through All", 1),
|
||||
("To First", 2),
|
||||
("Up To Face", 3),
|
||||
("Two Dimensions", 4),
|
||||
]
|
||||
|
||||
def __init__(self, sketch=None):
|
||||
self.form = QtGui.QWidget()
|
||||
self.form.setWindowTitle("ztools Enhanced Pocket")
|
||||
self.sketch = sketch
|
||||
self.selected_face = None
|
||||
self.setup_ui()
|
||||
self.setup_selection_observer()
|
||||
|
||||
# If sketch provided, show it in selection
|
||||
if self.sketch:
|
||||
self.update_sketch_display()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QtGui.QVBoxLayout(self.form)
|
||||
|
||||
# Sketch selection display
|
||||
sketch_group = QtGui.QGroupBox("Sketch")
|
||||
sketch_layout = QtGui.QVBoxLayout(sketch_group)
|
||||
self.sketch_label = QtGui.QLabel("No sketch selected")
|
||||
self.sketch_label.setWordWrap(True)
|
||||
sketch_layout.addWidget(self.sketch_label)
|
||||
layout.addWidget(sketch_group)
|
||||
|
||||
# Type selection
|
||||
type_group = QtGui.QGroupBox("Type")
|
||||
type_layout = QtGui.QFormLayout(type_group)
|
||||
|
||||
self.type_combo = QtGui.QComboBox()
|
||||
for label, _ in self.POCKET_TYPES:
|
||||
self.type_combo.addItem(label)
|
||||
self.type_combo.currentIndexChanged.connect(self.on_type_changed)
|
||||
type_layout.addRow("Type:", self.type_combo)
|
||||
|
||||
layout.addWidget(type_group)
|
||||
|
||||
# Dimensions group
|
||||
self.dim_group = QtGui.QGroupBox("Dimensions")
|
||||
self.dim_layout = QtGui.QFormLayout(self.dim_group)
|
||||
|
||||
# Length input
|
||||
self.length_spin = QtGui.QDoubleSpinBox()
|
||||
self.length_spin.setRange(0.001, 10000)
|
||||
self.length_spin.setValue(10.0)
|
||||
self.length_spin.setSuffix(" mm")
|
||||
self.length_spin.setDecimals(3)
|
||||
self.dim_layout.addRow("Length:", self.length_spin)
|
||||
|
||||
# Length2 input (for Two Dimensions mode)
|
||||
self.length2_spin = QtGui.QDoubleSpinBox()
|
||||
self.length2_spin.setRange(0.001, 10000)
|
||||
self.length2_spin.setValue(10.0)
|
||||
self.length2_spin.setSuffix(" mm")
|
||||
self.length2_spin.setDecimals(3)
|
||||
self.length2_label = QtGui.QLabel("Length 2:")
|
||||
# Hidden by default
|
||||
self.length2_spin.setVisible(False)
|
||||
self.length2_label.setVisible(False)
|
||||
self.dim_layout.addRow(self.length2_label, self.length2_spin)
|
||||
|
||||
layout.addWidget(self.dim_group)
|
||||
|
||||
# Up To Face selection (hidden by default)
|
||||
self.face_group = QtGui.QGroupBox("Up To Face")
|
||||
face_layout = QtGui.QVBoxLayout(self.face_group)
|
||||
self.face_label = QtGui.QLabel("Select a face...")
|
||||
self.face_label.setWordWrap(True)
|
||||
face_layout.addWidget(self.face_label)
|
||||
self.face_group.setVisible(False)
|
||||
layout.addWidget(self.face_group)
|
||||
|
||||
# Direction options
|
||||
dir_group = QtGui.QGroupBox("Direction")
|
||||
dir_layout = QtGui.QVBoxLayout(dir_group)
|
||||
|
||||
self.reversed_cb = QtGui.QCheckBox("Reversed")
|
||||
self.reversed_cb.setToolTip("Reverse the pocket direction")
|
||||
dir_layout.addWidget(self.reversed_cb)
|
||||
|
||||
self.symmetric_cb = QtGui.QCheckBox("Symmetric to plane")
|
||||
self.symmetric_cb.setToolTip(
|
||||
"Extend pocket equally on both sides of sketch plane"
|
||||
)
|
||||
self.symmetric_cb.toggled.connect(self.on_symmetric_changed)
|
||||
dir_layout.addWidget(self.symmetric_cb)
|
||||
|
||||
layout.addWidget(dir_group)
|
||||
|
||||
# FLIP SIDE TO CUT - The main new feature
|
||||
flip_group = QtGui.QGroupBox("Flip Side to Cut")
|
||||
flip_layout = QtGui.QVBoxLayout(flip_group)
|
||||
|
||||
self.flipped_cb = QtGui.QCheckBox("Cut outside profile (keep inside)")
|
||||
self.flipped_cb.setToolTip(
|
||||
"Instead of removing material inside the sketch profile,\n"
|
||||
"remove material OUTSIDE the profile.\n\n"
|
||||
"This keeps only the material covered by the sketch,\n"
|
||||
"similar to SOLIDWORKS 'Flip side to cut' option."
|
||||
)
|
||||
flip_layout.addWidget(self.flipped_cb)
|
||||
|
||||
# Info label
|
||||
flip_info = QtGui.QLabel(
|
||||
"<i>When enabled, material outside the sketch profile is removed,\n"
|
||||
"leaving only the material inside the sketch boundary.</i>"
|
||||
)
|
||||
flip_info.setWordWrap(True)
|
||||
flip_info.setStyleSheet("color: gray; font-size: 10px;")
|
||||
flip_layout.addWidget(flip_info)
|
||||
|
||||
layout.addWidget(flip_group)
|
||||
|
||||
# Taper angle (optional)
|
||||
taper_group = QtGui.QGroupBox("Taper")
|
||||
taper_layout = QtGui.QFormLayout(taper_group)
|
||||
|
||||
self.taper_spin = QtGui.QDoubleSpinBox()
|
||||
self.taper_spin.setRange(-89.99, 89.99)
|
||||
self.taper_spin.setValue(0.0)
|
||||
self.taper_spin.setSuffix(" °")
|
||||
self.taper_spin.setDecimals(2)
|
||||
taper_layout.addRow("Taper Angle:", self.taper_spin)
|
||||
|
||||
layout.addWidget(taper_group)
|
||||
|
||||
# Create button
|
||||
self.create_btn = QtGui.QPushButton("Create Pocket")
|
||||
self.create_btn.clicked.connect(self.on_create)
|
||||
layout.addWidget(self.create_btn)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def setup_selection_observer(self):
|
||||
"""Setup selection observer to track user selections."""
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self, panel):
|
||||
self.panel = panel
|
||||
|
||||
def addSelection(self, doc, obj, sub, pos):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def removeSelection(self, doc, obj, sub):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
def clearSelection(self, doc):
|
||||
self.panel.on_selection_changed()
|
||||
|
||||
self.observer = SelectionObserver(self)
|
||||
Gui.Selection.addObserver(self.observer)
|
||||
|
||||
def on_selection_changed(self):
|
||||
"""Handle selection changes."""
|
||||
sel = Gui.Selection.getSelectionEx()
|
||||
|
||||
for s in sel:
|
||||
obj = s.Object
|
||||
|
||||
# Check if it's a sketch (for sketch selection)
|
||||
if obj.TypeId == "Sketcher::SketchObject" and not self.sketch:
|
||||
self.sketch = obj
|
||||
self.update_sketch_display()
|
||||
|
||||
# Check for face selection (for Up To Face mode)
|
||||
if s.SubElementNames:
|
||||
for sub in s.SubElementNames:
|
||||
if sub.startswith("Face"):
|
||||
shape = obj.Shape.getElement(sub)
|
||||
if isinstance(shape, Part.Face):
|
||||
self.selected_face = (obj, sub)
|
||||
self.face_label.setText(f"Face: {obj.Name}.{sub}")
|
||||
|
||||
def update_sketch_display(self):
|
||||
"""Update sketch label."""
|
||||
if self.sketch:
|
||||
self.sketch_label.setText(f"Sketch: {self.sketch.Label}")
|
||||
else:
|
||||
self.sketch_label.setText("No sketch selected")
|
||||
|
||||
def on_type_changed(self, index):
|
||||
"""Update UI based on pocket type."""
|
||||
pocket_type = self.POCKET_TYPES[index][1]
|
||||
|
||||
# Show/hide dimension inputs based on type
|
||||
show_length = pocket_type in [0, 4] # Dimension or Two Dimensions
|
||||
self.dim_group.setVisible(show_length or pocket_type == 4)
|
||||
self.length_spin.setVisible(show_length)
|
||||
|
||||
# Two Dimensions mode
|
||||
show_length2 = pocket_type == 4
|
||||
self.length2_spin.setVisible(show_length2)
|
||||
self.length2_label.setVisible(show_length2)
|
||||
|
||||
# Up To Face mode
|
||||
self.face_group.setVisible(pocket_type == 3)
|
||||
|
||||
def on_symmetric_changed(self, checked):
|
||||
"""Handle symmetric checkbox change."""
|
||||
if checked:
|
||||
self.reversed_cb.setEnabled(False)
|
||||
self.reversed_cb.setChecked(False)
|
||||
else:
|
||||
self.reversed_cb.setEnabled(True)
|
||||
|
||||
def get_body(self):
|
||||
"""Get the active PartDesign body."""
|
||||
if hasattr(Gui, "ActiveDocument") and Gui.ActiveDocument:
|
||||
active_view = Gui.ActiveDocument.ActiveView
|
||||
if hasattr(active_view, "getActiveObject"):
|
||||
body = active_view.getActiveObject("pdbody")
|
||||
if body:
|
||||
return body
|
||||
|
||||
# Fallback: find body containing the sketch
|
||||
if self.sketch:
|
||||
for obj in App.ActiveDocument.Objects:
|
||||
if obj.TypeId == "PartDesign::Body":
|
||||
if self.sketch in obj.Group:
|
||||
return obj
|
||||
|
||||
return None
|
||||
|
||||
def on_create(self):
|
||||
"""Create the pocket feature."""
|
||||
if not self.sketch:
|
||||
QtGui.QMessageBox.warning(
|
||||
self.form, "Error", "Please select a sketch first."
|
||||
)
|
||||
return
|
||||
|
||||
body = self.get_body()
|
||||
if not body:
|
||||
QtGui.QMessageBox.warning(
|
||||
self.form, "Error", "No active body found. Please activate a body."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
App.ActiveDocument.openTransaction("Create Enhanced Pocket")
|
||||
|
||||
flipped = self.flipped_cb.isChecked()
|
||||
|
||||
if flipped:
|
||||
self.create_flipped_pocket(body)
|
||||
else:
|
||||
self.create_standard_pocket(body)
|
||||
|
||||
App.ActiveDocument.commitTransaction()
|
||||
App.ActiveDocument.recompute()
|
||||
|
||||
App.Console.PrintMessage("Enhanced Pocket created successfully\n")
|
||||
|
||||
except Exception as e:
|
||||
App.ActiveDocument.abortTransaction()
|
||||
App.Console.PrintError(f"Failed to create pocket: {e}\n")
|
||||
QtGui.QMessageBox.critical(self.form, "Error", str(e))
|
||||
|
||||
def create_standard_pocket(self, body):
|
||||
"""Create a standard PartDesign Pocket."""
|
||||
pocket = body.newObject("PartDesign::Pocket", "Pocket")
|
||||
pocket.Profile = self.sketch
|
||||
|
||||
# Set type
|
||||
pocket_type = self.POCKET_TYPES[self.type_combo.currentIndex()][1]
|
||||
pocket.Type = pocket_type
|
||||
|
||||
# Set dimensions
|
||||
pocket.Length = self.length_spin.value()
|
||||
if pocket_type == 4: # Two Dimensions
|
||||
pocket.Length2 = self.length2_spin.value()
|
||||
|
||||
# Set direction options
|
||||
pocket.Reversed = self.reversed_cb.isChecked()
|
||||
pocket.Midplane = self.symmetric_cb.isChecked()
|
||||
|
||||
# Set taper
|
||||
if abs(self.taper_spin.value()) > 0.001:
|
||||
pocket.TaperAngle = self.taper_spin.value()
|
||||
|
||||
# Up To Face
|
||||
if pocket_type == 3 and self.selected_face:
|
||||
obj, sub = self.selected_face
|
||||
pocket.UpToFace = (obj, [sub])
|
||||
|
||||
# Hide sketch
|
||||
self.sketch.ViewObject.Visibility = False
|
||||
|
||||
# Add metadata
|
||||
pocket.addProperty(
|
||||
"App::PropertyString", "ZTools_Type", "ZTools", "ZTools feature type"
|
||||
)
|
||||
pocket.ZTools_Type = "EnhancedPocket"
|
||||
|
||||
def create_flipped_pocket(self, body):
|
||||
"""Create a flipped pocket (cut outside profile).
|
||||
|
||||
This uses Boolean Common operation: keeps only the intersection
|
||||
of the body with the extruded profile.
|
||||
"""
|
||||
# Get current body shape (the Tip)
|
||||
tip = body.Tip
|
||||
if not tip or not hasattr(tip, "Shape"):
|
||||
raise ValueError("Body has no valid tip shape")
|
||||
|
||||
base_shape = tip.Shape.copy()
|
||||
|
||||
# Get sketch profile
|
||||
sketch_shape = self.sketch.Shape
|
||||
if not sketch_shape.Wires:
|
||||
raise ValueError("Sketch has no closed profile")
|
||||
|
||||
# Create face from sketch wires
|
||||
wires = sketch_shape.Wires
|
||||
if len(wires) == 0:
|
||||
raise ValueError("Sketch has no wires")
|
||||
|
||||
# Create a face from the outer wire
|
||||
# For multiple wires, the first is outer, rest are holes
|
||||
face = Part.Face(wires[0])
|
||||
if len(wires) > 1:
|
||||
# Handle holes in the profile
|
||||
face = Part.Face(wires)
|
||||
|
||||
# Get extrusion direction (sketch normal)
|
||||
sketch_placement = self.sketch.Placement
|
||||
normal = sketch_placement.Rotation.multVec(App.Vector(0, 0, 1))
|
||||
|
||||
if self.reversed_cb.isChecked():
|
||||
normal = normal.negative()
|
||||
|
||||
# Calculate extrusion length/direction
|
||||
pocket_type = self.POCKET_TYPES[self.type_combo.currentIndex()][1]
|
||||
|
||||
if pocket_type == 0: # Dimension
|
||||
length = self.length_spin.value()
|
||||
if self.symmetric_cb.isChecked():
|
||||
# Symmetric: extrude half in each direction
|
||||
half_length = length / 2
|
||||
tool_solid = face.extrude(normal * half_length)
|
||||
tool_solid2 = face.extrude(normal.negative() * half_length)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
elif pocket_type == 1: # Through All
|
||||
# Use a large value based on bounding box
|
||||
bbox = base_shape.BoundBox
|
||||
diagonal = bbox.DiagonalLength
|
||||
length = diagonal * 2
|
||||
|
||||
if self.symmetric_cb.isChecked():
|
||||
tool_solid = face.extrude(normal * length)
|
||||
tool_solid2 = face.extrude(normal.negative() * length)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
elif pocket_type == 4: # Two Dimensions
|
||||
length1 = self.length_spin.value()
|
||||
length2 = self.length2_spin.value()
|
||||
tool_solid = face.extrude(normal * length1)
|
||||
tool_solid2 = face.extrude(normal.negative() * length2)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
|
||||
else:
|
||||
# For other types, fall back to Through All behavior
|
||||
bbox = base_shape.BoundBox
|
||||
length = bbox.DiagonalLength * 2
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
# Apply taper if specified
|
||||
# Note: Taper with flipped pocket is complex, skip for now
|
||||
if abs(self.taper_spin.value()) > 0.001:
|
||||
App.Console.PrintWarning(
|
||||
"Taper angle is not supported with Flip Side to Cut. Ignoring.\n"
|
||||
)
|
||||
|
||||
# Boolean Common: keep only intersection
|
||||
result_shape = base_shape.common(tool_solid)
|
||||
|
||||
if result_shape.isNull() or result_shape.Volume < 1e-6:
|
||||
raise ValueError(
|
||||
"Flip pocket resulted in empty shape. "
|
||||
"Make sure the sketch profile intersects with the body."
|
||||
)
|
||||
|
||||
# Create a FeaturePython object to hold the result
|
||||
feature = body.newObject("PartDesign::FeaturePython", "FlippedPocket")
|
||||
|
||||
# Set up the feature
|
||||
FlippedPocketFeature(feature, self.sketch, result_shape)
|
||||
FlippedPocketViewProvider(feature.ViewObject)
|
||||
|
||||
# Store parameters as properties
|
||||
feature.addProperty("App::PropertyDistance", "Length", "Pocket", "Pocket depth")
|
||||
feature.Length = self.length_spin.value()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyBool", "Reversed", "Pocket", "Reverse direction"
|
||||
)
|
||||
feature.Reversed = self.reversed_cb.isChecked()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyBool", "Symmetric", "Pocket", "Symmetric to plane"
|
||||
)
|
||||
feature.Symmetric = self.symmetric_cb.isChecked()
|
||||
|
||||
feature.addProperty(
|
||||
"App::PropertyInteger", "PocketType", "Pocket", "Pocket type"
|
||||
)
|
||||
feature.PocketType = pocket_type
|
||||
|
||||
# Hide sketch
|
||||
self.sketch.ViewObject.Visibility = False
|
||||
|
||||
def accept(self):
|
||||
"""Called when OK is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def reject(self):
|
||||
"""Called when Cancel is clicked."""
|
||||
Gui.Selection.removeObserver(self.observer)
|
||||
return True
|
||||
|
||||
def getStandardButtons(self):
|
||||
return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel
|
||||
|
||||
|
||||
class FlippedPocketFeature:
|
||||
"""Feature object for flipped pocket (cuts outside profile)."""
|
||||
|
||||
def __init__(self, obj, sketch, initial_shape):
|
||||
obj.Proxy = self
|
||||
self.sketch = sketch
|
||||
|
||||
# Store reference to sketch
|
||||
obj.addProperty("App::PropertyLink", "Profile", "Base", "Sketch profile")
|
||||
obj.Profile = sketch
|
||||
|
||||
# ZTools metadata
|
||||
obj.addProperty(
|
||||
"App::PropertyString", "ZTools_Type", "ZTools", "ZTools feature type"
|
||||
)
|
||||
obj.ZTools_Type = "FlippedPocket"
|
||||
|
||||
# Set initial shape
|
||||
obj.Shape = initial_shape
|
||||
|
||||
def execute(self, obj):
|
||||
"""Recompute the flipped pocket."""
|
||||
if not obj.Profile:
|
||||
return
|
||||
|
||||
# Get the base feature (previous feature in the body)
|
||||
body = obj.getParentGeoFeatureGroup()
|
||||
if not body:
|
||||
return
|
||||
|
||||
# Find the feature before this one
|
||||
base_feature = None
|
||||
group = body.Group
|
||||
for i, feat in enumerate(group):
|
||||
if feat == obj and i > 0:
|
||||
base_feature = group[i - 1]
|
||||
break
|
||||
|
||||
if not base_feature or not hasattr(base_feature, "Shape"):
|
||||
return
|
||||
|
||||
base_shape = base_feature.Shape.copy()
|
||||
sketch = obj.Profile
|
||||
|
||||
# Get sketch profile
|
||||
sketch_shape = sketch.Shape
|
||||
if not sketch_shape.Wires:
|
||||
return
|
||||
|
||||
wires = sketch_shape.Wires
|
||||
face = Part.Face(wires[0])
|
||||
if len(wires) > 1:
|
||||
face = Part.Face(wires)
|
||||
|
||||
# Get direction
|
||||
sketch_placement = sketch.Placement
|
||||
normal = sketch_placement.Rotation.multVec(App.Vector(0, 0, 1))
|
||||
|
||||
if hasattr(obj, "Reversed") and obj.Reversed:
|
||||
normal = normal.negative()
|
||||
|
||||
# Get length
|
||||
length = obj.Length.Value if hasattr(obj, "Length") else 10.0
|
||||
symmetric = obj.Symmetric if hasattr(obj, "Symmetric") else False
|
||||
pocket_type = obj.PocketType if hasattr(obj, "PocketType") else 0
|
||||
|
||||
# Create tool solid
|
||||
if pocket_type == 1: # Through All
|
||||
bbox = base_shape.BoundBox
|
||||
length = bbox.DiagonalLength * 2
|
||||
|
||||
if symmetric:
|
||||
half = length / 2
|
||||
tool_solid = face.extrude(normal * half)
|
||||
tool_solid2 = face.extrude(normal.negative() * half)
|
||||
tool_solid = tool_solid.fuse(tool_solid2)
|
||||
else:
|
||||
tool_solid = face.extrude(normal * length)
|
||||
|
||||
# Boolean Common
|
||||
result_shape = base_shape.common(tool_solid)
|
||||
obj.Shape = result_shape
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
"""Handle property changes."""
|
||||
pass
|
||||
|
||||
|
||||
class FlippedPocketViewProvider:
|
||||
"""View provider for flipped pocket."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
|
||||
def attach(self, vobj):
|
||||
self.Object = vobj.Object
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
pass
|
||||
|
||||
def onChanged(self, vobj, prop):
|
||||
pass
|
||||
|
||||
def getIcon(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return get_icon("pocket_flipped")
|
||||
|
||||
def __getstate__(self):
|
||||
return None
|
||||
|
||||
def __setstate__(self, state):
|
||||
return None
|
||||
|
||||
|
||||
class ZTools_EnhancedPocket:
|
||||
"""Command to create enhanced pocket with flip option."""
|
||||
|
||||
def GetResources(self):
|
||||
from ztools.resources.icons import get_icon
|
||||
|
||||
return {
|
||||
"Pixmap": get_icon("pocket_enhanced"),
|
||||
"MenuText": "Enhanced Pocket",
|
||||
"ToolTip": (
|
||||
"Create a pocket with additional options including\n"
|
||||
"'Flip side to cut' - removes material outside the sketch profile"
|
||||
),
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
# Check if a sketch is selected
|
||||
sketch = None
|
||||
sel = Gui.Selection.getSelection()
|
||||
for obj in sel:
|
||||
if obj.TypeId == "Sketcher::SketchObject":
|
||||
sketch = obj
|
||||
break
|
||||
|
||||
panel = EnhancedPocketTaskPanel(sketch)
|
||||
Gui.Control.showDialog(panel)
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
|
||||
# Register the command
|
||||
Gui.addCommand("ZTools_EnhancedPocket", ZTools_EnhancedPocket())
|
||||
39
ztools/ztools/datums/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# ztools/datums - Datum creation tools
|
||||
from .core import (
|
||||
# Planes
|
||||
plane_offset_from_face,
|
||||
plane_midplane,
|
||||
plane_from_3_points,
|
||||
plane_normal_to_edge,
|
||||
plane_angled,
|
||||
plane_tangent_to_cylinder,
|
||||
# Axes
|
||||
axis_from_2_points,
|
||||
axis_from_edge,
|
||||
axis_cylinder_center,
|
||||
axis_intersection_planes,
|
||||
# Points
|
||||
point_at_vertex,
|
||||
point_at_coordinates,
|
||||
point_on_edge,
|
||||
point_center_of_face,
|
||||
point_center_of_circle,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"plane_offset_from_face",
|
||||
"plane_midplane",
|
||||
"plane_from_3_points",
|
||||
"plane_normal_to_edge",
|
||||
"plane_angled",
|
||||
"plane_tangent_to_cylinder",
|
||||
"axis_from_2_points",
|
||||
"axis_from_edge",
|
||||
"axis_cylinder_center",
|
||||
"axis_intersection_planes",
|
||||
"point_at_vertex",
|
||||
"point_at_coordinates",
|
||||
"point_on_edge",
|
||||
"point_center_of_face",
|
||||
"point_center_of_circle",
|
||||
]
|
||||
BIN
ztools/ztools/datums/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ztools/ztools/datums/__pycache__/core.cpython-312.pyc
Normal file
BIN
ztools/ztools/datums/__pycache__/core.cpython-313.pyc
Normal file
1138
ztools/ztools/datums/core.py
Normal file
10
ztools/ztools/resources/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# ztools/resources - Icons and assets
|
||||
from .icons import MOCHA, get_icon, save_icons_to_disk
|
||||
from .theme import get_stylesheet
|
||||
|
||||
__all__ = [
|
||||
"get_icon",
|
||||
"save_icons_to_disk",
|
||||
"MOCHA",
|
||||
"get_stylesheet",
|
||||
]
|
||||
BIN
ztools/ztools/resources/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ztools/ztools/resources/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ztools/ztools/resources/__pycache__/icons.cpython-312.pyc
Normal file
BIN
ztools/ztools/resources/__pycache__/icons.cpython-313.pyc
Normal file
BIN
ztools/ztools/resources/__pycache__/theme.cpython-312.pyc
Normal file
BIN
ztools/ztools/resources/__pycache__/theme.cpython-313.pyc
Normal file
386
ztools/ztools/resources/icons.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# ztools/resources/icons.py
|
||||
# Catppuccin Mocha themed icons for ztools
|
||||
|
||||
# Catppuccin Mocha Palette
|
||||
MOCHA = {
|
||||
"rosewater": "#f5e0dc",
|
||||
"flamingo": "#f2cdcd",
|
||||
"pink": "#f5c2e7",
|
||||
"mauve": "#cba6f7",
|
||||
"red": "#f38ba8",
|
||||
"maroon": "#eba0ac",
|
||||
"peach": "#fab387",
|
||||
"yellow": "#f9e2af",
|
||||
"green": "#a6e3a1",
|
||||
"teal": "#94e2d5",
|
||||
"sky": "#89dceb",
|
||||
"sapphire": "#74c7ec",
|
||||
"blue": "#89b4fa",
|
||||
"lavender": "#b4befe",
|
||||
"text": "#cdd6f4",
|
||||
"subtext1": "#bac2de",
|
||||
"subtext0": "#a6adc8",
|
||||
"overlay2": "#9399b2",
|
||||
"overlay1": "#7f849c",
|
||||
"overlay0": "#6c7086",
|
||||
"surface2": "#585b70",
|
||||
"surface1": "#45475a",
|
||||
"surface0": "#313244",
|
||||
"base": "#1e1e2e",
|
||||
"mantle": "#181825",
|
||||
"crust": "#11111b",
|
||||
}
|
||||
|
||||
|
||||
def _svg_to_base64(svg_content: str) -> str:
|
||||
"""Convert SVG string to base64 data URI for FreeCAD."""
|
||||
import base64
|
||||
|
||||
encoded = base64.b64encode(svg_content.encode("utf-8")).decode("utf-8")
|
||||
return f"data:image/svg+xml;base64,{encoded}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SVG Icon Definitions
|
||||
# =============================================================================
|
||||
|
||||
# Workbench main icon - stylized "Z"
|
||||
ICON_WORKBENCH_SVG = f'''<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"]}"/>
|
||||
<path d="M8 10 L24 10 L10 22 L24 22" stroke="{MOCHA["mauve"]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<circle cx="24" cy="10" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Datum Creator icon - plane with plus
|
||||
ICON_DATUM_CREATOR_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"]}"/>
|
||||
<!-- Plane representation -->
|
||||
<path d="M6 20 L16 8 L26 20 L16 26 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Plus sign -->
|
||||
<circle cx="24" cy="8" r="6" fill="{MOCHA["green"]}"/>
|
||||
<path d="M24 5 L24 11 M21 8 L27 8" stroke="{MOCHA["base"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Datum Manager icon - stacked planes with list
|
||||
ICON_DATUM_MANAGER_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"]}"/>
|
||||
<!-- Stacked planes -->
|
||||
<path d="M4 18 L12 12 L20 18 L12 22 Z" fill="{MOCHA["blue"]}" fill-opacity="0.5" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<path d="M4 14 L12 8 L20 14 L12 18 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.5" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
<!-- List lines -->
|
||||
<line x1="22" y1="10" x2="28" y2="10" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="16" x2="28" y2="16" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="22" x2="28" y2="22" stroke="{MOCHA["text"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Offset icon
|
||||
ICON_PLANE_OFFSET_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"]}"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 14 L28 22 L16 28 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Offset plane -->
|
||||
<path d="M4 14 L16 6 L28 14 L16 20 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Offset arrow -->
|
||||
<path d="M16 24 L16 18" stroke="{MOCHA["peach"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 20 L16 17 L18 20" stroke="{MOCHA["peach"]}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Midplane icon
|
||||
ICON_PLANE_MIDPLANE_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"]}"/>
|
||||
<!-- Top plane -->
|
||||
<path d="M4 10 L16 4 L28 10 L16 14 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Middle plane (result) -->
|
||||
<path d="M4 16 L16 10 L28 16 L16 20 Z" fill="{MOCHA["green"]}" fill-opacity="0.7" stroke="{MOCHA["teal"]}" stroke-width="1.5"/>
|
||||
<!-- Bottom plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane 3 Points icon
|
||||
ICON_PLANE_3PT_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"]}"/>
|
||||
<!-- Plane -->
|
||||
<path d="M4 20 L16 8 L28 18 L14 28 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Three points -->
|
||||
<circle cx="8" cy="20" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="16" cy="10" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="24" cy="18" r="3" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Normal to Edge icon
|
||||
ICON_PLANE_NORMAL_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"]}"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 26 Q16 6 26 16" stroke="{MOCHA["yellow"]}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Plane perpendicular -->
|
||||
<path d="M12 8 L20 12 L20 24 L12 20 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Point on curve -->
|
||||
<circle cx="16" cy="16" r="2.5" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Angled icon
|
||||
ICON_PLANE_ANGLED_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"]}"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="{MOCHA["overlay1"]}" stroke="{MOCHA["overlay2"]}" stroke-width="1"/>
|
||||
<!-- Angled plane -->
|
||||
<path d="M8 10 L20 6 L24 18 L12 22 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.7" stroke="{MOCHA["lavender"]}" stroke-width="1.5"/>
|
||||
<!-- Angle arc -->
|
||||
<path d="M14 20 Q18 18 18 14" stroke="{MOCHA["peach"]}" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Plane Tangent icon
|
||||
ICON_PLANE_TANGENT_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"]}"/>
|
||||
<!-- Cylinder outline -->
|
||||
<ellipse cx="12" cy="16" rx="6" ry="10" fill="none" stroke="{MOCHA["yellow"]}" stroke-width="2"/>
|
||||
<!-- Tangent plane -->
|
||||
<path d="M18 6 L28 10 L28 26 L18 22 Z" fill="{MOCHA["blue"]}" fill-opacity="0.7" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Tangent point -->
|
||||
<circle cx="18" cy="16" r="2.5" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis 2 Points icon
|
||||
ICON_AXIS_2PT_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"]}"/>
|
||||
<!-- Axis line -->
|
||||
<line x1="6" y1="26" x2="26" y2="6" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<!-- End points -->
|
||||
<circle cx="6" cy="26" r="3" fill="{MOCHA["peach"]}"/>
|
||||
<circle cx="26" cy="6" r="3" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis from Edge icon
|
||||
ICON_AXIS_EDGE_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"]}"/>
|
||||
<!-- Box edge representation -->
|
||||
<path d="M8 24 L8 12 L20 8 L20 20 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Selected edge highlighted -->
|
||||
<line x1="8" y1="24" x2="8" y2="12" stroke="{MOCHA["yellow"]}" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Resulting axis -->
|
||||
<line x1="8" y1="28" x2="8" y2="4" stroke="{MOCHA["red"]}" stroke-width="2" stroke-dasharray="4,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis Cylinder Center icon
|
||||
ICON_AXIS_CYL_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"]}"/>
|
||||
<!-- Cylinder -->
|
||||
<ellipse cx="16" cy="8" rx="8" ry="3" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M8 8 L8 24" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M24 8 L24 24" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<ellipse cx="16" cy="24" rx="8" ry="3" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Center axis -->
|
||||
<line x1="16" y1="4" x2="16" y2="28" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="16" r="2" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Axis Intersection icon
|
||||
ICON_AXIS_INTERSECT_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"]}"/>
|
||||
<!-- First plane -->
|
||||
<path d="M4 12 L16 6 L28 12 L16 18 Z" fill="{MOCHA["blue"]}" fill-opacity="0.5" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<!-- Second plane -->
|
||||
<path d="M4 20 L16 14 L28 20 L16 26 Z" fill="{MOCHA["mauve"]}" fill-opacity="0.5" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
<!-- Intersection axis -->
|
||||
<line x1="4" y1="16" x2="28" y2="16" stroke="{MOCHA["red"]}" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Point at Vertex icon
|
||||
ICON_POINT_VERTEX_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"]}"/>
|
||||
<!-- Wireframe box corner -->
|
||||
<path d="M10 20 L10 10 L20 6" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L20 16 L20 6" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L4 24" stroke="{MOCHA["overlay1"]}" stroke-width="1.5" fill="none"/>
|
||||
<!-- Vertex point -->
|
||||
<circle cx="10" cy="20" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="10" cy="20" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Point XYZ icon
|
||||
ICON_POINT_XYZ_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"]}"/>
|
||||
<!-- Coordinate axes -->
|
||||
<line x1="6" y1="24" x2="26" y2="24" stroke="{MOCHA["red"]}" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="6" y2="6" stroke="{MOCHA["green"]}" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="16" y2="28" stroke="{MOCHA["blue"]}" stroke-width="1.5"/>
|
||||
<!-- Point -->
|
||||
<circle cx="18" cy="12" r="4" fill="{MOCHA["peach"]}"/>
|
||||
<!-- Projection lines -->
|
||||
<line x1="18" y1="12" x2="18" y2="24" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
<line x1="18" y1="12" x2="6" y2="12" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Point on Edge icon
|
||||
ICON_POINT_EDGE_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"]}"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 24 Q16 4 26 20" stroke="{MOCHA["yellow"]}" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Point on edge -->
|
||||
<circle cx="14" cy="12" r="4" fill="{MOCHA["green"]}"/>
|
||||
<!-- Parameter indicator -->
|
||||
<text x="20" y="10" font-family="monospace" font-size="8" fill="{MOCHA["text"]}">t</text>
|
||||
</svg>'''
|
||||
|
||||
# Point Face Center icon
|
||||
ICON_POINT_FACE_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"]}"/>
|
||||
<!-- Face -->
|
||||
<path d="M6 20 L16 10 L26 20 L16 26 Z" fill="{MOCHA["blue"]}" fill-opacity="0.6" stroke="{MOCHA["sapphire"]}" stroke-width="1.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="19" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="16" cy="19" r="2" fill="{MOCHA["teal"]}"/>
|
||||
</svg>'''
|
||||
|
||||
# Point Circle Center icon
|
||||
ICON_POINT_CIRCLE_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"]}"/>
|
||||
<!-- Circle -->
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="{MOCHA["yellow"]}" stroke-width="2.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="16" r="4" fill="{MOCHA["green"]}"/>
|
||||
<circle cx="16" cy="16" r="2" fill="{MOCHA["teal"]}"/>
|
||||
<!-- Radius line -->
|
||||
<line x1="16" y1="16" x2="26" y2="16" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>'''
|
||||
|
||||
# Rotated Linear Pattern icon - objects along line with rotation
|
||||
ICON_ROTATED_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 line -->
|
||||
<line x1="4" y1="24" x2="28" y2="24" stroke="{MOCHA["overlay1"]}" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- First cube (original) -->
|
||||
<rect x="4" y="16" width="6" height="6" fill="{MOCHA["blue"]}" stroke="{MOCHA["sapphire"]}" stroke-width="1"/>
|
||||
<!-- Second cube (rotated 15deg) -->
|
||||
<g transform="translate(14,19) rotate(-15)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="{MOCHA["mauve"]}" stroke="{MOCHA["lavender"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Third cube (rotated 30deg) -->
|
||||
<g transform="translate(24,19) rotate(-30)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="{MOCHA["pink"]}" stroke="{MOCHA["flamingo"]}" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M24 8 A5 5 0 0 1 19 13" stroke="{MOCHA["peach"]}" stroke-width="1.5" fill="none"/>
|
||||
<path d="M18 11 L19 13 L21 12" stroke="{MOCHA["peach"]}" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
</svg>'''
|
||||
|
||||
# Enhanced Pocket icon - pocket with plus/settings indicator
|
||||
ICON_POCKET_ENHANCED_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"]}"/>
|
||||
<!-- 3D block with pocket cutout -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="{MOCHA["overlay1"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Pocket depression -->
|
||||
<path d="M10 12 L16 10 L22 12 L22 18 L16 20 L10 18 Z" fill="{MOCHA["base"]}" stroke="{MOCHA["mauve"]}" stroke-width="1.5"/>
|
||||
<!-- Down arrow indicating cut -->
|
||||
<path d="M16 13 L16 17" stroke="{MOCHA["red"]}" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 15 L16 18 L18 15" stroke="{MOCHA["red"]}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>'''
|
||||
|
||||
# Flipped Pocket icon - pocket cutting outside the profile
|
||||
ICON_POCKET_FLIPPED_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"]}"/>
|
||||
<!-- Outer material removed (dark) -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="{MOCHA["base"]}" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="{MOCHA["overlay0"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="{MOCHA["overlay0"]}" stroke-width="1"/>
|
||||
<!-- Inner remaining material (raised) -->
|
||||
<path d="M10 20 L10 12 L16 10 L22 12 L22 20 L16 22 Z" fill="{MOCHA["surface2"]}" stroke="{MOCHA["teal"]}" stroke-width="1.5"/>
|
||||
<path d="M10 12 L16 14 L22 12" stroke="{MOCHA["overlay1"]}" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 22" stroke="{MOCHA["overlay1"]}" stroke-width="1"/>
|
||||
<!-- Flip arrows -->
|
||||
<path d="M4 16 L8 14 L8 18 Z" fill="{MOCHA["peach"]}"/>
|
||||
<path d="M28 16 L24 14 L24 18 Z" fill="{MOCHA["peach"]}"/>
|
||||
</svg>'''
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Icon Registry - Base64 encoded for FreeCAD
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_icon(name: str) -> str:
|
||||
"""Get icon file path by name.
|
||||
|
||||
Returns the path to an SVG icon file. If the file doesn't exist,
|
||||
it will be created from the embedded SVG definitions.
|
||||
"""
|
||||
import os
|
||||
|
||||
# Map of short names to SVG content
|
||||
icons = {
|
||||
"workbench": ICON_WORKBENCH_SVG,
|
||||
"datum_creator": ICON_DATUM_CREATOR_SVG,
|
||||
"datum_manager": ICON_DATUM_MANAGER_SVG,
|
||||
"plane_offset": ICON_PLANE_OFFSET_SVG,
|
||||
"plane_midplane": ICON_PLANE_MIDPLANE_SVG,
|
||||
"plane_3pt": ICON_PLANE_3PT_SVG,
|
||||
"plane_normal": ICON_PLANE_NORMAL_SVG,
|
||||
"plane_angled": ICON_PLANE_ANGLED_SVG,
|
||||
"plane_tangent": ICON_PLANE_TANGENT_SVG,
|
||||
"axis_2pt": ICON_AXIS_2PT_SVG,
|
||||
"axis_edge": ICON_AXIS_EDGE_SVG,
|
||||
"axis_cyl": ICON_AXIS_CYL_SVG,
|
||||
"axis_intersect": ICON_AXIS_INTERSECT_SVG,
|
||||
"point_vertex": ICON_POINT_VERTEX_SVG,
|
||||
"point_xyz": ICON_POINT_XYZ_SVG,
|
||||
"point_edge": ICON_POINT_EDGE_SVG,
|
||||
"point_face": ICON_POINT_FACE_SVG,
|
||||
"point_circle": ICON_POINT_CIRCLE_SVG,
|
||||
"rotated_pattern": ICON_ROTATED_PATTERN_SVG,
|
||||
"pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
|
||||
"pocket_flipped": ICON_POCKET_FLIPPED_SVG,
|
||||
}
|
||||
|
||||
if name not in icons:
|
||||
return ""
|
||||
|
||||
# Get the icons directory path (relative to this file)
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||
icon_path = os.path.join(icons_dir, f"ztools_{name}.svg")
|
||||
|
||||
# If the icon file doesn't exist, create it
|
||||
if not os.path.exists(icon_path):
|
||||
os.makedirs(icons_dir, exist_ok=True)
|
||||
with open(icon_path, "w") as f:
|
||||
f.write(icons[name])
|
||||
|
||||
return icon_path
|
||||
|
||||
|
||||
def save_icons_to_disk(directory: str):
|
||||
"""Save all icons as SVG files to a directory."""
|
||||
import os
|
||||
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
icons = {
|
||||
"ztools_workbench": ICON_WORKBENCH_SVG,
|
||||
"ztools_datum_creator": ICON_DATUM_CREATOR_SVG,
|
||||
"ztools_datum_manager": ICON_DATUM_MANAGER_SVG,
|
||||
"ztools_plane_offset": ICON_PLANE_OFFSET_SVG,
|
||||
"ztools_plane_midplane": ICON_PLANE_MIDPLANE_SVG,
|
||||
"ztools_plane_3pt": ICON_PLANE_3PT_SVG,
|
||||
"ztools_plane_normal": ICON_PLANE_NORMAL_SVG,
|
||||
"ztools_plane_angled": ICON_PLANE_ANGLED_SVG,
|
||||
"ztools_plane_tangent": ICON_PLANE_TANGENT_SVG,
|
||||
"ztools_axis_2pt": ICON_AXIS_2PT_SVG,
|
||||
"ztools_axis_edge": ICON_AXIS_EDGE_SVG,
|
||||
"ztools_axis_cyl": ICON_AXIS_CYL_SVG,
|
||||
"ztools_axis_intersect": ICON_AXIS_INTERSECT_SVG,
|
||||
"ztools_point_vertex": ICON_POINT_VERTEX_SVG,
|
||||
"ztools_point_xyz": ICON_POINT_XYZ_SVG,
|
||||
"ztools_point_edge": ICON_POINT_EDGE_SVG,
|
||||
"ztools_point_face": ICON_POINT_FACE_SVG,
|
||||
"ztools_point_circle": ICON_POINT_CIRCLE_SVG,
|
||||
"ztools_rotated_pattern": ICON_ROTATED_PATTERN_SVG,
|
||||
"ztools_pocket_enhanced": ICON_POCKET_ENHANCED_SVG,
|
||||
"ztools_pocket_flipped": ICON_POCKET_FLIPPED_SVG,
|
||||
}
|
||||
|
||||
for name, svg in icons.items():
|
||||
filepath = os.path.join(directory, f"{name}.svg")
|
||||
with open(filepath, "w") as f:
|
||||
f.write(svg)
|
||||
print(f"Saved: {filepath}")
|
||||
8
ztools/ztools/resources/icons/ztools_axis_2pt.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<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"/>
|
||||
<!-- Axis line -->
|
||||
<line x1="6" y1="26" x2="26" y2="6" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<!-- End points -->
|
||||
<circle cx="6" cy="26" r="3" fill="#fab387"/>
|
||||
<circle cx="26" cy="6" r="3" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
11
ztools/ztools/resources/icons/ztools_axis_cyl.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<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"/>
|
||||
<!-- Cylinder -->
|
||||
<ellipse cx="16" cy="8" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M8 8 L8 24" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M24 8 L24 24" stroke="#7f849c" stroke-width="1"/>
|
||||
<ellipse cx="16" cy="24" rx="8" ry="3" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Center axis -->
|
||||
<line x1="16" y1="4" x2="16" y2="28" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="16" r="2" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
9
ztools/ztools/resources/icons/ztools_axis_edge.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Box edge representation -->
|
||||
<path d="M8 24 L8 12 L20 8 L20 20 Z" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Selected edge highlighted -->
|
||||
<line x1="8" y1="24" x2="8" y2="12" stroke="#f9e2af" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Resulting axis -->
|
||||
<line x1="8" y1="28" x2="8" y2="4" stroke="#f38ba8" stroke-width="2" stroke-dasharray="4,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 515 B |
9
ztools/ztools/resources/icons/ztools_axis_intersect.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- First plane -->
|
||||
<path d="M4 12 L16 6 L28 12 L16 18 Z" fill="#89b4fa" fill-opacity="0.5" stroke="#74c7ec" stroke-width="1"/>
|
||||
<!-- Second plane -->
|
||||
<path d="M4 20 L16 14 L28 20 L16 26 Z" fill="#cba6f7" fill-opacity="0.5" stroke="#b4befe" stroke-width="1"/>
|
||||
<!-- Intersection axis -->
|
||||
<line x1="4" y1="16" x2="28" y2="16" stroke="#f38ba8" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 531 B |
8
ztools/ztools/resources/icons/ztools_datum_creator.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<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"/>
|
||||
<!-- Plane representation -->
|
||||
<path d="M6 20 L16 8 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Plus sign -->
|
||||
<circle cx="24" cy="8" r="6" fill="#a6e3a1"/>
|
||||
<path d="M24 5 L24 11 M21 8 L27 8" stroke="#1e1e2e" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 443 B |
10
ztools/ztools/resources/icons/ztools_datum_manager.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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"/>
|
||||
<!-- Stacked planes -->
|
||||
<path d="M4 18 L12 12 L20 18 L12 22 Z" fill="#89b4fa" fill-opacity="0.5" stroke="#74c7ec" stroke-width="1"/>
|
||||
<path d="M4 14 L12 8 L20 14 L12 18 Z" fill="#cba6f7" fill-opacity="0.5" stroke="#b4befe" stroke-width="1"/>
|
||||
<!-- List lines -->
|
||||
<line x1="22" y1="10" x2="28" y2="10" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="16" x2="28" y2="16" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="22" x2="28" y2="22" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
9
ztools/ztools/resources/icons/ztools_plane_3pt.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Plane -->
|
||||
<path d="M4 20 L16 8 L28 18 L14 28 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Three points -->
|
||||
<circle cx="8" cy="20" r="3" fill="#fab387"/>
|
||||
<circle cx="16" cy="10" r="3" fill="#fab387"/>
|
||||
<circle cx="24" cy="18" r="3" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
9
ztools/ztools/resources/icons/ztools_plane_angled.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Angled plane -->
|
||||
<path d="M8 10 L20 6 L24 18 L12 22 Z" fill="#cba6f7" fill-opacity="0.7" stroke="#b4befe" stroke-width="1.5"/>
|
||||
<!-- Angle arc -->
|
||||
<path d="M14 20 Q18 18 18 14" stroke="#fab387" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
9
ztools/ztools/resources/icons/ztools_plane_midplane.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Top plane -->
|
||||
<path d="M4 10 L16 4 L28 10 L16 14 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Middle plane (result) -->
|
||||
<path d="M4 16 L16 10 L28 16 L16 20 Z" fill="#a6e3a1" fill-opacity="0.7" stroke="#94e2d5" stroke-width="1.5"/>
|
||||
<!-- Bottom plane -->
|
||||
<path d="M4 22 L16 16 L28 22 L16 26 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
9
ztools/ztools/resources/icons/ztools_plane_normal.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 26 Q16 6 26 16" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Plane perpendicular -->
|
||||
<path d="M12 8 L20 12 L20 24 L12 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Point on curve -->
|
||||
<circle cx="16" cy="16" r="2.5" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 480 B |
10
ztools/ztools/resources/icons/ztools_plane_offset.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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"/>
|
||||
<!-- Base plane -->
|
||||
<path d="M4 22 L16 14 L28 22 L16 28 Z" fill="#7f849c" stroke="#9399b2" stroke-width="1"/>
|
||||
<!-- Offset plane -->
|
||||
<path d="M4 14 L16 6 L28 14 L16 20 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Offset arrow -->
|
||||
<path d="M16 24 L16 18" stroke="#fab387" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 20 L16 17 L18 20" stroke="#fab387" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 621 B |
9
ztools/ztools/resources/icons/ztools_plane_tangent.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Cylinder outline -->
|
||||
<ellipse cx="12" cy="16" rx="6" ry="10" fill="none" stroke="#f9e2af" stroke-width="2"/>
|
||||
<!-- Tangent plane -->
|
||||
<path d="M18 6 L28 10 L28 26 L18 22 Z" fill="#89b4fa" fill-opacity="0.7" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Tangent point -->
|
||||
<circle cx="18" cy="16" r="2.5" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
12
ztools/ztools/resources/icons/ztools_pocket_enhanced.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<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"/>
|
||||
<!-- 3D block with pocket cutout -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="#585b70" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="#7f849c" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Pocket depression -->
|
||||
<path d="M10 12 L16 10 L22 12 L22 18 L16 20 L10 18 Z" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Down arrow indicating cut -->
|
||||
<path d="M16 13 L16 17" stroke="#f38ba8" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M14 15 L16 18 L18 15" stroke="#f38ba8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 807 B |
14
ztools/ztools/resources/icons/ztools_pocket_flipped.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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"/>
|
||||
<!-- Outer material removed (dark) -->
|
||||
<path d="M6 22 L6 10 L16 6 L26 10 L26 22 L16 26 Z" fill="#1e1e2e" stroke="#7f849c" stroke-width="1"/>
|
||||
<path d="M6 10 L16 14 L26 10" stroke="#6c7086" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 26" stroke="#6c7086" stroke-width="1"/>
|
||||
<!-- Inner remaining material (raised) -->
|
||||
<path d="M10 20 L10 12 L16 10 L22 12 L22 20 L16 22 Z" fill="#585b70" stroke="#94e2d5" stroke-width="1.5"/>
|
||||
<path d="M10 12 L16 14 L22 12" stroke="#7f849c" stroke-width="1" fill="none"/>
|
||||
<path d="M16 14 L16 22" stroke="#7f849c" stroke-width="1"/>
|
||||
<!-- Flip arrows -->
|
||||
<path d="M4 16 L8 14 L8 18 Z" fill="#fab387"/>
|
||||
<path d="M28 16 L24 14 L24 18 Z" fill="#fab387"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 842 B |
10
ztools/ztools/resources/icons/ztools_point_circle.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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"/>
|
||||
<!-- Circle -->
|
||||
<circle cx="16" cy="16" r="10" fill="none" stroke="#f9e2af" stroke-width="2.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="16" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="16" cy="16" r="2" fill="#94e2d5"/>
|
||||
<!-- Radius line -->
|
||||
<line x1="16" y1="16" x2="26" y2="16" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 479 B |
9
ztools/ztools/resources/icons/ztools_point_edge.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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"/>
|
||||
<!-- Edge/curve -->
|
||||
<path d="M6 24 Q16 4 26 20" stroke="#f9e2af" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Point on edge -->
|
||||
<circle cx="14" cy="12" r="4" fill="#a6e3a1"/>
|
||||
<!-- Parameter indicator -->
|
||||
<text x="20" y="10" font-family="monospace" font-size="8" fill="#cdd6f4">t</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
8
ztools/ztools/resources/icons/ztools_point_face.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<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"/>
|
||||
<!-- Face -->
|
||||
<path d="M6 20 L16 10 L26 20 L16 26 Z" fill="#89b4fa" fill-opacity="0.6" stroke="#74c7ec" stroke-width="1.5"/>
|
||||
<!-- Center point -->
|
||||
<circle cx="16" cy="19" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="16" cy="19" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
10
ztools/ztools/resources/icons/ztools_point_vertex.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<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"/>
|
||||
<!-- Wireframe box corner -->
|
||||
<path d="M10 20 L10 10 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L20 16 L20 6" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<path d="M10 20 L4 24" stroke="#7f849c" stroke-width="1.5" fill="none"/>
|
||||
<!-- Vertex point -->
|
||||
<circle cx="10" cy="20" r="4" fill="#a6e3a1"/>
|
||||
<circle cx="10" cy="20" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
12
ztools/ztools/resources/icons/ztools_point_xyz.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<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"/>
|
||||
<!-- Coordinate axes -->
|
||||
<line x1="6" y1="24" x2="26" y2="24" stroke="#f38ba8" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="6" y2="6" stroke="#a6e3a1" stroke-width="1.5"/>
|
||||
<line x1="6" y1="24" x2="16" y2="28" stroke="#89b4fa" stroke-width="1.5"/>
|
||||
<!-- Point -->
|
||||
<circle cx="18" cy="12" r="4" fill="#fab387"/>
|
||||
<!-- Projection lines -->
|
||||
<line x1="18" y1="12" x2="18" y2="24" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
<line x1="18" y1="12" x2="6" y2="12" stroke="#7f849c" stroke-width="1" stroke-dasharray="2,2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 681 B |
18
ztools/ztools/resources/icons/ztools_rotated_pattern.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<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 line -->
|
||||
<line x1="4" y1="24" x2="28" y2="24" stroke="#7f849c" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<!-- First cube (original) -->
|
||||
<rect x="4" y="16" width="6" height="6" fill="#89b4fa" stroke="#74c7ec" stroke-width="1"/>
|
||||
<!-- Second cube (rotated 15deg) -->
|
||||
<g transform="translate(14,19) rotate(-15)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="#cba6f7" stroke="#b4befe" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Third cube (rotated 30deg) -->
|
||||
<g transform="translate(24,19) rotate(-30)">
|
||||
<rect x="-3" y="-3" width="6" height="6" fill="#f5c2e7" stroke="#f2cdcd" stroke-width="1"/>
|
||||
</g>
|
||||
<!-- Rotation arrow -->
|
||||
<path d="M24 8 A5 5 0 0 1 19 13" stroke="#fab387" stroke-width="1.5" fill="none"/>
|
||||
<path d="M18 11 L19 13 L21 12" stroke="#fab387" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 980 B |
15
ztools/ztools/resources/icons/ztools_theme_apply.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<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"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots on palette -->
|
||||
<circle cx="9" cy="16" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Check mark -->
|
||||
<circle cx="24" cy="8" r="5" fill="#a6e3a1"/>
|
||||
<path d="M21.5 8 L23 9.5 L26.5 6" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 767 B |
15
ztools/ztools/resources/icons/ztools_theme_export.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<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"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="16" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots -->
|
||||
<circle cx="9" cy="14" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="11" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="14" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="19" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="18" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Download arrow -->
|
||||
<rect x="21" y="20" width="8" height="8" rx="2" fill="#fab387"/>
|
||||
<path d="M25 22 L25 26 M23 24.5 L25 26.5 L27 24.5" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
15
ztools/ztools/resources/icons/ztools_theme_remove.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<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"/>
|
||||
<!-- Paint palette (dimmed) -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#6c7086" stroke-width="1.5"/>
|
||||
<!-- Color dots (dimmed) -->
|
||||
<circle cx="9" cy="16" r="2" fill="#7f849c"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#7f849c"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#7f849c"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#7f849c"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- X mark -->
|
||||
<circle cx="24" cy="8" r="5" fill="#f38ba8"/>
|
||||
<path d="M22 6 L26 10 M26 6 L22 10" stroke="#11111b" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 748 B |
17
ztools/ztools/resources/icons/ztools_theme_toggle.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
|
||||
<!-- Paint palette -->
|
||||
<ellipse cx="14" cy="18" rx="10" ry="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="1.5"/>
|
||||
<!-- Color dots -->
|
||||
<circle cx="9" cy="16" r="2" fill="#f38ba8"/>
|
||||
<circle cx="14" cy="13" r="2" fill="#f9e2af"/>
|
||||
<circle cx="19" cy="16" r="2" fill="#a6e3a1"/>
|
||||
<circle cx="14" cy="21" r="2" fill="#89b4fa"/>
|
||||
<!-- Thumb hole -->
|
||||
<ellipse cx="10" cy="20" rx="2" ry="1.5" fill="#313244"/>
|
||||
<!-- Toggle arrows -->
|
||||
<path d="M22 5 A4 4 0 0 1 26 9" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<path d="M25 5 L22 5 L22 8" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<path d="M28 11 A4 4 0 0 1 24 7" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<path d="M25 11 L28 11 L28 8" stroke="#94e2d5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
ztools/ztools/resources/icons/ztools_workbench.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<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"/>
|
||||
<path d="M8 10 L24 10 L10 22 L24 22" stroke="#cba6f7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<circle cx="24" cy="10" r="2" fill="#94e2d5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |