Files
create/src/Mod/Assembly/Solver/IKCSolver.h
forbes 7ea0078ba3
All checks were successful
Build and Test / build (pull_request) Successful in 29m19s
feat(kcsolve): pybind11 bindings and Python solver support
Add the kcsolve pybind11 module exposing the KCSolve C++ API to Python:

- PyIKCSolver trampoline enabling Python IKCSolver subclasses
- Bindings for all 5 enums, 10 structs, IKCSolver, and OndselAdapter
- Module functions wrapping SolverRegistry (available, load, joints_for,
  set_default, get_default, register_solver)
- PySolverHolder class forwarding virtual calls with GIL acquisition
- register_solver() for runtime Python solver registration

IKCSolver constructor moved from protected to public for pybind11
trampoline access (class remains abstract via 3 pure virtuals).

Includes 16 Python tests covering module import, type bindings, enum
values, registry functions, Python solver subclassing, and full
register/load/solve round-trip.

Closes #288
2026-02-19 18:04:49 -06:00

190 lines
8.0 KiB
C++

// 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, {}, -1, {}, 0};
}
/// 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, {}, -1, {}, 0};
}
/// 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, {}, -1, {}, 0};
}
// ── 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;
}
/// Export solver-native debug/diagnostic file (e.g. ASMT for OndselSolver).
/// Default: no-op. Requires a prior solve() or run_kinematic() call.
virtual void export_native(const std::string& /*path*/)
{
}
/// 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;
}
// Public default constructor for pybind11 trampoline support.
// The class remains abstract (3 pure virtuals prevent direct instantiation).
IKCSolver() = default;
private:
// 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