feat(solver): Phase 1c — OndselAdapter implementation #294

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

Summary

Wrap OndselSolver behind the IKCSolver interface. This is the largest piece of Phase 1 — it moves all MbD-specific code out of AssemblyObject and into a self-contained adapter.

Parent: #287 (Phase 1)
Depends on: #292 (1a — API types), #293 (1b — SolverRegistry)
Blocks: #295 (1d — AssemblyObject refactor)

Scope

Files

src/Mod/Assembly/Solver/
├── src/
│   ├── SolverRegistry.cpp    # from 1b
│   └── OndselAdapter.cpp     # NEW — all OndselSolver integration
├── include/KCSolve/
│   └── OndselAdapter.h       # NEW
└── CMakeLists.txt             # add OndselSolver link dependency

OndselAdapter class

class OndselAdapter : public IKCSolver {
public:
    std::string id() const override { return "ondsel"; }
    std::string name() const override { return "OndselSolver (Lagrangian)"; }
    std::string version() const override;

    std::vector<JointDef> supported_joints() const override;

    SolveResult solve(const SolveContext& ctx) override;
    SolveResult update(const SolveContext& ctx, const std::string& changed) override;

    void pre_drag(const SolveContext& ctx) override;
    SolveResult drag_step(const SolveContext& ctx,
                           const std::vector<std::string>& dragged_parts) override;
    void post_drag() override;

    SolveResult run_kinematic(const SolveContext& ctx) override;
    size_t num_frames() const override;
    SolveResult update_for_frame(size_t index) override;

    SolveStatus diagnose(const SolveContext& ctx) override;
    bool is_deterministic() const override { return true; }
    bool supports_bundle_fixed() const override { return false; }

private:
    std::shared_ptr<MbD::ASMTAssembly> mbdAssembly_;
    // Internal translation helpers
    std::shared_ptr<MbD::ASMTAssembly> build_assembly(const SolveContext& ctx);
    std::shared_ptr<MbD::ASMTJoint> create_joint(const Constraint& c);
    SolveResult extract_result();
    std::vector<ConstraintDiagnostic> extract_diagnostics();
};

Translation layer: SolveContext -> ASMTAssembly

This moves the following logic out of AssemblyObject.cpp into OndselAdapter:

Current AssemblyObject method OndselAdapter equivalent
makeMbdAssembly() build_assembly(ctx) — creates ASMTAssembly from SolveContext
makeMbdPart() Internal — iterates ctx.placements, creates ASMTPart per part
makeMbdMarker() Internal — creates coordinate frames from constraint geometry refs
makeMbdJointOfType() create_joint(c) — maps BaseJointKind to ASMT* types
makeMbdJointDistance() Eliminated — BaseJointKind is already decomposed
fixGroundedPart() Internal — iterates ctx.grounded, creates ASMTFixedJoint per grounded part

Joint type mapping: BaseJointKind -> ASMT*

BaseJointKind OndselSolver Type Notes
Coincident ASMTSphericalJoint (dist=0) or ASMTSphSphJoint Current PointPoint logic
PointOnLine ASMTCylSphJoint Current PointLine logic
PointInPlane ASMTPointInPlaneJoint Current PointPlane logic
Concentric ASMTRevCylJoint Current LineLine/CircleCircle logic
Parallel ASMTParallelAxesJoint Direct map
Perpendicular ASMTPerpendicularJoint Direct map
Angle ASMTAngleJoint With theIzJz parameter
Tangent ASMTPlanarJoint (tangent mode) New — not currently exposed
Planar ASMTPlanarJoint Current PlanePlane logic
Fixed ASMTFixedJoint Direct map
Revolute ASMTRevoluteJoint Direct map
Cylindrical ASMTCylindricalJoint Direct map
Slider ASMTTranslationalJoint Direct map
Ball ASMTSphericalJoint Direct map
Screw ASMTScrewJoint Direct map
Gear ASMTGearJoint radiusI/radiusJ from params
Rack ASMTRackPinionJoint pitchRadius from params
Belt ASMTGearJoint (negative radius) Current Belt logic

Plus: limits (ASMTRotationLimit, ASMTTranslationLimit) and motions (ASMTRotationalMotion, ASMTTranslationalMotion, ASMTGeneralMotion) from Constraint params.

Result extraction: ASMTAssembly -> SolveResult

Current AssemblyObject method OndselAdapter equivalent
getMbdPlacement() extract_result() — reads position3D + quaternions from all ASMTParts
updateSolveStatus() extract_diagnostics() — iterates mbdSystem->jointsMotionsDo() to build ConstraintDiagnostic list
validateNewPlacements() Stays in AssemblyObject — this is orientation-flip detection, solver-agnostic

ExternalSystem decoupling

Break the bidirectional pointer:

  • OndselAdapter creates ASMTAssembly internally but does NOT set externalSystem->freecadAssemblyObject = this
  • Instead, OndselAdapter owns the full ASMTAssembly lifecycle
  • The ExternalSystem::preMbDrun() / updateFromMbD() callbacks that referenced freecadAssemblyObject were already commented-out stubs — no functional change
  • If OndselSolver's System.cpp calls externalSystem->preMbDrun(), it routes through asmtAssembly path (which works today)

Solver registration

In Assembly module init (or OndselAdapter static init):

auto& registry = SolverRegistry::instance();
registry.register_solver(std::make_unique<OndselAdapter>());
registry.set_default("ondsel");

Key complexity

  1. The Distance decomposition — Currently makeMbdJointDistance() is a 35-case switch on DistanceType. With decomposed BaseJointKind, this logic moves upstream into AssemblyObject's SolveContext builder (1d) which classifies geometry and emits the specific BaseJointKind. OndselAdapter receives the already-classified type.

  2. Marker coordinate frames — OndselSolver uses named markers on parts for joint attachment. The adapter must translate SolveContext constraint refs (part + geometry) into ASMTMarker placements. This is currently split across handleOneSideOfJoint() and getRackPinionMarkers() in AssemblyObject.

  3. Drag statepre_drag() must call runPreDrag(), drag_step() must update ASMTPart positions and call runDragStep(), post_drag() must call runPostDrag(). The adapter holds mbdAssembly_ as internal state between these calls.

  4. Simulation staterun_kinematic() calls runKINEMATIC(), then num_frames() and update_for_frame() query the stored simulation results.

Acceptance criteria

  • OndselAdapter implements all IKCSolver methods
  • All 30+ OndselSolver #includes are in OndselAdapter.cpp, not AssemblyObject
  • Joint type mapping covers all current makeMbdJointOfType() + makeMbdJointDistance() cases
  • ExternalSystem no longer holds a raw pointer to AssemblyObject
  • Drag protocol (pre/step/post) works with stateful ASMTAssembly
  • Simulation protocol (run/frames/update) works with stateful ASMTAssembly
  • extract_diagnostics() produces ConstraintDiagnostic list equivalent to current updateSolveStatus() parsing
  • Registers as "ondsel" in SolverRegistry at module init

References

  • src/Mod/Assembly/App/AssemblyObject.cpp — current integration (2157 lines)
  • src/3rdParty/OndselSolver/OndselSolver/ExternalSystem.h — bidirectional coupling to break
  • src/3rdParty/OndselSolver/OndselSolver/ASMTAssembly.h — OndselSolver API surface
  • docs/INTER_SOLVER.md §5 (Layer 2: OndselSolver Adapter)
## Summary Wrap OndselSolver behind the IKCSolver interface. This is the largest piece of Phase 1 — it moves all MbD-specific code out of AssemblyObject and into a self-contained adapter. **Parent:** #287 (Phase 1) **Depends on:** #292 (1a — API types), #293 (1b — SolverRegistry) **Blocks:** #295 (1d — AssemblyObject refactor) ## Scope ### Files ``` src/Mod/Assembly/Solver/ ├── src/ │ ├── SolverRegistry.cpp # from 1b │ └── OndselAdapter.cpp # NEW — all OndselSolver integration ├── include/KCSolve/ │ └── OndselAdapter.h # NEW └── CMakeLists.txt # add OndselSolver link dependency ``` ### OndselAdapter class ```cpp class OndselAdapter : public IKCSolver { public: std::string id() const override { return "ondsel"; } std::string name() const override { return "OndselSolver (Lagrangian)"; } std::string version() const override; std::vector<JointDef> supported_joints() const override; SolveResult solve(const SolveContext& ctx) override; SolveResult update(const SolveContext& ctx, const std::string& changed) override; void pre_drag(const SolveContext& ctx) override; SolveResult drag_step(const SolveContext& ctx, const std::vector<std::string>& dragged_parts) override; void post_drag() override; SolveResult run_kinematic(const SolveContext& ctx) override; size_t num_frames() const override; SolveResult update_for_frame(size_t index) override; SolveStatus diagnose(const SolveContext& ctx) override; bool is_deterministic() const override { return true; } bool supports_bundle_fixed() const override { return false; } private: std::shared_ptr<MbD::ASMTAssembly> mbdAssembly_; // Internal translation helpers std::shared_ptr<MbD::ASMTAssembly> build_assembly(const SolveContext& ctx); std::shared_ptr<MbD::ASMTJoint> create_joint(const Constraint& c); SolveResult extract_result(); std::vector<ConstraintDiagnostic> extract_diagnostics(); }; ``` ### Translation layer: SolveContext -> ASMTAssembly This moves the following logic **out of AssemblyObject.cpp** into OndselAdapter: | Current AssemblyObject method | OndselAdapter equivalent | |------|------| | `makeMbdAssembly()` | `build_assembly(ctx)` — creates ASMTAssembly from SolveContext | | `makeMbdPart()` | Internal — iterates `ctx.placements`, creates ASMTPart per part | | `makeMbdMarker()` | Internal — creates coordinate frames from constraint geometry refs | | `makeMbdJointOfType()` | `create_joint(c)` — maps BaseJointKind to ASMT* types | | `makeMbdJointDistance()` | Eliminated — BaseJointKind is already decomposed | | `fixGroundedPart()` | Internal — iterates `ctx.grounded`, creates ASMTFixedJoint per grounded part | ### Joint type mapping: BaseJointKind -> ASMT* | BaseJointKind | OndselSolver Type | Notes | |---|---|---| | Coincident | ASMTSphericalJoint (dist=0) or ASMTSphSphJoint | Current PointPoint logic | | PointOnLine | ASMTCylSphJoint | Current PointLine logic | | PointInPlane | ASMTPointInPlaneJoint | Current PointPlane logic | | Concentric | ASMTRevCylJoint | Current LineLine/CircleCircle logic | | Parallel | ASMTParallelAxesJoint | Direct map | | Perpendicular | ASMTPerpendicularJoint | Direct map | | Angle | ASMTAngleJoint | With theIzJz parameter | | Tangent | ASMTPlanarJoint (tangent mode) | New — not currently exposed | | Planar | ASMTPlanarJoint | Current PlanePlane logic | | Fixed | ASMTFixedJoint | Direct map | | Revolute | ASMTRevoluteJoint | Direct map | | Cylindrical | ASMTCylindricalJoint | Direct map | | Slider | ASMTTranslationalJoint | Direct map | | Ball | ASMTSphericalJoint | Direct map | | Screw | ASMTScrewJoint | Direct map | | Gear | ASMTGearJoint | radiusI/radiusJ from params | | Rack | ASMTRackPinionJoint | pitchRadius from params | | Belt | ASMTGearJoint (negative radius) | Current Belt logic | Plus: limits (ASMTRotationLimit, ASMTTranslationLimit) and motions (ASMTRotationalMotion, ASMTTranslationalMotion, ASMTGeneralMotion) from Constraint params. ### Result extraction: ASMTAssembly -> SolveResult | Current AssemblyObject method | OndselAdapter equivalent | |------|------| | `getMbdPlacement()` | `extract_result()` — reads position3D + quaternions from all ASMTParts | | `updateSolveStatus()` | `extract_diagnostics()` — iterates `mbdSystem->jointsMotionsDo()` to build ConstraintDiagnostic list | | `validateNewPlacements()` | Stays in AssemblyObject — this is orientation-flip detection, solver-agnostic | ### ExternalSystem decoupling **Break the bidirectional pointer:** - OndselAdapter creates ASMTAssembly internally but does NOT set `externalSystem->freecadAssemblyObject = this` - Instead, OndselAdapter owns the full ASMTAssembly lifecycle - The `ExternalSystem::preMbDrun()` / `updateFromMbD()` callbacks that referenced `freecadAssemblyObject` were already commented-out stubs — no functional change - If OndselSolver's System.cpp calls `externalSystem->preMbDrun()`, it routes through `asmtAssembly` path (which works today) ### Solver registration In Assembly module init (or OndselAdapter static init): ```cpp auto& registry = SolverRegistry::instance(); registry.register_solver(std::make_unique<OndselAdapter>()); registry.set_default("ondsel"); ``` ## Key complexity 1. **The `Distance` decomposition** — Currently `makeMbdJointDistance()` is a 35-case switch on `DistanceType`. With decomposed BaseJointKind, this logic moves **upstream** into AssemblyObject's SolveContext builder (1d) which classifies geometry and emits the specific BaseJointKind. OndselAdapter receives the already-classified type. 2. **Marker coordinate frames** — OndselSolver uses named markers on parts for joint attachment. The adapter must translate SolveContext constraint refs (part + geometry) into ASMTMarker placements. This is currently split across `handleOneSideOfJoint()` and `getRackPinionMarkers()` in AssemblyObject. 3. **Drag state** — `pre_drag()` must call `runPreDrag()`, `drag_step()` must update ASMTPart positions and call `runDragStep()`, `post_drag()` must call `runPostDrag()`. The adapter holds `mbdAssembly_` as internal state between these calls. 4. **Simulation state** — `run_kinematic()` calls `runKINEMATIC()`, then `num_frames()` and `update_for_frame()` query the stored simulation results. ## Acceptance criteria - [ ] OndselAdapter implements all IKCSolver methods - [ ] All 30+ OndselSolver `#include`s are in OndselAdapter.cpp, not AssemblyObject - [ ] Joint type mapping covers all current `makeMbdJointOfType()` + `makeMbdJointDistance()` cases - [ ] ExternalSystem no longer holds a raw pointer to AssemblyObject - [ ] Drag protocol (pre/step/post) works with stateful ASMTAssembly - [ ] Simulation protocol (run/frames/update) works with stateful ASMTAssembly - [ ] `extract_diagnostics()` produces ConstraintDiagnostic list equivalent to current `updateSolveStatus()` parsing - [ ] Registers as "ondsel" in SolverRegistry at module init ## References - `src/Mod/Assembly/App/AssemblyObject.cpp` — current integration (2157 lines) - `src/3rdParty/OndselSolver/OndselSolver/ExternalSystem.h` — bidirectional coupling to break - `src/3rdParty/OndselSolver/OndselSolver/ASMTAssembly.h` — OndselSolver API surface - `docs/INTER_SOLVER.md` §5 (Layer 2: OndselSolver Adapter)
forbes added the enhancement label 2026-02-19 21:27:28 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#294