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
6.3 KiB
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:
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:
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:
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:
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.
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.
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.
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.
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 returnFailed. 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()returnsFalse) -- the solver receives unbundled parts; the Assembly module pre-bundles when needed. - Native export (
export_native()) -- no solver-native debug format defined.