Files
create/docs/src/solver/assembly-integration.md
forbes acc255972d
Some checks failed
Build and Test / build (pull_request) Failing after 7m52s
feat(assembly): fixed reference planes + solver docs
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
2026-02-21 09:09:16 -06:00

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 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.