Files
create/src/Mod/Assembly/App/AssemblyObjectPyImp.cpp
forbes 4cf54caf7b
All checks were successful
Build and Test / build (pull_request) Successful in 29m51s
feat(solver): pack SolveContext into .kc archives on save (#289 phase 3d)
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
2026-02-20 17:12:25 -06:00

443 lines
14 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Ondsel <development@ondsel.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/>. *
* *
***************************************************************************/
// 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
{
return {"<Assembly object>"};
}
PyObject* AssemblyObjectPy::getCustomAttributes(const char* /*attr*/) const
{
return nullptr;
}
int AssemblyObjectPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
{
return 0;
}
PyObject* AssemblyObjectPy::solve(PyObject* args) const
{
PyObject* enableUndoPy;
bool enableUndo;
if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &enableUndoPy)) {
PyErr_Clear();
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
else {
enableUndo = false;
}
}
else {
enableUndo = Base::asBoolean(enableUndoPy);
}
int ret = this->getAssemblyObjectPtr()->solve(enableUndo);
return Py_BuildValue("i", ret);
}
PyObject* AssemblyObjectPy::generateSimulation(PyObject* args) const
{
PyObject* pyobj;
if (!PyArg_ParseTuple(args, "O", &pyobj)) {
return nullptr;
}
auto* obj = static_cast<App::DocumentObjectPy*>(pyobj)->getDocumentObjectPtr();
int ret = this->getAssemblyObjectPtr()->generateSimulation(obj);
return Py_BuildValue("i", ret);
}
PyObject* AssemblyObjectPy::ensureIdentityPlacements(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
this->getAssemblyObjectPtr()->ensureIdentityPlacements();
Py_Return;
}
PyObject* AssemblyObjectPy::updateForFrame(PyObject* args) const
{
unsigned long index {};
if (!PyArg_ParseTuple(args, "k", &index)) {
throw Py::RuntimeError("updateForFrame requires an integer index");
}
PY_TRY
{
this->getAssemblyObjectPtr()->updateForFrame(index);
}
PY_CATCH;
Py_Return;
}
PyObject* AssemblyObjectPy::numberOfFrames(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
size_t ret = this->getAssemblyObjectPtr()->numberOfFrames();
return Py_BuildValue("k", ret);
}
PyObject* AssemblyObjectPy::updateSolveStatus(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
this->getAssemblyObjectPtr()->updateSolveStatus();
Py_Return;
}
PyObject* AssemblyObjectPy::undoSolve(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
this->getAssemblyObjectPtr()->undoSolve();
Py_Return;
}
PyObject* AssemblyObjectPy::clearUndo(PyObject* args) const
{
if (!PyArg_ParseTuple(args, "")) {
return nullptr;
}
this->getAssemblyObjectPtr()->clearUndo();
Py_Return;
}
PyObject* AssemblyObjectPy::isPartConnected(PyObject* args) const
{
PyObject* pyobj;
if (!PyArg_ParseTuple(args, "O!", &(App::DocumentObjectPy::Type), &pyobj)) {
return nullptr;
}
auto* obj = static_cast<App::DocumentObjectPy*>(pyobj)->getDocumentObjectPtr();
bool ok = this->getAssemblyObjectPtr()->isPartConnected(obj);
return Py_BuildValue("O", (ok ? Py_True : Py_False));
}
PyObject* AssemblyObjectPy::isPartGrounded(PyObject* args) const
{
PyObject* pyobj;
if (!PyArg_ParseTuple(args, "O!", &(App::DocumentObjectPy::Type), &pyobj)) {
return nullptr;
}
auto* obj = static_cast<App::DocumentObjectPy*>(pyobj)->getDocumentObjectPtr();
bool ok = this->getAssemblyObjectPtr()->isPartGrounded(obj);
return Py_BuildValue("O", (ok ? Py_True : Py_False));
}
PyObject* AssemblyObjectPy::isJointConnectingPartToGround(PyObject* args) const
{
PyObject* pyobj;
char* pname;
if (!PyArg_ParseTuple(args, "O!s", &(App::DocumentObjectPy::Type), &pyobj, &pname)) {
return nullptr;
}
auto* obj = static_cast<App::DocumentObjectPy*>(pyobj)->getDocumentObjectPtr();
bool ok = this->getAssemblyObjectPtr()->isJointConnectingPartToGround(obj, pname);
return Py_BuildValue("O", (ok ? Py_True : Py_False));
}
PyObject* AssemblyObjectPy::exportAsASMT(PyObject* args) const
{
char* utf8Name;
if (!PyArg_ParseTuple(args, "et", "utf-8", &utf8Name)) {
return nullptr;
}
std::string fileName = utf8Name;
PyMem_Free(utf8Name);
if (fileName.empty()) {
PyErr_SetString(PyExc_ValueError, "Passed string is empty");
return nullptr;
}
this->getAssemblyObjectPtr()->exportAsASMT(fileName);
Py_Return;
}
Py::List AssemblyObjectPy::getJoints() const
{
Py::List ret;
std::vector<App::DocumentObject*> list = getAssemblyObjectPtr()->getJoints(false);
for (auto It : list) {
ret.append(Py::Object(It->getPyObject(), true));
}
return ret;
}
PyObject* AssemblyObjectPy::getDownstreamParts(PyObject* args) const
{
PyObject* pyPart;
PyObject* pyJoint;
// Parse the two arguments: a part object and a joint object
if (!PyArg_ParseTuple(
args,
"O!O!",
&(App::DocumentObjectPy::Type),
&pyPart,
&(App::DocumentObjectPy::Type),
&pyJoint
)) {
return nullptr;
}
auto* part = static_cast<App::DocumentObjectPy*>(pyPart)->getDocumentObjectPtr();
auto* joint = static_cast<App::DocumentObjectPy*>(pyJoint)->getDocumentObjectPtr();
// Call the C++ method
std::vector<Assembly::ObjRef> downstreamParts
= this->getAssemblyObjectPtr()->getDownstreamParts(part, joint);
// Convert the result into a Python list of DocumentObjects
Py::List ret;
for (const auto& objRef : downstreamParts) {
if (objRef.obj) {
ret.append(Py::Object(objRef.obj->getPyObject(), true));
}
}
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;
}