Compare commits
20 Commits
chore/upda
...
test/plana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c2887f5a | ||
|
|
983e211f12 | ||
|
|
1788b5778a | ||
| 418e947cbd | |||
| 4c9ff957e3 | |||
| 9aaf244179 | |||
| 69ccdbf742 | |||
| 4acd09171e | |||
| 7f909f166f | |||
| f69e0efec7 | |||
|
|
559a240799 | ||
| 7c85b2ad93 | |||
| 311d911cfa | |||
| 4ef8e64a7c | |||
| d94e8c8294 | |||
| 3550d916bd | |||
| 6e15b25134 | |||
| 82f2422285 | |||
| 314955c3ef | |||
| c7a7436e7b |
@@ -322,6 +322,7 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TAG: ${{ github.ref_name || inputs.tag }}
|
BUILD_TAG: ${{ github.ref_name || inputs.tag }}
|
||||||
|
COMMIT_SHA: ${{ github.sha }}
|
||||||
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/ca-certificates.crt
|
NODE_EXTRA_CA_CERTS: /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -386,6 +387,7 @@ jobs:
|
|||||||
'name': f'Kindred Create {tag}',
|
'name': f'Kindred Create {tag}',
|
||||||
'body': body,
|
'body': body,
|
||||||
'prerelease': prerelease,
|
'prerelease': prerelease,
|
||||||
|
'target_commitish': '${COMMIT_SHA}',
|
||||||
}))
|
}))
|
||||||
")
|
")
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -77,3 +77,5 @@ docs/book/
|
|||||||
|
|
||||||
# To regenerate themed icons: python3 icons/retheme.py
|
# To regenerate themed icons: python3 icons/retheme.py
|
||||||
# icons/themed/ is tracked (committed) so CI builds include them
|
# icons/themed/ is tracked (committed) so CI builds include them
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
285
CLAUDE.md
Normal file
285
CLAUDE.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# CLAUDE.md — Developer Context for Kindred Create
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Kindred Create is a fork of FreeCAD 1.0+ that adds integrated tooling for professional engineering workflows. It ships a context-aware UI system, two addon command sets (ztools and Silo), a Catppuccin Mocha dark theme, and a pluggable file origin layer on top of FreeCAD's parametric modeling core.
|
||||||
|
|
||||||
|
- **Kindred Create version:** 0.1.5
|
||||||
|
- **FreeCAD base version:** 1.2.0
|
||||||
|
- **License:** LGPL-2.1-or-later
|
||||||
|
- **Repository:** `git.kindred-systems.com/kindred/create` (Gitea)
|
||||||
|
- **Main branch:** `main`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recursive ssh://git@git.kindred-systems.com:2222/kindred/create.git
|
||||||
|
cd create
|
||||||
|
pixi run configure # CMake configure (debug by default)
|
||||||
|
pixi run build # Build
|
||||||
|
pixi run install # Install to build dir
|
||||||
|
pixi run freecad # Launch
|
||||||
|
pixi run test # Run C++ tests (ctest)
|
||||||
|
pixi run test-kindred # Run Python/Kindred tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Build variants: append `-debug` or `-release` (e.g., `pixi run build-release`). See `CMakePresets.json` for platform-specific presets (Linux x86_64/aarch64, macOS Intel/ARM, Windows x64).
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
create/
|
||||||
|
├── src/
|
||||||
|
│ ├── App/ Core application (C++)
|
||||||
|
│ ├── Base/ Base classes, type system, persistence (C++)
|
||||||
|
│ ├── Gui/ GUI framework (C++)
|
||||||
|
│ │ ├── EditingContext.h Editing context resolver (Kindred feature)
|
||||||
|
│ │ ├── BreadcrumbToolBar.h Breadcrumb navigation widget (Kindred feature)
|
||||||
|
│ │ ├── FileOrigin.h Abstract origin interface (Kindred feature)
|
||||||
|
│ │ ├── OriginManager.h Origin lifecycle management
|
||||||
|
│ │ ├── CommandOrigin.cpp Origin_Commit/Pull/Push/Info/BOM commands
|
||||||
|
│ │ ├── ApplicationPy.h All FreeCADGui.* Python bindings
|
||||||
|
│ │ ├── Application.h App signals (fastsignals)
|
||||||
|
│ │ ├── Stylesheets/ QSS theme files
|
||||||
|
│ │ └── PreferencePacks/ Preference configurations (build-time generated)
|
||||||
|
│ ├── Mod/ FreeCAD modules (PartDesign, Assembly, Sketcher, etc.)
|
||||||
|
│ │ └── Create/ Kindred Create module
|
||||||
|
│ │ ├── Init.py Console bootstrap — loads addons
|
||||||
|
│ │ ├── InitGui.py GUI bootstrap — loads addons, Silo setup, update checker
|
||||||
|
│ │ ├── addon_loader.py Manifest-driven loader with dependency resolution
|
||||||
|
│ │ └── kc_format.py .kc file format preservation
|
||||||
|
│ └── 3rdParty/ Vendored dependencies
|
||||||
|
│ ├── OndselSolver/ [submodule] Assembly constraint solver (forked)
|
||||||
|
│ ├── FastSignals/ Signal/slot library (NOT Boost)
|
||||||
|
│ └── GSL/ [submodule] Microsoft Guidelines Support Library
|
||||||
|
├── mods/ Kindred addon modules
|
||||||
|
│ ├── sdk/ Addon SDK — stable API contract (priority 0)
|
||||||
|
│ ├── ztools/ [submodule] Command provider (priority 50)
|
||||||
|
│ ├── silo/ [submodule] PLM workbench (priority 60)
|
||||||
|
│ ├── solver/ [submodule] Assembly solver research (GNN-based)
|
||||||
|
│ └── quicknav/ [submodule] Navigation addon
|
||||||
|
├── docs/ mdBook documentation + architecture docs
|
||||||
|
├── tests/ C++ unit tests (GoogleTest)
|
||||||
|
├── package/ Packaging (debian/, rattler-build/)
|
||||||
|
├── resources/ Branding, icons, desktop integration
|
||||||
|
├── cMake/ CMake helper modules
|
||||||
|
├── .gitea/workflows/ CI/CD pipelines
|
||||||
|
├── CMakeLists.txt Root build configuration (CMake 3.22.0+)
|
||||||
|
├── CMakePresets.json Platform build presets
|
||||||
|
└── pixi.toml Pixi environment and build tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build System
|
||||||
|
|
||||||
|
- **Primary:** CMake 3.22.0+ with Ninja generator
|
||||||
|
- **Environment:** [Pixi](https://pixi.sh) (conda-forge) manages all dependencies
|
||||||
|
- **Key deps:** Qt 6.8.x, Python 3.11.x, OpenCASCADE 7.8.x, PySide6, Boost, VTK, SMESH
|
||||||
|
- **Presets:** `conda-linux-debug`, `conda-linux-release`, `conda-macos-debug`, `conda-macos-release`, `conda-windows-debug`, `conda-windows-release`
|
||||||
|
- **Tasks summary:**
|
||||||
|
|
||||||
|
| Task | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `pixi run configure` | CMake configure (debug) |
|
||||||
|
| `pixi run build` | Build (debug) |
|
||||||
|
| `pixi run install` | Install to build dir |
|
||||||
|
| `pixi run freecad` | Launch FreeCAD |
|
||||||
|
| `pixi run test` | C++ tests via ctest |
|
||||||
|
| `pixi run test-kindred` | Python/Kindred test suite |
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Signals — Use FastSignals, NOT Boost
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <fastsignals/signal.h>
|
||||||
|
// See src/Gui/Application.h:121-155 for signal declarations
|
||||||
|
```
|
||||||
|
|
||||||
|
All signals in `src/Gui/` use `fastsignals::signal`. Never use Boost.Signals2.
|
||||||
|
|
||||||
|
### Type Checking Across Modules
|
||||||
|
|
||||||
|
Avoid header dependencies between `src/Gui/` and `src/Mod/` by using runtime type checks:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto type = Base::Type::fromName("Sketcher::SketchObject");
|
||||||
|
if (obj->isDerivedFrom(type)) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Bindings
|
||||||
|
|
||||||
|
All `FreeCADGui.*` functions go in `src/Gui/ApplicationPy.h` and `src/Gui/ApplicationPy.cpp`. Use `METH_VARARGS` only (no `METH_KEYWORDS` in this file). Do not create separate files for new Python bindings.
|
||||||
|
|
||||||
|
### Toolbar Visibility
|
||||||
|
|
||||||
|
Use `ToolBarItem::DefaultVisibility::Unavailable` to hide toolbars by default, then `ToolBarManager::setState(ForceAvailable)` to show them contextually. This pattern is proven by the Sketcher module.
|
||||||
|
|
||||||
|
The `appendToolbar` Python API accepts an optional 3rd argument: `"Visible"`, `"Hidden"`, or `"Unavailable"`.
|
||||||
|
|
||||||
|
### Editing Context System
|
||||||
|
|
||||||
|
The `EditingContextResolver` singleton (`src/Gui/EditingContext.h/.cpp`) drives the context-aware UI. It evaluates registered context definitions in priority order and activates the matching one, setting toolbar visibility and updating the `BreadcrumbToolBar`.
|
||||||
|
|
||||||
|
Built-in contexts: `sketcher.edit`, `assembly.edit`, `partdesign.feature`, `partdesign.body`, `assembly.idle`, `spreadsheet`, `empty_document`, `no_document`.
|
||||||
|
|
||||||
|
Python API:
|
||||||
|
- `FreeCADGui.registerEditingContext()` — register a new context
|
||||||
|
- `FreeCADGui.registerEditingOverlay()` — conditional toolbar overlay
|
||||||
|
- `FreeCADGui.injectEditingCommands()` — add commands to existing contexts
|
||||||
|
- `FreeCADGui.currentEditingContext()` — query active context
|
||||||
|
- `FreeCADGui.refreshEditingContext()` — force re-evaluation
|
||||||
|
|
||||||
|
### Addon Loading
|
||||||
|
|
||||||
|
Addons in `mods/` are loaded by `src/Mod/Create/addon_loader.py`. Each addon provides a `package.xml` with `<kindred>` extensions declaring version bounds, load priority, and dependencies. The loader resolves via topological sort: **sdk** (0) -> **ztools** (50) -> **silo** (60).
|
||||||
|
|
||||||
|
A `<workbench>` tag in `package.xml` is required for `InitGui.py` to be loaded, even if no actual workbench is registered.
|
||||||
|
|
||||||
|
### Deferred Initialization
|
||||||
|
|
||||||
|
GUI setup uses `QTimer.singleShot` with staggered delays:
|
||||||
|
- 500ms: `.kc` file format registration
|
||||||
|
- 1500ms: Silo origin registration
|
||||||
|
- 2000ms: Auth dock + ztools commands
|
||||||
|
- 2500ms: Silo overlay
|
||||||
|
- 3000ms: Silo first-start check
|
||||||
|
- 4000ms: Activity panel
|
||||||
|
- 10000ms: Update checker
|
||||||
|
|
||||||
|
### Unified Origin System
|
||||||
|
|
||||||
|
File operations (New, Open, Save, Commit, Pull, Push) are abstracted behind `FileOrigin` (`src/Gui/FileOrigin.h`). `LocalFileOrigin` handles local files; `SiloOrigin` (`mods/silo/freecad/silo_origin.py`) backs Silo-tracked documents. The active origin is selected automatically based on document properties (`SiloItemId`, `SiloPartNumber`).
|
||||||
|
|
||||||
|
## Submodules
|
||||||
|
|
||||||
|
| Path | Repository | Branch | Purpose |
|
||||||
|
|------|------------|--------|---------|
|
||||||
|
| `mods/ztools` | `git.kindred-systems.com/forbes/ztools` | `main` | Extended PartDesign/Assembly/Spreadsheet tools |
|
||||||
|
| `mods/silo` | `git.kindred-systems.com/kindred/silo-mod` | `main` | PLM workbench (includes silo-client submodule) |
|
||||||
|
| `mods/solver` | `git.kindred-systems.com/kindred/solver` | `main` | Assembly solver research (GNN-based) |
|
||||||
|
| `mods/quicknav` | `git.kindred-systems.com/kindred/quicknav` | — | Navigation addon |
|
||||||
|
| `src/3rdParty/OndselSolver` | `git.kindred-systems.com/kindred/solver` | — | Constraint solver (forked with NR fix) |
|
||||||
|
| `src/3rdParty/GSL` | `github.com/microsoft/GSL` | — | Guidelines Support Library |
|
||||||
|
| `src/Mod/AddonManager` | `github.com/FreeCAD/AddonManager` | — | FreeCAD addon manager |
|
||||||
|
| `tests/lib` | `github.com/google/googletest` | — | C++ test framework |
|
||||||
|
|
||||||
|
Update a submodule:
|
||||||
|
```bash
|
||||||
|
cd mods/silo
|
||||||
|
git checkout main && git pull
|
||||||
|
cd ../..
|
||||||
|
git add mods/silo
|
||||||
|
git commit -m "chore: update silo submodule"
|
||||||
|
```
|
||||||
|
|
||||||
|
Initialize all submodules: `git submodule update --init --recursive`
|
||||||
|
|
||||||
|
## Key Addon Modules
|
||||||
|
|
||||||
|
### ztools (`mods/ztools/`)
|
||||||
|
|
||||||
|
Command provider (NOT a workbench). Injects tools into PartDesign, Assembly, and Spreadsheet contexts via `_ZToolsManipulator` (WorkbenchManipulator) and `injectEditingCommands()`.
|
||||||
|
|
||||||
|
Commands: `ZTools_DatumCreator`, `ZTools_EnhancedPocket`, `ZTools_RotatedLinearPattern`, `ZTools_AssemblyLinearPattern`, `ZTools_AssemblyPolarPattern`, spreadsheet formatting (Bold, Italic, Underline, alignment, colors, QuickAlias).
|
||||||
|
|
||||||
|
Source: `mods/ztools/ztools/ztools/commands/` (note the double `ztools` nesting).
|
||||||
|
|
||||||
|
### Silo (`mods/silo/`)
|
||||||
|
|
||||||
|
PLM workbench with 14 commands for parts lifecycle management. Go REST API server + PostgreSQL + MinIO backend. FreeCAD client communicates via shared `silo-client` submodule.
|
||||||
|
|
||||||
|
Silo origin detection: `silo_origin.py:ownsDocument()` checks for `SiloItemId`/`SiloPartNumber` properties on the active document.
|
||||||
|
|
||||||
|
### SDK (`mods/sdk/`)
|
||||||
|
|
||||||
|
Stable API contract for addons. Provides wrappers for editing contexts, theme tokens (Catppuccin Mocha YAML palette), FileOrigin registration, and deferred dock panels. Addons should use `kindred_sdk.*` instead of `FreeCADGui.*` internals where possible.
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
|
||||||
|
- **Canonical source:** `src/Gui/Stylesheets/KindredCreate.qss`
|
||||||
|
- The PreferencePacks copy at `src/Gui/PreferencePacks/KindredCreate/KindredCreate.qss` is **generated at build time** via `configure_file()`. Only edit the Stylesheets copy.
|
||||||
|
- Color palette: Catppuccin Mocha (26 colors + 14 semantic roles, defined in `mods/sdk/kindred_sdk/palettes/catppuccin-mocha.yaml`)
|
||||||
|
- Default preferences: `src/Gui/PreferencePacks/KindredCreate/KindredCreate.cfg`
|
||||||
|
|
||||||
|
## Git Conventions
|
||||||
|
|
||||||
|
### Branch Names
|
||||||
|
|
||||||
|
`type/kebab-case-description`
|
||||||
|
|
||||||
|
Types: `feat/`, `fix/`, `chore/`, `docs/`, `refactor/`, `art/`
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
|
||||||
|
[Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): lowercase imperative description
|
||||||
|
```
|
||||||
|
|
||||||
|
| Prefix | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `feat:` | New feature |
|
||||||
|
| `fix:` | Bug fix |
|
||||||
|
| `chore:` | Maintenance, dependencies |
|
||||||
|
| `docs:` | Documentation only |
|
||||||
|
| `art:` | Icons, theme, visual assets |
|
||||||
|
| `refactor:` | Code restructuring |
|
||||||
|
|
||||||
|
Scopes: `solver`, `sketcher`, `editing-context`, `toolbar`, `ztools`, `silo`, `breadcrumb`, `gui`, `assembly`, `ci`, `theme`, `quicknav`, or omitted.
|
||||||
|
|
||||||
|
### PR Workflow
|
||||||
|
|
||||||
|
1. Create a branch from `main`: `git checkout -b feat/my-feature main`
|
||||||
|
2. Commit with conventional commit messages
|
||||||
|
3. Push and open a PR against `main` via Gitea (or `tea pulls create`)
|
||||||
|
4. CI runs automatically on PRs
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- **C++:** clang-format (`.clang-format`), clang-tidy (`.clang-tidy`)
|
||||||
|
- **Python:** black (100-char line length), pylint (`.pylintrc`)
|
||||||
|
- **Pre-commit hooks:** `pre-commit install` (runs clang-format, black, trailing-whitespace, etc.)
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
- **Build:** `.gitea/workflows/build.yml` — runs on pushes to `main` and on PRs
|
||||||
|
- **Release:** `.gitea/workflows/release.yml` — triggered by `v*` tags, builds AppImage and .deb
|
||||||
|
- **Platform:** Currently Linux x86_64 only in CI; other platforms have presets but no runners yet
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Document | Content |
|
||||||
|
|----------|---------|
|
||||||
|
| `README.md` | Project overview, installation, usage |
|
||||||
|
| `CONTRIBUTING.md` | Branch workflow, commit conventions, code style |
|
||||||
|
| `docs/ARCHITECTURE.md` | Bootstrap flow, addon lifecycle, source layout |
|
||||||
|
| `docs/COMPONENTS.md` | Feature inventory (ztools, Silo, origin, theme, icons) |
|
||||||
|
| `docs/KNOWN_ISSUES.md` | Known issues, incomplete features, next steps |
|
||||||
|
| `docs/INTEGRATION_PLAN.md` | 5-layer architecture, phase status |
|
||||||
|
| `docs/CI_CD.md` | Build and release workflows |
|
||||||
|
| `docs/KC_SPECIFICATION.md` | .kc file format specification |
|
||||||
|
| `docs/UPSTREAM.md` | FreeCAD upstream merge strategy |
|
||||||
|
| `docs/INTER_SOLVER.md` | Assembly solver integration |
|
||||||
|
| `docs/BOM_MERGE.md` | BOM-Assembly bridge specification |
|
||||||
|
|
||||||
|
The `docs/src/` directory contains an mdBook site with detailed guides organized by topic (architecture, development, guide, reference, silo-server, solver).
|
||||||
|
|
||||||
|
## Issue Tracker
|
||||||
|
|
||||||
|
Issues are tracked on Gitea at `git.kindred-systems.com/kindred/create/issues`. Use the `tea` CLI for local interaction:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tea issues # List open issues
|
||||||
|
tea issues 123 # View issue #123 details
|
||||||
|
tea pulls create # Create a PR
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Issues and Pitfalls
|
||||||
|
|
||||||
|
1. **Silo auth not production-hardened** — LDAP/OIDC backends are coded but need infrastructure deployment
|
||||||
|
2. **No unit tests** for ztools/Silo FreeCAD commands or Go backend
|
||||||
|
3. **Assembly solver datum handling is minimal** — joints referencing datum planes/points may produce incorrect placement
|
||||||
|
4. **PartDesign menu insertion fragility** — `_ZToolsPartDesignManipulator.modifyMenuBar()` inserts after `PartDesign_Boolean`; upstream renames break silently
|
||||||
|
5. **`Silo_BOM` requires Silo-tracked document** — unregistered documents show a warning with no registration path
|
||||||
|
6. **QSS edits** — only edit `src/Gui/Stylesheets/KindredCreate.qss`; the PreferencePacks copy is auto-generated
|
||||||
@@ -98,53 +98,107 @@ if hasattr(FreeCADGui, "ActiveDocument"):
|
|||||||
|
|
||||||
## Interactive drag protocol
|
## Interactive drag protocol
|
||||||
|
|
||||||
The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol:
|
The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol with a caching layer that avoids rebuilding the constraint system on every mouse move.
|
||||||
|
|
||||||
### pre_drag(ctx, drag_parts)
|
### pre_drag(ctx, drag_parts)
|
||||||
|
|
||||||
Called when the user begins dragging. Stores the context and dragged part IDs, then runs a full solve to establish the starting state.
|
Called when the user begins dragging. Builds the constraint system once, runs the substitution pre-pass, constructs the symbolic Jacobian, compiles the evaluator, performs an initial solve, and caches everything in a `_DragCache` for reuse across subsequent `drag_step()` calls.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def pre_drag(self, ctx, drag_parts):
|
def pre_drag(self, ctx, drag_parts):
|
||||||
self._drag_ctx = ctx
|
self._drag_ctx = ctx
|
||||||
self._drag_parts = set(drag_parts)
|
self._drag_parts = set(drag_parts)
|
||||||
return self.solve(ctx)
|
|
||||||
|
system = _build_system(ctx)
|
||||||
|
|
||||||
|
half_spaces = compute_half_spaces(...)
|
||||||
|
weight_vec = build_weight_vector(system.params)
|
||||||
|
|
||||||
|
residuals = substitution_pass(system.all_residuals, system.params)
|
||||||
|
# single_equation_pass is intentionally skipped — it bakes variable
|
||||||
|
# values as constants that become stale when dragged parts move.
|
||||||
|
|
||||||
|
jac_exprs = [[r.diff(name).simplify() for name in free] for r in residuals]
|
||||||
|
compiled_eval = try_compile_system(residuals, jac_exprs, ...)
|
||||||
|
|
||||||
|
# Initial solve (Newton-Raphson + BFGS fallback)
|
||||||
|
newton_solve(residuals, system.params, ...)
|
||||||
|
|
||||||
|
# Cache for drag_step() reuse
|
||||||
|
cache = _DragCache()
|
||||||
|
cache.system = system
|
||||||
|
cache.residuals = residuals
|
||||||
|
cache.jac_exprs = jac_exprs
|
||||||
|
cache.compiled_eval = compiled_eval
|
||||||
|
cache.half_spaces = half_spaces
|
||||||
|
cache.weight_vec = weight_vec
|
||||||
|
...
|
||||||
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important:** `single_equation_pass` is not used in the drag path. It analytically solves single-variable equations and bakes the results as `Const()` nodes into downstream expressions. During drag, those baked values become stale when part positions change, causing constraints to silently stop being enforced. Only `substitution_pass` (which replaces genuinely grounded parameters) is safe to cache.
|
||||||
|
|
||||||
### drag_step(drag_placements)
|
### drag_step(drag_placements)
|
||||||
|
|
||||||
Called on each mouse move. Updates the dragged parts' placements in the stored context, then re-solves. Since the parts moved only slightly from the previous position, Newton-Raphson converges in 1-2 iterations.
|
Called on each mouse move. Updates only the dragged part's 7 parameter values in the cached `ParamTable`, then re-solves using the cached residuals, Jacobian, and compiled evaluator. No system rebuild occurs.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def drag_step(self, drag_placements):
|
def drag_step(self, drag_placements):
|
||||||
ctx = self._drag_ctx
|
cache = self._drag_cache
|
||||||
|
params = cache.system.params
|
||||||
|
|
||||||
|
# Update only the dragged part's parameters
|
||||||
for pr in drag_placements:
|
for pr in drag_placements:
|
||||||
for part in ctx.parts:
|
pfx = pr.id + "/"
|
||||||
if part.id == pr.id:
|
params.set_value(pfx + "tx", pr.placement.position[0])
|
||||||
part.placement = pr.placement
|
params.set_value(pfx + "ty", pr.placement.position[1])
|
||||||
break
|
params.set_value(pfx + "tz", pr.placement.position[2])
|
||||||
return self.solve(ctx)
|
params.set_value(pfx + "qw", pr.placement.quaternion[0])
|
||||||
|
params.set_value(pfx + "qx", pr.placement.quaternion[1])
|
||||||
|
params.set_value(pfx + "qy", pr.placement.quaternion[2])
|
||||||
|
params.set_value(pfx + "qz", pr.placement.quaternion[3])
|
||||||
|
|
||||||
|
# Solve with cached artifacts — no rebuild
|
||||||
|
newton_solve(cache.residuals, params, ...,
|
||||||
|
jac_exprs=cache.jac_exprs,
|
||||||
|
compiled_eval=cache.compiled_eval)
|
||||||
|
|
||||||
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
### post_drag()
|
### post_drag()
|
||||||
|
|
||||||
Called when the drag ends. Clears the stored state.
|
Called when the drag ends. Clears the cached state.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def post_drag(self):
|
def post_drag(self):
|
||||||
self._drag_ctx = None
|
self._drag_ctx = None
|
||||||
self._drag_parts = None
|
self._drag_parts = None
|
||||||
|
self._drag_cache = None
|
||||||
```
|
```
|
||||||
|
|
||||||
### Performance notes
|
### _DragCache
|
||||||
|
|
||||||
The current implementation re-solves from scratch on each drag step, using the updated placements as the initial guess. This is correct and simple. For assemblies with fewer than ~50 parts, interactive frame rates are maintained because:
|
The cache holds all artifacts built in `pre_drag()` that are invariant across drag steps (constraint topology doesn't change during a drag):
|
||||||
|
|
||||||
|
| Field | Contents |
|
||||||
|
|-------|----------|
|
||||||
|
| `system` | `_System` -- owns `ParamTable` and `Expr` trees |
|
||||||
|
| `residuals` | `list[Expr]` -- after substitution pass |
|
||||||
|
| `jac_exprs` | `list[list[Expr]]` -- symbolic Jacobian |
|
||||||
|
| `compiled_eval` | `Callable` or `None` -- native compiled evaluator |
|
||||||
|
| `half_spaces` | `list[HalfSpace]` -- branch trackers |
|
||||||
|
| `weight_vec` | `ndarray` or `None` -- minimum-movement weights |
|
||||||
|
| `post_step_fn` | `Callable` or `None` -- half-space correction callback |
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
The caching layer eliminates the expensive per-frame overhead (~150 ms for system build + Jacobian construction + compilation). Each `drag_step()` only evaluates the cached expressions at updated parameter values:
|
||||||
|
|
||||||
- Newton-Raphson converges in 1-2 iterations from a nearby initial guess
|
- Newton-Raphson converges in 1-2 iterations from a nearby initial guess
|
||||||
- Pre-passes eliminate fixed parameters before the iterative loop
|
- The compiled evaluator (`codegen.py`) uses native Python `exec` for flat evaluation, avoiding the recursive tree-walk overhead
|
||||||
- The symbolic Jacobian is recomputed each step (no caching yet)
|
- The substitution pass compiles grounded-body parameters to constants, reducing the effective system size
|
||||||
|
- DOF counting is skipped during drag for speed (`result.dof = -1`)
|
||||||
For larger assemblies, cached incremental solving (reusing the decomposition and Jacobian structure across drag steps) is planned as a future optimization.
|
|
||||||
|
|
||||||
## Diagnostics integration
|
## Diagnostics integration
|
||||||
|
|
||||||
|
|||||||
Submodule mods/solver updated: 9d86bb203e...f85dc047e8
@@ -251,7 +251,6 @@ QDockWidget::title {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 8px 6px;
|
padding: 8px 6px;
|
||||||
border-bottom: 1px solid #313244;
|
border-bottom: 1px solid #313244;
|
||||||
min-height: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDockWidget::close-button,
|
QDockWidget::close-button,
|
||||||
@@ -733,7 +732,7 @@ QGroupBox {
|
|||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
border: 1px solid #45475a;
|
border: 1px solid #45475a;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-top: 12px;
|
margin-top: 16px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,7 +740,7 @@ QGroupBox::title {
|
|||||||
subcontrol-origin: margin;
|
subcontrol-origin: margin;
|
||||||
subcontrol-position: top left;
|
subcontrol-position: top left;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
padding: 0 4px;
|
padding: 2px 4px;
|
||||||
color: #bac2de;
|
color: #bac2de;
|
||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
}
|
}
|
||||||
@@ -1234,7 +1233,7 @@ QSint--ActionGroup QToolButton {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 6px;
|
padding: 8px 6px;
|
||||||
min-height: 18px;
|
min-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSint--ActionGroup QToolButton:hover {
|
QSint--ActionGroup QToolButton:hover {
|
||||||
|
|||||||
@@ -311,6 +311,19 @@ void AssemblyLink::updateContents()
|
|||||||
purgeTouched();
|
purgeTouched();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate an instance label for assembly components by appending a -N suffix.
|
||||||
|
// All instances get a suffix (starting at -1) so that structured part numbers
|
||||||
|
// like "P03-0001" are never mangled by UniqueNameManager's trailing-digit logic.
|
||||||
|
static std::string makeInstanceLabel(App::Document* doc, const std::string& baseLabel)
|
||||||
|
{
|
||||||
|
for (int i = 1;; ++i) {
|
||||||
|
std::string candidate = baseLabel + "-" + std::to_string(i);
|
||||||
|
if (!doc->containsLabel(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AssemblyLink::synchronizeComponents()
|
void AssemblyLink::synchronizeComponents()
|
||||||
{
|
{
|
||||||
App::Document* doc = getDocument();
|
App::Document* doc = getDocument();
|
||||||
@@ -428,7 +441,7 @@ void AssemblyLink::synchronizeComponents()
|
|||||||
auto* subAsmLink = static_cast<AssemblyLink*>(newObj);
|
auto* subAsmLink = static_cast<AssemblyLink*>(newObj);
|
||||||
subAsmLink->LinkedObject.setValue(obj);
|
subAsmLink->LinkedObject.setValue(obj);
|
||||||
subAsmLink->Rigid.setValue(asmLink->Rigid.getValue());
|
subAsmLink->Rigid.setValue(asmLink->Rigid.getValue());
|
||||||
subAsmLink->Label.setValue(obj->Label.getValue());
|
subAsmLink->Label.setValue(makeInstanceLabel(doc, obj->Label.getValue()));
|
||||||
addObject(subAsmLink);
|
addObject(subAsmLink);
|
||||||
link = subAsmLink;
|
link = subAsmLink;
|
||||||
}
|
}
|
||||||
@@ -440,7 +453,7 @@ void AssemblyLink::synchronizeComponents()
|
|||||||
);
|
);
|
||||||
newLink->LinkedObject.setValue(srcLink->getTrueLinkedObject(false));
|
newLink->LinkedObject.setValue(srcLink->getTrueLinkedObject(false));
|
||||||
|
|
||||||
newLink->Label.setValue(obj->Label.getValue());
|
newLink->Label.setValue(makeInstanceLabel(doc, obj->Label.getValue()));
|
||||||
addObject(newLink);
|
addObject(newLink);
|
||||||
|
|
||||||
newLink->ElementCount.setValue(srcLink->ElementCount.getValue());
|
newLink->ElementCount.setValue(srcLink->ElementCount.getValue());
|
||||||
@@ -461,7 +474,7 @@ void AssemblyLink::synchronizeComponents()
|
|||||||
App::DocumentObject* newObj = doc->addObject("App::Link", obj->getNameInDocument());
|
App::DocumentObject* newObj = doc->addObject("App::Link", obj->getNameInDocument());
|
||||||
auto* newLink = static_cast<App::Link*>(newObj);
|
auto* newLink = static_cast<App::Link*>(newObj);
|
||||||
newLink->LinkedObject.setValue(obj);
|
newLink->LinkedObject.setValue(obj);
|
||||||
newLink->Label.setValue(obj->Label.getValue());
|
newLink->Label.setValue(makeInstanceLabel(doc, obj->Label.getValue()));
|
||||||
addObject(newLink);
|
addObject(newLink);
|
||||||
link = newLink;
|
link = newLink;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,4 +122,15 @@ TEST(UniqueNameManager, UniqueNameWith9NDigits)
|
|||||||
manager.addExactName("Compound123456789");
|
manager.addExactName("Compound123456789");
|
||||||
EXPECT_EQ(manager.makeUniqueName("Compound", 3), "Compound123456790");
|
EXPECT_EQ(manager.makeUniqueName("Compound", 3), "Compound123456790");
|
||||||
}
|
}
|
||||||
|
TEST(UniqueNameManager, StructuredPartNumberDecomposition)
|
||||||
|
{
|
||||||
|
// Structured part numbers like P03-0001 have their trailing digits
|
||||||
|
// treated as the uniquifying suffix by UniqueNameManager. This is
|
||||||
|
// correct for default FreeCAD objects (Body -> Body001) but wrong
|
||||||
|
// for structured identifiers. Assembly module handles this separately
|
||||||
|
// via makeInstanceLabel which appends -N instance suffixes instead.
|
||||||
|
Base::UniqueNameManager manager;
|
||||||
|
manager.addExactName("P03-0001");
|
||||||
|
EXPECT_EQ(manager.makeUniqueName("P03-0001", 3), "P03-0002");
|
||||||
|
}
|
||||||
// NOLINTEND(cppcoreguidelines-*,readability-*)
|
// NOLINTEND(cppcoreguidelines-*,readability-*)
|
||||||
|
|||||||
Reference in New Issue
Block a user