feat(solver): Phase 1d — refactor AssemblyObject to use SolverRegistry #295

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

Summary

Refactor AssemblyObject to call through SolverRegistry/IKCSolver instead of directly using OndselSolver. This is the integration seam — after this, AssemblyObject has zero OndselSolver dependencies.

Parent: #287 (Phase 1)
Depends on: #292 (1a), #293 (1b), #294 (1c)
Blocks: #296 (1e — tests)

Scope

Files modified

  • src/Mod/Assembly/App/AssemblyObject.h — remove MbD forward decls, replace with IKCSolver*
  • src/Mod/Assembly/App/AssemblyObject.cpp — rewrite solver interaction layer
  • src/Mod/Assembly/App/CMakeLists.txt — link KCSolve instead of OndselSolver directly
  • src/Mod/Assembly/CMakeLists.txt — remove OndselSolver include path (moved into KCSolve)

AssemblyObject.h changes

Remove:

#include <OndselSolver/enum.h>

namespace MbD {
class ASMTPart;
class ASMTAssembly;
class ASMTJoint;
class ASMTMarker;
}

Remove from private members:

std::shared_ptr<MbD::ASMTAssembly> mbdAssembly;
std::unordered_map<App::DocumentObject*, MbDPartData> objectPartMap;

Remove all MbD-specific methods:

  • makeMbdAssembly(), makeMbdPart(), makeMbdMarker()
  • makeMbdJoint(), makeMbdJointOfType(), makeMbdJointDistance()
  • handleOneSideOfJoint(), getRackPinionMarkers()
  • getMbDPart(), getMbDData(), getMbdPlacement()
  • MbDPartData struct

Add:

#include <KCSolve/Types.h>

// New private members:
KCSolve::IKCSolver* solver_;  // from registry, not owned
KCSolve::SolveContext buildSolveContext(
    const std::vector<App::DocumentObject*>& joints,
    const std::unordered_set<App::DocumentObject*>& groundedParts
);

AssemblyObject.cpp rewrite

solve() — currently 30 lines of MbD setup + runPreDrag() call:

int AssemblyObject::solve(bool enableRedo, bool updateJCS) {
    ensureIdentityPlacements();
    auto groundedObjs = fixGroundedParts();  // still needed for grounded set
    auto joints = getJoints(updateJCS);
    removeUnconnectedJoints(joints, groundedObjs);

    auto ctx = buildSolveContext(joints, groundedObjs);
    savePlacementsForUndo();

    auto result = solver_->solve(ctx);
    if (result.status != KCSolve::SolveStatus::Converged) {
        // handle error
    }

    applyPlacements(result.placements);  // was setNewPlacements()
    updateSolveStatus(result.diagnostics);  // was MbD introspection
    // ...
}

preDrag() / doDragStep() / postDrag() — rewritten to use IKCSolver drag protocol:

void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts) {
    auto ctx = buildSolveContext(/*...*/);
    solver_->pre_drag(ctx);
    // ...
}
void AssemblyObject::doDragStep() {
    auto ctx = buildSolveContext(/*...*/);  // with updated placements
    auto result = solver_->drag_step(ctx, draggedPartIds_);
    applyPlacements(result.placements);
}
void AssemblyObject::postDrag() {
    solver_->post_drag();
}

generateSimulation() / updateForFrame() — rewritten for simulation protocol:

int AssemblyObject::generateSimulation(App::DocumentObject* sim) {
    auto ctx = buildSolveContext(/*...*/);
    // add simulation parameters to ctx
    auto result = solver_->run_kinematic(ctx);
    // ...
}
int AssemblyObject::updateForFrame(size_t index, bool updateJCS) {
    auto result = solver_->update_for_frame(index);
    applyPlacements(result.placements);
    // ...
}
size_t AssemblyObject::numberOfFrames() {
    return solver_->num_frames();
}

buildSolveContext() — the new core method

This is where the Distance decomposition happens. The current makeMbdJointDistance() 35-case geometry dispatch moves here:

KCSolve::SolveContext AssemblyObject::buildSolveContext(
    const std::vector<App::DocumentObject*>& joints,
    const std::unordered_set<App::DocumentObject*>& groundedParts)
{
    KCSolve::SolveContext ctx;
    ctx.tolerance = getPreferenceTolerance();
    ctx.max_iterations = getPreferenceMaxIter();

    // Parts and placements
    for (auto* comp : getAssemblyComponents(this)) {
        auto plc = getPlacementFromProp(comp, "Placement");
        ctx.placements[comp->getFullName()] = placementToTransform(plc);
    }

    // Grounded parts
    for (auto* gp : groundedParts) {
        ctx.grounded.insert(gp->getFullName());
    }

    // Constraints — this is where JointType + DistanceType -> BaseJointKind happens
    for (auto* joint : joints) {
        JointType jtype = getJointType(joint);
        if (jtype == JointType::Distance) {
            // Classify geometry and emit specific BaseJointKind
            DistanceType dtype = getDistanceType(joint);
            ctx.constraints.push_back(classifyDistanceConstraint(joint, dtype));
        } else {
            ctx.constraints.push_back(classifyDirectConstraint(joint, jtype));
        }
    }

    // Bundle hint
    ctx.bundle_fixed = !solver_->supports_bundle_fixed();

    return ctx;
}

updateSolveStatus() rewrite

Currently introspects mbdSystem->jointsMotionsDo() to find redundant constraints. Replace with:

void AssemblyObject::updateSolveStatus(const std::vector<KCSolve::ConstraintDiagnostic>& diags) {
    lastRedundantJoints.clear();
    for (const auto& d : diags) {
        if (!d.satisfied && d.message.starts_with("Redundant")) {
            lastRedundantJoints.push_back(d.constraint_id);
        }
    }
    lastHasRedundancies = !lastRedundantJoints.empty();
    signalSolverUpdate();
}

What stays in AssemblyObject (solver-agnostic)

These methods are NOT solver-specific and remain:

  • ensureIdentityPlacements() — LinkGroup placement normalization
  • getJoints(), getGroundedParts(), fixGroundedParts() — FreeCAD document traversal
  • removeUnconnectedJoints(), traverseAndMarkConnectedParts() — graph connectivity
  • validateNewPlacements() — orientation flip detection
  • savePlacementsForUndo(), undoSolve(), clearUndo() — undo system
  • redrawJointPlacements() — Python callback for joint visualization
  • exportAsASMT()needs discussion: currently calls mbdAssembly->outputFile(). Could call through solver or stay Ondsel-specific.
  • getJointsOfPart(), isPartGrounded(), isPartConnected() — part/joint queries
  • getDownstreamParts(), getUpstreamMovingPart() — kinematic chain traversal

CMakeLists.txt changes

# Assembly/App/CMakeLists.txt
set(Assembly_LIBS
    Part
    PartDesign
    Spreadsheet
    FreeCADApp
    KCSolve          # was: OndselSolver
)
# Assembly/CMakeLists.txt — remove:
if (NOT FREECAD_USE_EXTERNAL_ONDSELSOLVER)
    include_directories(${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver)
endif()
# KCSolve handles its own OndselSolver include paths internally

GUI callers — NO CHANGES

All GUI code calls AssemblyObject methods which have the same signatures:

  • ViewProviderAssembly.cpp: solve(), preDrag(), doDragStep(), postDrag()
  • CommandCreateSimulation.py: generateSimulation(), numberOfFrames(), updateForFrame()
  • CommandExportASMT.py: exportAsASMT()
  • JointObject.py: solve()

None of these need modification.

Acceptance criteria

  • Zero #include <OndselSolver/*> in AssemblyObject.cpp or .h
  • Zero MbD:: references in AssemblyObject.cpp or .h
  • Assembly module links KCSolve, not OndselSolver directly
  • buildSolveContext() correctly decomposes Distance into specific BaseJointKind types
  • All 6 solver call patterns work through IKCSolver interface
  • GUI callers unchanged — same AssemblyObject public API
  • exportAsASMT() still functional (Ondsel-specific path acceptable for now)
  • Build succeeds with no OndselSolver include path in Assembly CMakeLists

References

  • src/Mod/Assembly/App/AssemblyObject.cpp — the file being refactored
  • src/Mod/Assembly/App/AssemblyObject.h — header cleanup
  • src/Mod/Assembly/Gui/ViewProviderAssembly.cpp — verify no breakage
  • src/Mod/Assembly/JointObject.py — verify no breakage
## Summary Refactor AssemblyObject to call through SolverRegistry/IKCSolver instead of directly using OndselSolver. This is the integration seam — after this, AssemblyObject has zero OndselSolver dependencies. **Parent:** #287 (Phase 1) **Depends on:** #292 (1a), #293 (1b), #294 (1c) **Blocks:** #296 (1e — tests) ## Scope ### Files modified - `src/Mod/Assembly/App/AssemblyObject.h` — remove MbD forward decls, replace with IKCSolver* - `src/Mod/Assembly/App/AssemblyObject.cpp` — rewrite solver interaction layer - `src/Mod/Assembly/App/CMakeLists.txt` — link KCSolve instead of OndselSolver directly - `src/Mod/Assembly/CMakeLists.txt` — remove OndselSolver include path (moved into KCSolve) ### AssemblyObject.h changes **Remove:** ```cpp #include <OndselSolver/enum.h> namespace MbD { class ASMTPart; class ASMTAssembly; class ASMTJoint; class ASMTMarker; } ``` **Remove from private members:** ```cpp std::shared_ptr<MbD::ASMTAssembly> mbdAssembly; std::unordered_map<App::DocumentObject*, MbDPartData> objectPartMap; ``` **Remove all MbD-specific methods:** - `makeMbdAssembly()`, `makeMbdPart()`, `makeMbdMarker()` - `makeMbdJoint()`, `makeMbdJointOfType()`, `makeMbdJointDistance()` - `handleOneSideOfJoint()`, `getRackPinionMarkers()` - `getMbDPart()`, `getMbDData()`, `getMbdPlacement()` - `MbDPartData` struct **Add:** ```cpp #include <KCSolve/Types.h> // New private members: KCSolve::IKCSolver* solver_; // from registry, not owned KCSolve::SolveContext buildSolveContext( const std::vector<App::DocumentObject*>& joints, const std::unordered_set<App::DocumentObject*>& groundedParts ); ``` ### AssemblyObject.cpp rewrite **`solve()` — currently 30 lines of MbD setup + `runPreDrag()` call:** ```cpp int AssemblyObject::solve(bool enableRedo, bool updateJCS) { ensureIdentityPlacements(); auto groundedObjs = fixGroundedParts(); // still needed for grounded set auto joints = getJoints(updateJCS); removeUnconnectedJoints(joints, groundedObjs); auto ctx = buildSolveContext(joints, groundedObjs); savePlacementsForUndo(); auto result = solver_->solve(ctx); if (result.status != KCSolve::SolveStatus::Converged) { // handle error } applyPlacements(result.placements); // was setNewPlacements() updateSolveStatus(result.diagnostics); // was MbD introspection // ... } ``` **`preDrag()` / `doDragStep()` / `postDrag()` — rewritten to use IKCSolver drag protocol:** ```cpp void AssemblyObject::preDrag(std::vector<App::DocumentObject*> dragParts) { auto ctx = buildSolveContext(/*...*/); solver_->pre_drag(ctx); // ... } void AssemblyObject::doDragStep() { auto ctx = buildSolveContext(/*...*/); // with updated placements auto result = solver_->drag_step(ctx, draggedPartIds_); applyPlacements(result.placements); } void AssemblyObject::postDrag() { solver_->post_drag(); } ``` **`generateSimulation()` / `updateForFrame()` — rewritten for simulation protocol:** ```cpp int AssemblyObject::generateSimulation(App::DocumentObject* sim) { auto ctx = buildSolveContext(/*...*/); // add simulation parameters to ctx auto result = solver_->run_kinematic(ctx); // ... } int AssemblyObject::updateForFrame(size_t index, bool updateJCS) { auto result = solver_->update_for_frame(index); applyPlacements(result.placements); // ... } size_t AssemblyObject::numberOfFrames() { return solver_->num_frames(); } ``` ### buildSolveContext() — the new core method This is where the **Distance decomposition** happens. The current `makeMbdJointDistance()` 35-case geometry dispatch moves here: ```cpp KCSolve::SolveContext AssemblyObject::buildSolveContext( const std::vector<App::DocumentObject*>& joints, const std::unordered_set<App::DocumentObject*>& groundedParts) { KCSolve::SolveContext ctx; ctx.tolerance = getPreferenceTolerance(); ctx.max_iterations = getPreferenceMaxIter(); // Parts and placements for (auto* comp : getAssemblyComponents(this)) { auto plc = getPlacementFromProp(comp, "Placement"); ctx.placements[comp->getFullName()] = placementToTransform(plc); } // Grounded parts for (auto* gp : groundedParts) { ctx.grounded.insert(gp->getFullName()); } // Constraints — this is where JointType + DistanceType -> BaseJointKind happens for (auto* joint : joints) { JointType jtype = getJointType(joint); if (jtype == JointType::Distance) { // Classify geometry and emit specific BaseJointKind DistanceType dtype = getDistanceType(joint); ctx.constraints.push_back(classifyDistanceConstraint(joint, dtype)); } else { ctx.constraints.push_back(classifyDirectConstraint(joint, jtype)); } } // Bundle hint ctx.bundle_fixed = !solver_->supports_bundle_fixed(); return ctx; } ``` ### updateSolveStatus() rewrite Currently introspects `mbdSystem->jointsMotionsDo()` to find redundant constraints. Replace with: ```cpp void AssemblyObject::updateSolveStatus(const std::vector<KCSolve::ConstraintDiagnostic>& diags) { lastRedundantJoints.clear(); for (const auto& d : diags) { if (!d.satisfied && d.message.starts_with("Redundant")) { lastRedundantJoints.push_back(d.constraint_id); } } lastHasRedundancies = !lastRedundantJoints.empty(); signalSolverUpdate(); } ``` ### What stays in AssemblyObject (solver-agnostic) These methods are NOT solver-specific and remain: - `ensureIdentityPlacements()` — LinkGroup placement normalization - `getJoints()`, `getGroundedParts()`, `fixGroundedParts()` — FreeCAD document traversal - `removeUnconnectedJoints()`, `traverseAndMarkConnectedParts()` — graph connectivity - `validateNewPlacements()` — orientation flip detection - `savePlacementsForUndo()`, `undoSolve()`, `clearUndo()` — undo system - `redrawJointPlacements()` — Python callback for joint visualization - `exportAsASMT()` — **needs discussion**: currently calls `mbdAssembly->outputFile()`. Could call through solver or stay Ondsel-specific. - `getJointsOfPart()`, `isPartGrounded()`, `isPartConnected()` — part/joint queries - `getDownstreamParts()`, `getUpstreamMovingPart()` — kinematic chain traversal ### CMakeLists.txt changes ```cmake # Assembly/App/CMakeLists.txt set(Assembly_LIBS Part PartDesign Spreadsheet FreeCADApp KCSolve # was: OndselSolver ) ``` ```cmake # Assembly/CMakeLists.txt — remove: if (NOT FREECAD_USE_EXTERNAL_ONDSELSOLVER) include_directories(${CMAKE_SOURCE_DIR}/src/3rdParty/OndselSolver) endif() # KCSolve handles its own OndselSolver include paths internally ``` ## GUI callers — NO CHANGES All GUI code calls AssemblyObject methods which have the same signatures: - `ViewProviderAssembly.cpp`: `solve()`, `preDrag()`, `doDragStep()`, `postDrag()` - `CommandCreateSimulation.py`: `generateSimulation()`, `numberOfFrames()`, `updateForFrame()` - `CommandExportASMT.py`: `exportAsASMT()` - `JointObject.py`: `solve()` None of these need modification. ## Acceptance criteria - [ ] Zero `#include <OndselSolver/*>` in AssemblyObject.cpp or .h - [ ] Zero `MbD::` references in AssemblyObject.cpp or .h - [ ] Assembly module links KCSolve, not OndselSolver directly - [ ] `buildSolveContext()` correctly decomposes Distance into specific BaseJointKind types - [ ] All 6 solver call patterns work through IKCSolver interface - [ ] GUI callers unchanged — same AssemblyObject public API - [ ] `exportAsASMT()` still functional (Ondsel-specific path acceptable for now) - [ ] Build succeeds with no OndselSolver include path in Assembly CMakeLists ## References - `src/Mod/Assembly/App/AssemblyObject.cpp` — the file being refactored - `src/Mod/Assembly/App/AssemblyObject.h` — header cleanup - `src/Mod/Assembly/Gui/ViewProviderAssembly.cpp` — verify no breakage - `src/Mod/Assembly/JointObject.py` — verify no breakage
forbes added the enhancement label 2026-02-19 21:28:20 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#295