feat(solver): define IKCSolver C++ API types and interface (#292)

Add the pluggable solver API as header-only files under
src/Mod/Assembly/Solver/. This is Phase 1a of the pluggable solver
system (INTER_SOLVER.md).

New files:
- Types.h: BaseJointKind enum (24 decomposed constraint types),
  Transform, Part, Constraint, SolveContext, SolveResult, and
  supporting types. Uses standalone types (no FreeCAD dependency)
  for future server worker compatibility.
- IKCSolver.h: Abstract solver interface with solve(), drag protocol
  (pre_drag/drag_step/post_drag), kinematic simulation
  (run_kinematic/num_frames/update_for_frame), and diagnostics.
  Only solve(), name(), and supported_joints() are pure virtual;
  all other methods have default implementations.
- SolverRegistry.h: Thread-safe singleton registry for solver
  backends with factory-based registration and default solver
  selection.
- CMakeLists.txt: INTERFACE library target (header-only for now).

Modified:
- Assembly/CMakeLists.txt: add_subdirectory(Solver)
- Assembly/App/CMakeLists.txt: link KCSolve INTERFACE target
This commit is contained in:
forbes
2026-02-19 15:55:57 -06:00
parent ab6d09c138
commit 47e6c14461
6 changed files with 645 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ set(Assembly_LIBS
Spreadsheet
FreeCADApp
OndselSolver
KCSolve
)
generate_from_py(AssemblyObject)

View File

@@ -11,6 +11,7 @@ else ()
endif ()
endif ()
add_subdirectory(Solver)
add_subdirectory(App)
if(BUILD_GUI)

View File

@@ -0,0 +1,12 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# Phase 1a: header-only INTERFACE library.
# Phase 1b will convert to SHARED when .cpp files are added.
add_library(KCSolve INTERFACE)
target_include_directories(KCSolve
INTERFACE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)

View File

@@ -0,0 +1,181 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSOLVE_IKCSOLVER_H
#define KCSOLVE_IKCSOLVER_H
#include <cstddef>
#include <string>
#include <vector>
#include "Types.h"
namespace KCSolve
{
/// Abstract interface for a pluggable assembly constraint solver.
///
/// Solver backends implement this interface. The Assembly module calls
/// through it via the SolverRegistry. A minimal solver only needs to
/// implement solve(), name(), and supported_joints() — all other methods
/// have default implementations that either delegate to solve() or
/// return sensible defaults.
///
/// Method mapping to current AssemblyObject operations:
///
/// solve() <-> AssemblyObject::solve()
/// pre_drag() <-> AssemblyObject::preDrag()
/// drag_step() <-> AssemblyObject::doDragStep()
/// post_drag() <-> AssemblyObject::postDrag()
/// run_kinematic() <-> AssemblyObject::generateSimulation()
/// num_frames() <-> AssemblyObject::numberOfFrames()
/// update_for_frame() <-> AssemblyObject::updateForFrame()
/// diagnose() <-> AssemblyObject::updateSolveStatus()
class IKCSolver
{
public:
virtual ~IKCSolver() = default;
/// Human-readable solver name (e.g. "OndselSolver (Lagrangian)").
virtual std::string name() const = 0;
/// Return the set of BaseJointKind values this solver supports.
/// The registry uses this for capability-based solver selection.
virtual std::vector<BaseJointKind> supported_joints() const = 0;
// ── Static solve ───────────────────────────────────────────────
/// Solve the assembly for static equilibrium.
/// @param ctx Complete description of parts, constraints, and options.
/// @return Result with updated placements and diagnostics.
virtual SolveResult solve(const SolveContext& ctx) = 0;
/// Incrementally update an already-solved assembly after parameter
/// changes (e.g. joint angle/distance changed during joint creation).
/// Default: delegates to solve().
virtual SolveResult update(const SolveContext& ctx)
{
return solve(ctx);
}
// ── Interactive drag ───────────────────────────────────────────
//
// Three-phase protocol for interactive part dragging:
// 1. pre_drag() — solve initial state, prepare for dragging
// 2. drag_step() — called on each mouse move with updated positions
// 3. post_drag() — finalize and release internal solver state
//
// Solvers can maintain internal state across the drag session for
// better interactive performance. This addresses a known weakness
// in the current direct-OndselSolver integration.
/// Prepare for an interactive drag session.
/// @param ctx Assembly state before dragging begins.
/// @param drag_parts IDs of parts being dragged.
/// @return Initial solve result.
virtual SolveResult pre_drag(const SolveContext& ctx,
const std::vector<std::string>& /*drag_parts*/)
{
return solve(ctx);
}
/// Perform one incremental drag step.
/// @param drag_placements Current placements of the dragged parts
/// (part ID + new transform).
/// @return Updated placements for all affected parts.
virtual SolveResult drag_step(
const std::vector<SolveResult::PartResult>& /*drag_placements*/)
{
return SolveResult {SolveStatus::Success};
}
/// End an interactive drag session and finalize state.
virtual void post_drag()
{
}
// ── Kinematic simulation ───────────────────────────────────────
/// Run a kinematic simulation over the time range in ctx.simulation.
/// After this call, num_frames() returns the frame count and
/// update_for_frame(i) retrieves individual frame placements.
/// Default: delegates to solve() (ignoring simulation params).
virtual SolveResult run_kinematic(const SolveContext& /*ctx*/)
{
return SolveResult {SolveStatus::Failed};
}
/// Number of simulation frames available after run_kinematic().
virtual std::size_t num_frames() const
{
return 0;
}
/// Retrieve part placements for simulation frame at index.
/// @pre index < num_frames()
virtual SolveResult update_for_frame(std::size_t /*index*/)
{
return SolveResult {SolveStatus::Failed};
}
// ── Diagnostics ────────────────────────────────────────────────
/// Analyze the assembly for redundant, conflicting, or malformed
/// constraints. May require a prior solve() call for some solvers.
virtual std::vector<ConstraintDiagnostic> diagnose(const SolveContext& /*ctx*/)
{
return {};
}
// ── Capability queries ─────────────────────────────────────────
/// Whether this solver produces deterministic results given
/// identical input.
virtual bool is_deterministic() const
{
return true;
}
/// Whether this solver handles fixed-joint part bundling internally.
/// When false, the caller bundles parts connected by Fixed joints
/// before building the SolveContext. When true, the solver receives
/// unbundled parts and optimizes internally.
virtual bool supports_bundle_fixed() const
{
return false;
}
protected:
IKCSolver() = default;
// Non-copyable, non-movable (polymorphic base class)
IKCSolver(const IKCSolver&) = delete;
IKCSolver& operator=(const IKCSolver&) = delete;
IKCSolver(IKCSolver&&) = delete;
IKCSolver& operator=(IKCSolver&&) = delete;
};
} // namespace KCSolve
#endif // KCSOLVE_IKCSOLVER_H

View File

@@ -0,0 +1,164 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSOLVE_SOLVERREGISTRY_H
#define KCSOLVE_SOLVERREGISTRY_H
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "IKCSolver.h"
namespace KCSolve
{
/// Factory function that creates a solver instance.
using CreateSolverFn = std::function<std::unique_ptr<IKCSolver>()>;
/// Singleton registry for pluggable solver backends.
///
/// Solver plugins register themselves at module load time via
/// register_solver(). The Assembly module retrieves solvers via get().
///
/// Thread safety: all public methods are internally synchronized.
///
/// Usage:
/// // Registration (at module init):
/// KCSolve::SolverRegistry::instance().register_solver(
/// "ondsel", []() { return std::make_unique<OndselAdapter>(); });
///
/// // Retrieval:
/// auto solver = KCSolve::SolverRegistry::instance().get(); // default
/// auto solver = KCSolve::SolverRegistry::instance().get("ondsel");
class SolverRegistry
{
public:
/// Access the singleton instance.
static SolverRegistry& instance()
{
static SolverRegistry reg;
return reg;
}
/// Register a solver backend.
/// @param name Unique solver name (e.g. "ondsel").
/// @param factory Factory function that creates solver instances.
/// @return true if registration succeeded, false if name taken.
bool register_solver(const std::string& name, CreateSolverFn factory)
{
std::lock_guard<std::mutex> lock(mutex_);
auto [it, inserted] = factories_.emplace(name, std::move(factory));
if (inserted && default_name_.empty()) {
default_name_ = name; // first registered becomes default
}
return inserted;
}
/// Create an instance of the named solver.
/// @param name Solver name. If empty, uses the default solver.
/// @return Solver instance, or nullptr if not found.
std::unique_ptr<IKCSolver> get(const std::string& name = {}) const
{
std::lock_guard<std::mutex> lock(mutex_);
const std::string& key = name.empty() ? default_name_ : name;
if (key.empty()) {
return nullptr;
}
auto it = factories_.find(key);
if (it == factories_.end()) {
return nullptr;
}
return it->second();
}
/// Return the names of all registered solvers.
std::vector<std::string> available() const
{
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::string> names;
names.reserve(factories_.size());
for (const auto& [name, _] : factories_) {
names.push_back(name);
}
return names;
}
/// Query which BaseJointKind values a named solver supports.
/// Creates a temporary instance to call supported_joints().
std::vector<BaseJointKind> joints_for(const std::string& name) const
{
auto solver = get(name);
if (!solver) {
return {};
}
return solver->supported_joints();
}
/// Set the default solver name.
/// @return true if the name is registered, false otherwise.
bool set_default(const std::string& name)
{
std::lock_guard<std::mutex> lock(mutex_);
if (factories_.find(name) == factories_.end()) {
return false;
}
default_name_ = name;
return true;
}
/// Get the default solver name.
std::string get_default() const
{
std::lock_guard<std::mutex> lock(mutex_);
return default_name_;
}
/// Scan a directory for solver plugins (Phase 1b).
/// Currently a no-op placeholder. Will dlopen/LoadLibrary shared
/// objects that export kcsolve_create() / kcsolve_api_version().
void scan(const std::string& /*directory*/)
{
}
private:
SolverRegistry() = default;
~SolverRegistry() = default;
SolverRegistry(const SolverRegistry&) = delete;
SolverRegistry& operator=(const SolverRegistry&) = delete;
SolverRegistry(SolverRegistry&&) = delete;
SolverRegistry& operator=(SolverRegistry&&) = delete;
mutable std::mutex mutex_;
std::unordered_map<std::string, CreateSolverFn> factories_;
std::string default_name_;
};
} // namespace KCSolve
#endif // KCSOLVE_SOLVERREGISTRY_H

View File

@@ -0,0 +1,286 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
* *
* This file is part of FreeCAD. *
* *
* FreeCAD is free software: you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation, either version 2.1 of the *
* License, or (at your option) any later version. *
* *
* FreeCAD is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with FreeCAD. If not, see *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef KCSOLVE_TYPES_H
#define KCSOLVE_TYPES_H
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace KCSolve
{
// ── Transform ──────────────────────────────────────────────────────
//
// Rigid-body transform: position (x, y, z) + unit quaternion (w, x, y, z).
// Semantically equivalent to Base::Placement but free of FreeCAD dependencies
// so that KCSolve headers remain standalone (for future server worker use).
//
// Quaternion convention: (w, x, y, z) — mathematical standard.
// Note: Base::Rotation(q0,q1,q2,q3) uses (x, y, z, w) ordering.
// The adapter layer handles this swap.
struct Transform
{
std::array<double, 3> position {0.0, 0.0, 0.0};
std::array<double, 4> quaternion {1.0, 0.0, 0.0, 0.0}; // w, x, y, z
static Transform identity()
{
return {};
}
};
// ── BaseJointKind ──────────────────────────────────────────────────
//
// Decomposed primitive constraint types. Uses SOLIDWORKS-inspired vocabulary
// from the INTER_SOLVER.md spec rather than OndselSolver internal names.
//
// The existing Assembly::JointType (13 values) and Assembly::DistanceType
// (35+ values) map to these via the adapter layer. In particular, the
// "Distance" JointType is decomposed based on geometry classification
// (see makeMbdJointDistance in AssemblyObject.cpp).
enum class BaseJointKind : std::uint8_t
{
// Point constraints (decomposed from JointType::Distance)
Coincident, // PointOnPoint, d=0 — 3 DOF removed
PointOnLine, // Point constrained to a line — 2 DOF removed
PointInPlane, // Point constrained to a plane — 1 DOF removed
// Axis/surface constraints (decomposed from JointType::Distance)
Concentric, // Coaxial (line-line, circle-circle, cyl-cyl) — 4 DOF removed
Tangent, // Face-on-face tangency — 1 DOF removed
Planar, // Coplanar faces — 3 DOF removed
LineInPlane, // Line constrained to a plane — 2 DOF removed
// Axis orientation constraints (direct from JointType)
Parallel, // Parallel axes — 2 DOF removed
Perpendicular, // 90-degree axes — 1 DOF removed
Angle, // Arbitrary axis angle — 1 DOF removed
// Standard kinematic joints (direct 1:1 from JointType)
Fixed, // Rigid weld — 6 DOF removed
Revolute, // Hinge — 5 DOF removed
Cylindrical, // Rotation + sliding on axis — 4 DOF removed
Slider, // 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 (future)
// Mechanical element constraints
Gear, // Gear pair or belt (sign determines direction)
RackPinion, // Rack-and-pinion
Cam, // Cam-follower (future)
Slot, // Slot constraint (future)
// Distance variants with non-zero offset
DistancePointPoint, // Point-to-point with offset — 2 DOF removed
DistanceCylSph, // Cylinder-sphere distance — varies
Custom, // Solver-specific extension point
};
// ── Part ───────────────────────────────────────────────────────────
struct Part
{
std::string id;
Transform placement;
double mass {1.0};
bool grounded {false};
};
// ── Constraint ─────────────────────────────────────────────────────
//
// A constraint between two parts. Built from a FreeCAD JointObject by
// the adapter layer (classifying geometry into the specific BaseJointKind).
struct Constraint
{
std::string id; // FreeCAD document object name (e.g. "Joint001")
std::string part_i; // solver-side part ID for first reference
Transform marker_i; // coordinate system on part_i
std::string part_j; // solver-side part ID for second reference
Transform marker_j; // coordinate system on part_j
BaseJointKind type {};
// Scalar parameters (interpretation depends on type):
// Angle: params[0] = angle in radians
// RackPinion: params[0] = pitch radius
// Screw: params[0] = pitch
// Gear: params[0] = radiusI, params[1] = radiusJ (negative for belt)
// DistancePointPoint: params[0] = distance
// DistanceCylSph: params[0] = distance
// Planar: params[0] = offset
// Concentric: params[0] = distance
// PointInPlane: params[0] = offset
// LineInPlane: params[0] = offset
std::vector<double> params;
// Joint limits (length or angle bounds)
struct Limit
{
enum class Kind : std::uint8_t
{
TranslationMin,
TranslationMax,
RotationMin,
RotationMax,
};
Kind kind {};
double value {0.0};
double tolerance {1.0e-9};
};
std::vector<Limit> limits;
bool activated {true};
};
// ── MotionDef ──────────────────────────────────────────────────────
//
// A motion driver for kinematic simulation.
struct MotionDef
{
enum class Kind : std::uint8_t
{
Rotational,
Translational,
General,
};
Kind kind {};
std::string joint_id; // which constraint this drives
std::string marker_i;
std::string marker_j;
// Motion law expressions (function of time 't').
// For General: both are set. Otherwise only the relevant one.
std::string rotation_expr;
std::string translation_expr;
};
// ── SimulationParams ───────────────────────────────────────────────
//
// Parameters for kinematic simulation (run_kinematic).
// Maps to create_mbdSimulationParameters() in AssemblyObject.cpp.
struct SimulationParams
{
double t_start {0.0};
double t_end {1.0};
double h_out {0.01}; // output time step
double h_min {1.0e-9};
double h_max {1.0};
double error_tol {1.0e-6};
};
// ── SolveContext ───────────────────────────────────────────────────
//
// Complete input to a solve operation. Built by the adapter layer
// from FreeCAD document objects.
struct SolveContext
{
std::vector<Part> parts;
std::vector<Constraint> constraints;
std::vector<MotionDef> motions;
// Present when running kinematic simulation via run_kinematic().
std::optional<SimulationParams> simulation;
// Hint: bundle parts connected by Fixed joints into single rigid bodies.
// When true and the solver does not support_bundle_fixed(), the adapter
// layer pre-bundles before passing to the solver.
bool bundle_fixed {false};
};
// ── SolveStatus ────────────────────────────────────────────────────
//
// Matches the return codes from AssemblyObject::solve().
enum class SolveStatus : std::int8_t
{
Success = 0,
Failed = -1,
InvalidFlip = -2, // orientation flipped past threshold
NoGroundedParts = -6, // no grounded parts in assembly
};
// ── ConstraintDiagnostic ───────────────────────────────────────────
//
// Per-constraint diagnostic information from updateSolveStatus().
struct ConstraintDiagnostic
{
enum class Kind : std::uint8_t
{
Redundant,
Conflicting,
PartiallyRedundant,
Malformed,
};
std::string constraint_id; // FreeCAD object name
Kind kind {};
std::string detail; // human-readable description
};
// ── SolveResult ────────────────────────────────────────────────────
//
// Output of a solve operation.
struct SolveResult
{
SolveStatus status {SolveStatus::Success};
// Updated placements for each part (only parts that moved).
struct PartResult
{
std::string id;
Transform placement;
};
std::vector<PartResult> placements;
// Degrees of freedom remaining (-1 = unknown).
int dof {-1};
// Constraint diagnostics (redundant, conflicting, etc.).
std::vector<ConstraintDiagnostic> diagnostics;
// For kinematic simulation: number of computed frames.
std::size_t num_frames {0};
};
} // namespace KCSolve
#endif // KCSOLVE_TYPES_H