feat(assembly): fixed reference planes + solver docs
Some checks failed
Build and Test / build (pull_request) Failing after 7m52s
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:
165
docs/src/solver/assembly-integration.md
Normal file
165
docs/src/solver/assembly-integration.md
Normal 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.
|
||||
Reference in New Issue
Block a user