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