first commit

This commit is contained in:
Zoe Forbes
2026-01-24 15:16:09 -06:00
commit 981b15804e
67 changed files with 7119 additions and 0 deletions

View 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>

File diff suppressed because it is too large Load Diff

89
Makefile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

11
ztools/setup.cfg Normal file
View 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

View File

@@ -0,0 +1,2 @@
# ztools - Extended PartDesign for FreeCAD
__version__ = "0.1.0"

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
# ztools/commands - GUI commands
from . import datum_commands, pattern_commands, pocket_commands
__all__ = ["datum_commands", "pattern_commands", "pocket_commands"]

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

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

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

View 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",
]

Binary file not shown.

Binary file not shown.

1138
ztools/ztools/datums/core.py Normal file

File diff suppressed because it is too large Load Diff

View 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",
]

View 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}")

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="2" y="2" width="28" height="28" rx="4" fill="#313244"/>
<!-- 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

View 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

File diff suppressed because it is too large Load Diff