feat(assembly): fixed reference planes + solver docs
Some checks failed
Build and Test / build (pull_request) Failing after 7m52s

Assembly Origin Planes:
- AssemblyObject::setupObject() relabels origin planes to
  Top (XY), Front (XZ), Right (YZ) on assembly creation
- CommandCreateAssembly.py makes origin planes visible by default
- AssemblyUtils.cpp getObjFromRef() resolves LocalCoordinateSystem
  to child datum elements for joint references to origin planes
- TestAssemblyOriginPlanes.py: 9 integration tests covering
  structure, labels, grounding, reference resolution, solver,
  and save/load round-trip

Solver Documentation:
- docs/src/solver/: 7 new pages covering architecture overview,
  expression DAG, constraints, solving algorithms, diagnostics,
  assembly integration, and writing custom solvers
- docs/src/SUMMARY.md: added Kindred Solver section
This commit is contained in:
forbes
2026-02-21 09:09:16 -06:00
parent 311b3ea4f1
commit acc255972d
15 changed files with 1303 additions and 10 deletions

View File

@@ -0,0 +1,165 @@
# Assembly Integration
The Kindred solver integrates with FreeCAD's Assembly workbench through the KCSolve pluggable solver framework. This page describes the bridge layer, preference system, and interactive drag protocol.
## KindredSolver class
**Source:** `mods/solver/kindred_solver/solver.py`
`KindredSolver` subclasses `kcsolve.IKCSolver` and implements the solver interface:
```python
class KindredSolver(kcsolve.IKCSolver):
def name(self):
return "Kindred (Newton-Raphson)"
def supported_joints(self):
return list(_SUPPORTED) # 20 of 24 BaseJointKind values
def solve(self, ctx): # Static solve
def diagnose(self, ctx): # Constraint analysis
def pre_drag(self, ctx, drag_parts): # Begin drag session
def drag_step(self, drag_placements): # Mouse move during drag
def post_drag(self): # End drag session
def is_deterministic(self): # Returns True
```
### Registration
The solver is registered at addon load time via `Init.py`:
```python
import kcsolve
from kindred_solver import KindredSolver
kcsolve.register_solver("kindred", KindredSolver)
```
The `mods/solver/` directory is a FreeCAD addon discovered by the addon loader through its `package.xml` manifest.
### Supported joints
The Kindred solver handles 20 of the 24 `BaseJointKind` values. The remaining 4 are stubs that produce no residuals:
| Supported | Stub (no residuals) |
|-----------|-------------------|
| Coincident, PointOnLine, PointInPlane, Concentric, Tangent, Planar, LineInPlane, Parallel, Perpendicular, Angle, Fixed, Revolute, Cylindrical, Slider, Ball, Screw, Universal, Gear, RackPinion, DistancePointPoint | Cam, Slot, DistanceCylSph, Custom |
### Joint limits
Joint travel limits (`Constraint.limits`) are accepted but not enforced. The solver logs a warning once per instance when limits are encountered. Enforcing inequality constraints requires active-set or barrier-method extensions beyond the current Newton-Raphson formulation.
## Solver selection
### C++ preference
`AssemblyObject::getOrCreateSolver()` reads the user preference to select the solver backend:
```cpp
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Assembly");
std::string solverName = hGrp->GetASCII("Solver", "");
solver_ = KCSolve::SolverRegistry::instance().get(solverName);
```
An empty string (`""`) returns the registry default (the first solver registered, which is OndselSolver). Setting `"kindred"` selects the Kindred solver.
`resetSolver()` clears the cached solver instance so the next solve picks up preference changes.
### Preferences UI
The Assembly preferences page (`Edit > Preferences > Assembly`) includes a "Solver backend" combo box populated from the registry at load time:
- **Default** -- empty string, uses the registry default (OndselSolver)
- **OndselSolver (Lagrangian)** -- `"ondsel"`
- **Kindred (Newton-Raphson)** -- `"kindred"` (available when the solver addon is loaded)
The preference is stored as `Mod/Assembly/Solver` in the FreeCAD parameter system.
### Programmatic switching
From the Python console:
```python
import FreeCAD
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Assembly")
# Switch to Kindred
pref.SetString("Solver", "kindred")
# Switch back to default
pref.SetString("Solver", "")
# Force the active assembly to pick up the change
if hasattr(FreeCADGui, "ActiveDocument"):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "resetSolver"):
obj.resetSolver()
```
## Interactive drag protocol
The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol:
### 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.
```python
def pre_drag(self, ctx, drag_parts):
self._drag_ctx = ctx
self._drag_parts = set(drag_parts)
return self.solve(ctx)
```
### 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.
```python
def drag_step(self, drag_placements):
ctx = self._drag_ctx
for pr in drag_placements:
for part in ctx.parts:
if part.id == pr.id:
part.placement = pr.placement
break
return self.solve(ctx)
```
### post_drag()
Called when the drag ends. Clears the stored state.
```python
def post_drag(self):
self._drag_ctx = None
self._drag_parts = None
```
### Performance notes
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:
- Newton-Raphson converges in 1-2 iterations from a nearby initial guess
- Pre-passes eliminate fixed parameters before the iterative loop
- The symbolic Jacobian is recomputed each step (no caching yet)
For larger assemblies, cached incremental solving (reusing the decomposition and Jacobian structure across drag steps) is planned as a future optimization.
## Diagnostics integration
`diagnose(ctx)` builds the constraint system and runs overconstrained detection, returning a list of `kcsolve.ConstraintDiagnostic` objects. The Assembly module calls this to populate the constraint diagnostics panel.
```python
def diagnose(self, ctx):
system = _build_system(ctx)
residuals = substitution_pass(system.all_residuals, system.params)
return _run_diagnostics(residuals, system.params, system.residual_ranges, ctx)
```
## Not yet implemented
- **Kinematic simulation** (`run_kinematic`, `num_frames`, `update_for_frame`) -- the base class defaults return `Failed`. Requires time-stepping integration with motion driver expression evaluation.
- **Joint limit enforcement** -- inequality constraints need active-set or barrier solver extensions.
- **Fixed-joint bundling** (`supports_bundle_fixed()` returns `False`) -- the solver receives unbundled parts; the Assembly module pre-bundles when needed.
- **Native export** (`export_native()`) -- no solver-native debug format defined.