feat(solver): Phase 1a — IKCSolver C++ API types and interface #292

Closed
opened 2026-02-19 21:26:00 +00:00 by forbes · 0 comments
Owner

Summary

Define the pluggable solver C++ API as header files. No integration changes — this is the contract that everything else builds against.

Parent: #287 (Phase 1)
Blocks: #293 (1b), #294 (1c), #295 (1d)

Scope

Directory structure

src/Mod/Assembly/Solver/
├── include/KCSolve/
│   ├── Types.h
│   ├── IKCSolver.h
│   └── SolverRegistry.h
└── CMakeLists.txt          # header-only target for now

Types.h — Core data types

BaseJointKind enum (22 decomposed types):

enum class BaseJointKind {
    // Point constraints
    Coincident,        // PointOnPoint — 3 DOF removed
    PointOnLine,       // — 2 DOF removed
    PointInPlane,      // — 1 DOF removed

    // Axis constraints
    Concentric,        // CylindricalOnCylindrical — 4 DOF removed
    Parallel,          // Parallel axes — 2 DOF removed (orientation only)
    Perpendicular,     // 90° axis constraint — 1 DOF removed
    Angle,             // Arbitrary axis angle — 1 DOF removed

    // Surface constraints
    Tangent,           // Face-on-face tangency — 1 DOF removed
    Planar,            // Coplanar faces — 3 DOF removed
    SymmetricPlane,    // Mirror symmetry — 1 DOF removed

    // Standard joints (full kinematic pairs)
    Fixed,             // Lock / rigid weld — 6 DOF removed
    Revolute,          // Hinge — 5 DOF removed
    Cylindrical,       // Rotation + sliding on axis — 4 DOF removed
    Slider,            // Prismatic / linear translation — 5 DOF removed
    Ball,              // Spherical — 3 DOF removed
    Screw,             // Helical (rotation + coupled translation) — 5 DOF removed
    Universal,         // U-joint / Cardan — 4 DOF removed

    // Mechanical element constraints
    Gear,              // Gear pair (same direction) — 1 DOF removed
    Rack,              // Rack-and-pinion — 1 DOF removed
    Belt,              // Belt/pulley (opposite direction) — 1 DOF removed
    Cam,               // Cam-follower — solver-specific
    Slot,              // Slot constraint — solver-specific

    Custom             // Solver-specific extension point
};

These are decomposed from FreeCAD's current 13 JointType values. In particular, the current Distance type smart-dispatches to 35+ geometry subtypes — those map to the decomposed types above (Coincident, PointOnLine, PointInPlane, Planar, Concentric, etc.). Each solver sees the specific constraint type rather than being forced to implement geometry dispatch.

Structs:

  • JointTypeId{solver_id, joint_name}
  • JointDef — solver-registered joint descriptor (base_kind, display_name, dof_removed, params, supports_limits, supports_friction)
  • Constraint — instance with part refs, geometry refs, param values, suppressed flag
  • SolveContext — full problem definition:
    • constraints, placements (4x4 transforms), grounded set
    • tolerance, max_iterations, deterministic flag
    • warm_start placements (optional)
    • bundle_fixed hint (default true — solver may override)
  • SolveResult — output:
    • status enum, iterations, final_residual, solve_time_ms
    • placements map, diagnostics per-constraint
    • input_hash for semi-determinism
  • SolveStatus enum — Converged, MaxIterationsReached, Overconstrained, Underconstrained, Redundant, Failed
  • ConstraintDiagnostic — per-constraint residual, satisfied flag, message

IKCSolver.h — Solver interface

class IKCSolver {
public:
    virtual ~IKCSolver() = default;

    // Identity
    virtual std::string id() const = 0;
    virtual std::string name() const = 0;
    virtual std::string version() const = 0;

    // Joint type registry
    virtual std::vector<JointDef> supported_joints() const = 0;

    // One-shot solve
    virtual SolveResult solve(const SolveContext& ctx) = 0;

    // Incremental update (default: full re-solve)
    virtual SolveResult update(const SolveContext& ctx,
                               const std::string& changed_constraint) {
        return solve(ctx);
    }

    // Interactive drag protocol
    virtual void pre_drag(const SolveContext& ctx) { /* default no-op */ }
    virtual SolveResult drag_step(const SolveContext& ctx,
                                   const std::vector<std::string>& dragged_parts) {
        return solve(ctx);
    }
    virtual void post_drag() { /* default no-op */ }

    // Kinematic simulation
    virtual SolveResult run_kinematic(const SolveContext& ctx) {
        return solve(ctx);
    }
    virtual size_t num_frames() const { return 0; }
    virtual SolveResult update_for_frame(size_t index) {
        return SolveResult{SolveStatus::Failed};
    }

    // Diagnostics
    virtual SolveStatus diagnose(const SolveContext& ctx) {
        return SolveStatus::Converged;
    }
    virtual bool is_deterministic() const { return false; }

    // Performance hints
    virtual bool supports_bundle_fixed() const { return false; }
};

Key design decisions:

  • Drag methods are on the interface — solvers can maintain internal state across drag steps for better interactive performance (addressing a known OndselSolver weakness)
  • Kinematic simulation is on the interfacerun_kinematic() + num_frames() + update_for_frame() allow solvers to own the simulation lifecycle
  • supports_bundle_fixed() — solver declares whether it handles fixed-joint bundling internally. If false, AssemblyObject does the bundling before building SolveContext. If true, the solver receives unbundled parts and optimizes itself
  • All drag/simulation methods have default implementations that fall back to solve(), so a minimal solver only needs to implement solve() + supported_joints()

SolverRegistry.h — Plugin discovery and lookup

class SolverRegistry {
public:
    void scan(const std::filesystem::path& plugin_dir);
    void register_solver(std::unique_ptr<IKCSolver> solver);
    IKCSolver* get(const std::string& solver_id) const;
    std::vector<std::string> available() const;
    std::vector<JointTypeId> joints_for(BaseJointKind kind) const;
    void set_default(const std::string& solver_id);
    IKCSolver* get_default() const;
};

// Plugin entry points
using CreateSolverFn = IKCSolver* (*)();

CMakeLists.txt

Header-only INTERFACE target so other modules can target_link_libraries(Assembly KCSolve) to get the include paths. Actual implementation comes in 1b.

Acceptance criteria

  • Headers compile cleanly with C++20
  • BaseJointKind covers all 22 decomposed types from the spec + geometry analysis
  • IKCSolver interface has solve, drag, simulation, and diagnostic methods
  • Default implementations allow a minimal solver to only implement solve()
  • SolveContext includes bundle_fixed hint for solver-controlled optimization
  • CMake INTERFACE target builds and can be linked by Assembly module

References

  • docs/INTER_SOLVER.md §4 (Layer 1: C++ Solver API)
  • src/Mod/Assembly/App/AssemblyUtils.h — current 13 JointType + 35 DistanceType
  • src/Mod/Assembly/App/AssemblyObject.cpp — current 6 solver call patterns
## Summary Define the pluggable solver C++ API as header files. No integration changes — this is the contract that everything else builds against. **Parent:** #287 (Phase 1) **Blocks:** #293 (1b), #294 (1c), #295 (1d) ## Scope ### Directory structure ``` src/Mod/Assembly/Solver/ ├── include/KCSolve/ │ ├── Types.h │ ├── IKCSolver.h │ └── SolverRegistry.h └── CMakeLists.txt # header-only target for now ``` ### Types.h — Core data types **BaseJointKind enum (22 decomposed types):** ```cpp enum class BaseJointKind { // Point constraints Coincident, // PointOnPoint — 3 DOF removed PointOnLine, // — 2 DOF removed PointInPlane, // — 1 DOF removed // Axis constraints Concentric, // CylindricalOnCylindrical — 4 DOF removed Parallel, // Parallel axes — 2 DOF removed (orientation only) Perpendicular, // 90° axis constraint — 1 DOF removed Angle, // Arbitrary axis angle — 1 DOF removed // Surface constraints Tangent, // Face-on-face tangency — 1 DOF removed Planar, // Coplanar faces — 3 DOF removed SymmetricPlane, // Mirror symmetry — 1 DOF removed // Standard joints (full kinematic pairs) Fixed, // Lock / rigid weld — 6 DOF removed Revolute, // Hinge — 5 DOF removed Cylindrical, // Rotation + sliding on axis — 4 DOF removed Slider, // Prismatic / linear translation — 5 DOF removed Ball, // Spherical — 3 DOF removed Screw, // Helical (rotation + coupled translation) — 5 DOF removed Universal, // U-joint / Cardan — 4 DOF removed // Mechanical element constraints Gear, // Gear pair (same direction) — 1 DOF removed Rack, // Rack-and-pinion — 1 DOF removed Belt, // Belt/pulley (opposite direction) — 1 DOF removed Cam, // Cam-follower — solver-specific Slot, // Slot constraint — solver-specific Custom // Solver-specific extension point }; ``` These are decomposed from FreeCAD's current 13 `JointType` values. In particular, the current `Distance` type smart-dispatches to 35+ geometry subtypes — those map to the decomposed types above (Coincident, PointOnLine, PointInPlane, Planar, Concentric, etc.). Each solver sees the specific constraint type rather than being forced to implement geometry dispatch. **Structs:** - `JointTypeId` — `{solver_id, joint_name}` - `JointDef` — solver-registered joint descriptor (base_kind, display_name, dof_removed, params, supports_limits, supports_friction) - `Constraint` — instance with part refs, geometry refs, param values, suppressed flag - `SolveContext` — full problem definition: - constraints, placements (4x4 transforms), grounded set - tolerance, max_iterations, deterministic flag - warm_start placements (optional) - `bundle_fixed` hint (default true — solver may override) - `SolveResult` — output: - status enum, iterations, final_residual, solve_time_ms - placements map, diagnostics per-constraint - input_hash for semi-determinism - `SolveStatus` enum — Converged, MaxIterationsReached, Overconstrained, Underconstrained, Redundant, Failed - `ConstraintDiagnostic` — per-constraint residual, satisfied flag, message ### IKCSolver.h — Solver interface ```cpp class IKCSolver { public: virtual ~IKCSolver() = default; // Identity virtual std::string id() const = 0; virtual std::string name() const = 0; virtual std::string version() const = 0; // Joint type registry virtual std::vector<JointDef> supported_joints() const = 0; // One-shot solve virtual SolveResult solve(const SolveContext& ctx) = 0; // Incremental update (default: full re-solve) virtual SolveResult update(const SolveContext& ctx, const std::string& changed_constraint) { return solve(ctx); } // Interactive drag protocol virtual void pre_drag(const SolveContext& ctx) { /* default no-op */ } virtual SolveResult drag_step(const SolveContext& ctx, const std::vector<std::string>& dragged_parts) { return solve(ctx); } virtual void post_drag() { /* default no-op */ } // Kinematic simulation virtual SolveResult run_kinematic(const SolveContext& ctx) { return solve(ctx); } virtual size_t num_frames() const { return 0; } virtual SolveResult update_for_frame(size_t index) { return SolveResult{SolveStatus::Failed}; } // Diagnostics virtual SolveStatus diagnose(const SolveContext& ctx) { return SolveStatus::Converged; } virtual bool is_deterministic() const { return false; } // Performance hints virtual bool supports_bundle_fixed() const { return false; } }; ``` Key design decisions: - **Drag methods are on the interface** — solvers can maintain internal state across drag steps for better interactive performance (addressing a known OndselSolver weakness) - **Kinematic simulation is on the interface** — `run_kinematic()` + `num_frames()` + `update_for_frame()` allow solvers to own the simulation lifecycle - **`supports_bundle_fixed()`** — solver declares whether it handles fixed-joint bundling internally. If false, AssemblyObject does the bundling before building SolveContext. If true, the solver receives unbundled parts and optimizes itself - **All drag/simulation methods have default implementations** that fall back to `solve()`, so a minimal solver only needs to implement `solve()` + `supported_joints()` ### SolverRegistry.h — Plugin discovery and lookup ```cpp class SolverRegistry { public: void scan(const std::filesystem::path& plugin_dir); void register_solver(std::unique_ptr<IKCSolver> solver); IKCSolver* get(const std::string& solver_id) const; std::vector<std::string> available() const; std::vector<JointTypeId> joints_for(BaseJointKind kind) const; void set_default(const std::string& solver_id); IKCSolver* get_default() const; }; // Plugin entry points using CreateSolverFn = IKCSolver* (*)(); ``` ### CMakeLists.txt Header-only INTERFACE target so other modules can `target_link_libraries(Assembly KCSolve)` to get the include paths. Actual implementation comes in 1b. ## Acceptance criteria - [ ] Headers compile cleanly with C++20 - [ ] `BaseJointKind` covers all 22 decomposed types from the spec + geometry analysis - [ ] `IKCSolver` interface has solve, drag, simulation, and diagnostic methods - [ ] Default implementations allow a minimal solver to only implement `solve()` - [ ] `SolveContext` includes `bundle_fixed` hint for solver-controlled optimization - [ ] CMake INTERFACE target builds and can be linked by Assembly module ## References - `docs/INTER_SOLVER.md` §4 (Layer 1: C++ Solver API) - `src/Mod/Assembly/App/AssemblyUtils.h` — current 13 JointType + 35 DistanceType - `src/Mod/Assembly/App/AssemblyObject.cpp` — current 6 solver call patterns
forbes added the enhancement label 2026-02-19 21:26:00 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#292