Compare commits
13 Commits
06911d27aa
...
b3a58a6d92
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3a58a6d92 | ||
| 2c564bea8a | |||
|
|
e443538548 | ||
| be0f8128ec | |||
|
|
48f5cc9311 | ||
| cb4b3363ec | |||
| 4334c9a494 | |||
| abc53e6b93 | |||
|
|
332e14f1f7 | ||
|
|
06475d5291 | ||
|
|
172f5cddee | ||
| f06bb96fdb | |||
|
|
04835c3629 |
177
docs/src/reference/datum-creator.md
Normal file
177
docs/src/reference/datum-creator.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Datum Creator System
|
||||
|
||||
The ZTools Datum Creator (`ZTools_DatumCreator`) creates parametric datum planes, axes, and points. It auto-detects the datum type from selected geometry and provides a unified task panel with 16 creation modes.
|
||||
|
||||
**Source files:**
|
||||
|
||||
- `mods/ztools/ztools/ztools/commands/datum_commands.py` -- UI, selection handling, mode detection
|
||||
- `mods/ztools/ztools/ztools/datums/core.py` -- geometry computation and FreeCAD object creation
|
||||
|
||||
## Geometry Classification
|
||||
|
||||
The `SelectionItem` class wraps a selected geometry element and classifies it into one of seven types:
|
||||
|
||||
| Type | Detection | Icon |
|
||||
|------|-----------|------|
|
||||
| face | Planar `Part.Face` or object with "Plane" in TypeId | ▢ |
|
||||
| plane | Datum plane objects or planar faces | ▣ |
|
||||
| cylinder | `Part.Face` with `Surface.Cylinder` | ◎ |
|
||||
| edge | `Part.Edge` with `Part.Line` curve | ― |
|
||||
| circle | `Part.Edge` with `Part.Circle` or `Part.ArcOfCircle` | ○ |
|
||||
| vertex | `Part.Vertex` | • |
|
||||
| unknown | Unclassified geometry | ? |
|
||||
|
||||
Classification flow:
|
||||
|
||||
1. Extract shape from object using the sub-element name (Face#, Edge#, Vertex#)
|
||||
2. Check shape type (Face, Edge, Vertex)
|
||||
3. For faces: check `surface.isPlanar()` or `isinstance(surface, Part.Cylinder)`
|
||||
4. For edges: check curve type (Line, Circle, ArcOfCircle)
|
||||
5. Fall back to `unknown`
|
||||
|
||||
## Creation Modes
|
||||
|
||||
### Planes (7 modes)
|
||||
|
||||
| Mode | Required Selection | Parameters | Description |
|
||||
|------|--------------------|------------|-------------|
|
||||
| `offset_face` | 1 face | distance (mm) | Plane parallel to face at offset |
|
||||
| `offset_plane` | 1 plane | distance (mm) | Plane parallel to datum plane at offset |
|
||||
| `midplane` | 2 faces | -- | Plane halfway between two parallel faces |
|
||||
| `3_points` | 3 vertices | -- | Plane through three non-collinear points |
|
||||
| `normal_edge` | 1 edge | position (0--1) | Plane perpendicular to edge at parameter |
|
||||
| `angled` | 1 face + 1 edge | angle (deg) | Plane at angle to face, rotating about edge |
|
||||
| `tangent_cyl` | 1 cylinder | angle (deg) | Plane tangent to cylinder at angular position |
|
||||
|
||||
### Axes (4 modes)
|
||||
|
||||
| Mode | Required Selection | Parameters | Description |
|
||||
|------|--------------------|------------|-------------|
|
||||
| `axis_2pt` | 2 vertices | -- | Axis through two points |
|
||||
| `axis_edge` | 1 edge | -- | Axis along linear edge |
|
||||
| `axis_cyl` | 1 cylinder | -- | Axis along cylinder centerline |
|
||||
| `axis_intersect` | 2 planes | -- | Axis at intersection of two planes |
|
||||
|
||||
### Points (5 modes)
|
||||
|
||||
| Mode | Required Selection | Parameters | Description |
|
||||
|------|--------------------|------------|-------------|
|
||||
| `point_vertex` | 1 vertex | -- | Point at vertex location |
|
||||
| `point_xyz` | (none) | x, y, z (mm) | Point at explicit coordinates |
|
||||
| `point_edge` | 1 edge | position (0--1) | Point at parameter location on edge |
|
||||
| `point_face` | 1 face | -- | Point at center of mass of face |
|
||||
| `point_circle` | 1 circle | -- | Point at center of circular edge |
|
||||
|
||||
## Auto-Detection Algorithm
|
||||
|
||||
The `_match_score()` method scores how well the current selection matches each mode's required types.
|
||||
|
||||
**Scoring:**
|
||||
|
||||
1. If the selection has fewer items than required, score is **0** (no match).
|
||||
2. For each required type, find a matching selected type using fuzzy matching:
|
||||
- `face` requirement matches `face` or `cylinder`
|
||||
- `edge` requirement matches `edge` or `circle`
|
||||
- All other types require exact match
|
||||
3. Score calculation:
|
||||
- **Exact cardinality** (selection count == required count): score = `100 + matched_count`
|
||||
- **Over-selected** (more items than required): score = `matched_count`
|
||||
- **No match**: score = `0`
|
||||
|
||||
The mode with the highest score wins. On each selection change, `update_mode_from_selection()` re-evaluates all 16 modes and activates the best match.
|
||||
|
||||
The detected mode is displayed with a category color:
|
||||
|
||||
- Planes: Mauve (#cba6f7)
|
||||
- Axes: Teal (#94e2d5)
|
||||
- Points: Yellow (#f9e2af)
|
||||
|
||||
## Task Panel UI
|
||||
|
||||
```
|
||||
+-------------------------------------+
|
||||
| ZTools Datum Creator |
|
||||
+-------------------------------------+
|
||||
| Selection |
|
||||
| [icon] Element Name [Remove] |
|
||||
| [Add Selected] [Remove] [Clear] |
|
||||
+-------------------------------------+
|
||||
| Datum Type |
|
||||
| DETECTED MODE (colored) |
|
||||
| Override: [dropdown] |
|
||||
+-------------------------------------+
|
||||
| Parameters (dynamic per mode) |
|
||||
| Offset: [spinner] mm |
|
||||
| Angle: [spinner] deg |
|
||||
| Position: [spinner] 0-1 |
|
||||
| X/Y/Z: [spinner] mm |
|
||||
+-------------------------------------+
|
||||
| Options |
|
||||
| [ ] Link to Spreadsheet |
|
||||
| [x] Add to Active Body |
|
||||
| [ ] Custom Name: [text] |
|
||||
+-------------------------------------+
|
||||
| [OK] [Cancel] |
|
||||
+-------------------------------------+
|
||||
```
|
||||
|
||||
### Selection table
|
||||
|
||||
Three columns: type icon (28px), element name (stretch), remove button (28px). Duplicates are rejected. A FreeCAD `SelectionObserver` keeps the "Add Selected" button state synchronized with the active 3D selection.
|
||||
|
||||
### Mode override
|
||||
|
||||
The dropdown lists all 16 modes prefixed by category (`[P]` plane, `[A]` axis, `[Pt]` point). Selecting a mode disables auto-detection until "(Auto-detect)" is re-selected.
|
||||
|
||||
### Parameters section
|
||||
|
||||
Rebuilt dynamically when the mode changes. Only the parameter widgets relevant to the active mode are shown. Modes with no parameters hide the section entirely.
|
||||
|
||||
### Options
|
||||
|
||||
- **Link to Spreadsheet**: creates a spreadsheet alias and expression-links the datum parameter (offset, angle, coordinates) for parametric control.
|
||||
- **Add to Active Body**: when checked, creates a `PartDesign::Plane/Line/Point` inside the active body. When unchecked, creates a document-level `Part::Plane/Line/Vertex`.
|
||||
- **Custom Name**: overrides the auto-generated name (format: `ZPlane_Offset_001`).
|
||||
|
||||
## Dispatch to core.py
|
||||
|
||||
`DatumCreatorTaskPanel.accept()` calls `create_datum()`, which:
|
||||
|
||||
1. Reads the current mode, body, name, and parameters from the UI
|
||||
2. Extracts `SelectionItem` objects by type
|
||||
3. Dispatches to the corresponding `core.*` function:
|
||||
|
||||
| Mode | Core function |
|
||||
|------|---------------|
|
||||
| `offset_face` | `core.plane_offset_from_face(face, distance, ...)` |
|
||||
| `offset_plane` | `core.plane_offset_from_plane(plane, distance, ...)` |
|
||||
| `midplane` | `core.plane_midplane(face1, face2, ...)` |
|
||||
| `3_points` | `core.plane_from_3_points(p1, p2, p3, ...)` |
|
||||
| `normal_edge` | `core.plane_normal_to_edge(edge, parameter, ...)` |
|
||||
| `angled` | `core.plane_angled(face, edge, angle, ...)` |
|
||||
| `tangent_cyl` | `core.plane_tangent_to_cylinder(face, angle, ...)` |
|
||||
| `axis_2pt` | `core.axis_from_2_points(p1, p2, ...)` |
|
||||
| `axis_edge` | `core.axis_from_edge(edge, ...)` |
|
||||
| `axis_cyl` | `core.axis_cylinder_center(face, ...)` |
|
||||
| `axis_intersect` | `core.axis_intersection_planes(plane1, plane2, ...)` |
|
||||
| `point_vertex` | `core.point_at_vertex(vertex, ...)` |
|
||||
| `point_xyz` | `core.point_at_coordinates(x, y, z, ...)` |
|
||||
| `point_edge` | `core.point_on_edge(edge, parameter, ...)` |
|
||||
| `point_face` | `core.point_center_of_face(face, ...)` |
|
||||
| `point_circle` | `core.point_center_of_circle(edge, ...)` |
|
||||
|
||||
## Core Implementation
|
||||
|
||||
Each `core.*` function:
|
||||
|
||||
1. Creates the FreeCAD object (`PartDesign::Plane` in a body, `Part::Plane` at document level)
|
||||
2. Computes the placement from the source geometry
|
||||
3. Configures attachment (MapMode, AttachmentSupport, offset) for parametric updates
|
||||
4. Stores ZTools metadata in custom properties:
|
||||
- `ZTools_Type` -- creation method identifier (e.g. `"offset_from_face"`)
|
||||
- `ZTools_Params` -- parameters as JSON (e.g. `{"distance": 10}`)
|
||||
- `ZTools_SourceRefs` -- source geometry references as JSON
|
||||
5. Applies Catppuccin Mocha styling (mauve at 70% transparency for planes)
|
||||
6. Optionally links parameters to a spreadsheet alias
|
||||
|
||||
Auto-naming uses the format `ZPlane_Offset_001`, incrementing the index for each new datum of the same type.
|
||||
Submodule mods/silo updated: fed72676bc...9e99b83091
Submodule mods/ztools updated: 2bf969c62a...ef16ecbaa2
@@ -7,7 +7,7 @@ Icon=kindred-create
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Graphics;Science;Engineering;
|
||||
MimeType=application/x-extension-fcstd;x-scheme-handler/kindred;
|
||||
MimeType=application/x-extension-fcstd;application/x-kindred-create;x-scheme-handler/kindred;
|
||||
Keywords=CAD;3D;modeling;engineering;design;parametric;
|
||||
StartupNotify=true
|
||||
StartupWMClass=KindredCreate
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/x-extension-fcstd">
|
||||
<comment>Kindred Create Document</comment>
|
||||
<comment xml:lang="en">Kindred Create Document</comment>
|
||||
<comment>FreeCAD Document</comment>
|
||||
<comment xml:lang="en">FreeCAD Document</comment>
|
||||
<icon name="kindred-create"/>
|
||||
<glob pattern="*.FCStd"/>
|
||||
<glob pattern="*.fcstd"/>
|
||||
<glob pattern="*.FCSTD"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/x-kindred-create">
|
||||
<comment>Kindred Create Document</comment>
|
||||
<comment xml:lang="en">Kindred Create Document</comment>
|
||||
<icon name="kindred-create"/>
|
||||
<glob pattern="*.kc"/>
|
||||
<glob pattern="*.KC"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
|
||||
@@ -247,6 +247,8 @@ QDockWidget {
|
||||
QDockWidget::title {
|
||||
background-color: #181825;
|
||||
color: #cdd6f4;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 8px 6px;
|
||||
border-bottom: 1px solid #313244;
|
||||
min-height: 18px;
|
||||
@@ -1129,6 +1131,11 @@ QSint--ActionGroup QFrame {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSint--ActionGroup QSint--ActionLabel[class="header"] {
|
||||
color: #cdd6f4;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Input Field */
|
||||
Gui--InputField {
|
||||
background-color: #313244;
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
<FCBool Name="checkShowReportViewOnWarning" Value="1" />
|
||||
</FCParamGroup>
|
||||
<FCParamGroup Name="View">
|
||||
<FCUInt Name="BackgroundColor" Value="505294591" />
|
||||
<FCUInt Name="BackgroundColor2" Value="286333951" />
|
||||
<FCUInt Name="BackgroundColor3" Value="404235775" />
|
||||
<FCUInt Name="BackgroundColor4" Value="825378047" />
|
||||
<FCUInt Name="BackgroundColor" Value="404235775" />
|
||||
<FCUInt Name="BackgroundColor2" Value="1819313919" />
|
||||
<FCUInt Name="BackgroundColor3" Value="1819313919" />
|
||||
<FCUInt Name="BackgroundColor4" Value="404235775" />
|
||||
<FCBool Name="Simple" Value="0" />
|
||||
<FCBool Name="Gradient" Value="1" />
|
||||
<FCBool Name="UseBackgroundColorMid" Value="0" />
|
||||
<FCBool Name="UseBackgroundColorMid" Value="1" />
|
||||
<FCUInt Name="HighlightColor" Value="3416717311" />
|
||||
<FCUInt Name="SelectionColor" Value="3032415999" />
|
||||
<FCUInt Name="PreselectColor" Value="2497893887" />
|
||||
|
||||
@@ -247,6 +247,8 @@ QDockWidget {
|
||||
QDockWidget::title {
|
||||
background-color: #181825;
|
||||
color: #cdd6f4;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 8px 6px;
|
||||
border-bottom: 1px solid #313244;
|
||||
min-height: 18px;
|
||||
@@ -976,7 +978,7 @@ QSplitter::handle:hover {
|
||||
|
||||
QStatusBar {
|
||||
background-color: #181825;
|
||||
color: #bac2de;
|
||||
color: #cdd6f4;
|
||||
border-top: 1px solid #313244;
|
||||
}
|
||||
|
||||
@@ -1244,6 +1246,11 @@ QSint--ActionGroup QFrame {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
QSint--ActionGroup QSint--ActionLabel[class="header"] {
|
||||
color: #cdd6f4;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Input Field */
|
||||
Gui--InputField {
|
||||
background-color: #313244;
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
"""
|
||||
kc_format.py — .kc file format round-trip preservation.
|
||||
kc_format.py — .kc file format support.
|
||||
|
||||
Caches silo/ ZIP entries before FreeCAD's C++ save rewrites the ZIP
|
||||
from scratch, then re-injects them after save completes.
|
||||
Handles two responsibilities:
|
||||
1. Round-trip preservation: caches silo/ ZIP entries before FreeCAD's C++
|
||||
save rewrites the ZIP from scratch, then re-injects them after save.
|
||||
2. Manifest auto-creation: ensures every .kc file has silo/manifest.json.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import FreeCAD
|
||||
|
||||
# Cache: filepath -> {entry_name: bytes}
|
||||
_silo_cache = {}
|
||||
|
||||
KC_VERSION = "1.0"
|
||||
|
||||
|
||||
def _default_manifest():
|
||||
"""Generate a default silo/manifest.json for new .kc files."""
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
username = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
|
||||
return {
|
||||
"kc_version": KC_VERSION,
|
||||
"silo_instance": None,
|
||||
"part_uuid": str(uuid.uuid4()),
|
||||
"revision_hash": None,
|
||||
"created_at": now,
|
||||
"modified_at": now,
|
||||
"created_by": username,
|
||||
}
|
||||
|
||||
|
||||
class _KcFormatObserver:
|
||||
"""Document observer that preserves silo/ entries across saves."""
|
||||
@@ -35,22 +57,43 @@ class _KcFormatObserver:
|
||||
pass
|
||||
|
||||
def slotFinishSaveDocument(self, doc, filename):
|
||||
"""After save: re-inject cached silo/ entries into the .kc ZIP."""
|
||||
"""After save: re-inject cached silo/ entries and ensure manifest."""
|
||||
if not filename.lower().endswith(".kc"):
|
||||
_silo_cache.pop(filename, None)
|
||||
return
|
||||
entries = _silo_cache.pop(filename, None)
|
||||
if not entries:
|
||||
return
|
||||
try:
|
||||
with zipfile.ZipFile(filename, "a") as zf:
|
||||
existing = set(zf.namelist())
|
||||
for name, data in entries.items():
|
||||
if name not in existing:
|
||||
zf.writestr(name, data)
|
||||
# Re-inject cached silo/ entries
|
||||
if entries:
|
||||
for name, data in entries.items():
|
||||
if name not in existing:
|
||||
zf.writestr(name, data)
|
||||
existing.add(name)
|
||||
# Ensure silo/manifest.json exists
|
||||
if "silo/manifest.json" not in existing:
|
||||
manifest = _default_manifest()
|
||||
zf.writestr(
|
||||
"silo/manifest.json",
|
||||
json.dumps(manifest, indent=2) + "\n",
|
||||
)
|
||||
else:
|
||||
# Update modified_at timestamp
|
||||
raw = zf.read("silo/manifest.json")
|
||||
manifest = json.loads(raw)
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
if manifest.get("modified_at") != now:
|
||||
manifest["modified_at"] = now
|
||||
# ZipFile append mode can't overwrite; write new entry
|
||||
# (last duplicate wins in most ZIP readers)
|
||||
zf.writestr(
|
||||
"silo/manifest.json",
|
||||
json.dumps(manifest, indent=2) + "\n",
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
f"kc_format: failed to preserve silo/ entries: {e}\n"
|
||||
f"kc_format: failed to update .kc silo/ entries: {e}\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user