feat(solver): pack SolveContext into .kc archives on save (#289 phase 3d)
All checks were successful
Build and Test / build (pull_request) Successful in 29m51s

Expose AssemblyObject::getSolveContext() to Python and hook into the
.kc save flow so that silo/solver/context.json is packed into every
assembly archive. This lets server-side solver runners operate on
pre-extracted constraint graphs without a full FreeCAD installation.

Changes:
- Add public getSolveContext() to AssemblyObject (C++ and Python)
- Build Python dict via CPython C API matching kcsolve.SolveContext.to_dict()
- Register _solver_context_hook in kc_format.py pre-reinject hooks
- Add silo/solver/context.json to silo_tree.py _KNOWN_ENTRIES
This commit is contained in:
forbes
2026-02-20 17:12:25 -06:00
parent 311b3ea4f1
commit 4cf54caf7b
6 changed files with 258 additions and 6 deletions

View File

@@ -21,13 +21,161 @@
* *
***************************************************************************/
// inclusion of the generated files (generated out of AssemblyObject.xml)
// inclusion of the generated files (generated out of AssemblyObject.xml)
#include "AssemblyObjectPy.h"
#include "AssemblyObjectPy.cpp"
#include <Mod/Assembly/Solver/SolverRegistry.h>
using namespace Assembly;
namespace
{
// ── Enum-to-string tables for dict serialization ───────────────────
// String values must match kcsolve_py.cpp py::enum_ .value() names exactly.
const char* baseJointKindStr(KCSolve::BaseJointKind k)
{
switch (k) {
case KCSolve::BaseJointKind::Coincident: return "Coincident";
case KCSolve::BaseJointKind::PointOnLine: return "PointOnLine";
case KCSolve::BaseJointKind::PointInPlane: return "PointInPlane";
case KCSolve::BaseJointKind::Concentric: return "Concentric";
case KCSolve::BaseJointKind::Tangent: return "Tangent";
case KCSolve::BaseJointKind::Planar: return "Planar";
case KCSolve::BaseJointKind::LineInPlane: return "LineInPlane";
case KCSolve::BaseJointKind::Parallel: return "Parallel";
case KCSolve::BaseJointKind::Perpendicular: return "Perpendicular";
case KCSolve::BaseJointKind::Angle: return "Angle";
case KCSolve::BaseJointKind::Fixed: return "Fixed";
case KCSolve::BaseJointKind::Revolute: return "Revolute";
case KCSolve::BaseJointKind::Cylindrical: return "Cylindrical";
case KCSolve::BaseJointKind::Slider: return "Slider";
case KCSolve::BaseJointKind::Ball: return "Ball";
case KCSolve::BaseJointKind::Screw: return "Screw";
case KCSolve::BaseJointKind::Universal: return "Universal";
case KCSolve::BaseJointKind::Gear: return "Gear";
case KCSolve::BaseJointKind::RackPinion: return "RackPinion";
case KCSolve::BaseJointKind::Cam: return "Cam";
case KCSolve::BaseJointKind::Slot: return "Slot";
case KCSolve::BaseJointKind::DistancePointPoint: return "DistancePointPoint";
case KCSolve::BaseJointKind::DistanceCylSph: return "DistanceCylSph";
case KCSolve::BaseJointKind::Custom: return "Custom";
}
return "Custom";
}
const char* limitKindStr(KCSolve::Constraint::Limit::Kind k)
{
switch (k) {
case KCSolve::Constraint::Limit::Kind::TranslationMin: return "TranslationMin";
case KCSolve::Constraint::Limit::Kind::TranslationMax: return "TranslationMax";
case KCSolve::Constraint::Limit::Kind::RotationMin: return "RotationMin";
case KCSolve::Constraint::Limit::Kind::RotationMax: return "RotationMax";
}
return "TranslationMin";
}
const char* motionKindStr(KCSolve::MotionDef::Kind k)
{
switch (k) {
case KCSolve::MotionDef::Kind::Rotational: return "Rotational";
case KCSolve::MotionDef::Kind::Translational: return "Translational";
case KCSolve::MotionDef::Kind::General: return "General";
}
return "Rotational";
}
// ── Python dict builders ───────────────────────────────────────────
// Layout matches solve_context_to_dict() in kcsolve_py.cpp exactly.
Py::Dict transformToDict(const KCSolve::Transform& t)
{
Py::Dict d;
d.setItem("position", Py::TupleN(
Py::Float(t.position[0]),
Py::Float(t.position[1]),
Py::Float(t.position[2])));
d.setItem("quaternion", Py::TupleN(
Py::Float(t.quaternion[0]),
Py::Float(t.quaternion[1]),
Py::Float(t.quaternion[2]),
Py::Float(t.quaternion[3])));
return d;
}
Py::Dict partToDict(const KCSolve::Part& p)
{
Py::Dict d;
d.setItem("id", Py::String(p.id));
d.setItem("placement", transformToDict(p.placement));
d.setItem("mass", Py::Float(p.mass));
d.setItem("grounded", Py::Boolean(p.grounded));
return d;
}
Py::Dict limitToDict(const KCSolve::Constraint::Limit& lim)
{
Py::Dict d;
d.setItem("kind", Py::String(limitKindStr(lim.kind)));
d.setItem("value", Py::Float(lim.value));
d.setItem("tolerance", Py::Float(lim.tolerance));
return d;
}
Py::Dict constraintToDict(const KCSolve::Constraint& c)
{
Py::Dict d;
d.setItem("id", Py::String(c.id));
d.setItem("part_i", Py::String(c.part_i));
d.setItem("marker_i", transformToDict(c.marker_i));
d.setItem("part_j", Py::String(c.part_j));
d.setItem("marker_j", transformToDict(c.marker_j));
d.setItem("type", Py::String(baseJointKindStr(c.type)));
Py::List params;
for (double v : c.params) {
params.append(Py::Float(v));
}
d.setItem("params", params);
Py::List lims;
for (const auto& l : c.limits) {
lims.append(limitToDict(l));
}
d.setItem("limits", lims);
d.setItem("activated", Py::Boolean(c.activated));
return d;
}
Py::Dict motionToDict(const KCSolve::MotionDef& m)
{
Py::Dict d;
d.setItem("kind", Py::String(motionKindStr(m.kind)));
d.setItem("joint_id", Py::String(m.joint_id));
d.setItem("marker_i", Py::String(m.marker_i));
d.setItem("marker_j", Py::String(m.marker_j));
d.setItem("rotation_expr", Py::String(m.rotation_expr));
d.setItem("translation_expr", Py::String(m.translation_expr));
return d;
}
Py::Dict simToDict(const KCSolve::SimulationParams& s)
{
Py::Dict d;
d.setItem("t_start", Py::Float(s.t_start));
d.setItem("t_end", Py::Float(s.t_end));
d.setItem("h_out", Py::Float(s.h_out));
d.setItem("h_min", Py::Float(s.h_min));
d.setItem("h_max", Py::Float(s.h_max));
d.setItem("error_tol", Py::Float(s.error_tol));
return d;
}
} // anonymous namespace
// returns a string which represents the object e.g. when printed in python
std::string AssemblyObjectPy::representation() const
{
@@ -243,3 +391,52 @@ PyObject* AssemblyObjectPy::getDownstreamParts(PyObject* args) const
return Py::new_reference_to(ret);
}
PyObject* AssemblyObjectPy::getSolveContext(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
PY_TRY
{
KCSolve::SolveContext ctx = getAssemblyObjectPtr()->getSolveContext();
// Empty context (no grounded parts) → return empty dict
if (ctx.parts.empty()) {
return Py::new_reference_to(Py::Dict());
}
Py::Dict d;
d.setItem("api_version", Py::Long(KCSolve::API_VERSION_MAJOR));
Py::List parts;
for (const auto& p : ctx.parts) {
parts.append(partToDict(p));
}
d.setItem("parts", parts);
Py::List constraints;
for (const auto& c : ctx.constraints) {
constraints.append(constraintToDict(c));
}
d.setItem("constraints", constraints);
Py::List motions;
for (const auto& m : ctx.motions) {
motions.append(motionToDict(m));
}
d.setItem("motions", motions);
if (ctx.simulation.has_value()) {
d.setItem("simulation", simToDict(*ctx.simulation));
}
else {
d.setItem("simulation", Py::None());
}
d.setItem("bundle_fixed", Py::Boolean(ctx.bundle_fixed));
return Py::new_reference_to(d);
}
PY_CATCH;
}