diff --git a/src/Mod/Assembly/App/CMakeLists.txt b/src/Mod/Assembly/App/CMakeLists.txt index 87053222f0..96919de54e 100644 --- a/src/Mod/Assembly/App/CMakeLists.txt +++ b/src/Mod/Assembly/App/CMakeLists.txt @@ -6,6 +6,7 @@ set(Assembly_LIBS Spreadsheet FreeCADApp OndselSolver + KCSolve ) generate_from_py(AssemblyObject) diff --git a/src/Mod/Assembly/CMakeLists.txt b/src/Mod/Assembly/CMakeLists.txt index f79b42cb35..1c51a9a13f 100644 --- a/src/Mod/Assembly/CMakeLists.txt +++ b/src/Mod/Assembly/CMakeLists.txt @@ -11,6 +11,7 @@ else () endif () endif () +add_subdirectory(Solver) add_subdirectory(App) if(BUILD_GUI) diff --git a/src/Mod/Assembly/Solver/CMakeLists.txt b/src/Mod/Assembly/Solver/CMakeLists.txt new file mode 100644 index 0000000000..51717be77e --- /dev/null +++ b/src/Mod/Assembly/Solver/CMakeLists.txt @@ -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 +) diff --git a/src/Mod/Assembly/Solver/IKCSolver.h b/src/Mod/Assembly/Solver/IKCSolver.h new file mode 100644 index 0000000000..cd9ac78a9b --- /dev/null +++ b/src/Mod/Assembly/Solver/IKCSolver.h @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSOLVE_IKCSOLVER_H +#define KCSOLVE_IKCSOLVER_H + +#include +#include +#include + +#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 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& /*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& /*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 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 diff --git a/src/Mod/Assembly/Solver/SolverRegistry.h b/src/Mod/Assembly/Solver/SolverRegistry.h new file mode 100644 index 0000000000..ea0dee13cc --- /dev/null +++ b/src/Mod/Assembly/Solver/SolverRegistry.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSOLVE_SOLVERREGISTRY_H +#define KCSOLVE_SOLVERREGISTRY_H + +#include +#include +#include +#include +#include +#include + +#include "IKCSolver.h" + +namespace KCSolve +{ + +/// Factory function that creates a solver instance. +using CreateSolverFn = std::function()>; + +/// 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(); }); +/// +/// // 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 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 get(const std::string& name = {}) const + { + std::lock_guard 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 available() const + { + std::lock_guard lock(mutex_); + std::vector 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 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 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 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 factories_; + std::string default_name_; +}; + +} // namespace KCSolve + +#endif // KCSOLVE_SOLVERREGISTRY_H diff --git a/src/Mod/Assembly/Solver/Types.h b/src/Mod/Assembly/Solver/Types.h new file mode 100644 index 0000000000..6fdcca6fb3 --- /dev/null +++ b/src/Mod/Assembly/Solver/Types.h @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kindred Systems * + * * + * 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 * + * . * + * * + ***************************************************************************/ + +#ifndef KCSOLVE_TYPES_H +#define KCSOLVE_TYPES_H + +#include +#include +#include +#include +#include +#include + +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 position {0.0, 0.0, 0.0}; + std::array 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 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 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 parts; + std::vector constraints; + std::vector motions; + + // Present when running kinematic simulation via run_kinematic(). + std::optional 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 placements; + + // Degrees of freedom remaining (-1 = unknown). + int dof {-1}; + + // Constraint diagnostics (redundant, conflicting, etc.). + std::vector diagnostics; + + // For kinematic simulation: number of computed frames. + std::size_t num_frames {0}; +}; + +} // namespace KCSolve + +#endif // KCSOLVE_TYPES_H