Merge pull request 'feat(solver): pack SolveContext into .kc archives on save (#289 phase 3d)' (#302) from feat/solver-context-packing into main
All checks were successful
Build and Test / build (push) Successful in 30m10s
All checks were successful
Build and Test / build (push) Successful in 30m10s
Reviewed-on: #302
This commit was merged in pull request #302.
This commit is contained in:
@@ -152,6 +152,22 @@ KCSolve::IKCSolver* AssemblyObject::getOrCreateSolver()
|
||||
return solver_.get();
|
||||
}
|
||||
|
||||
KCSolve::SolveContext AssemblyObject::getSolveContext()
|
||||
{
|
||||
partIdToObjs_.clear();
|
||||
objToPartId_.clear();
|
||||
|
||||
auto groundedObjs = getGroundedParts();
|
||||
if (groundedObjs.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<App::DocumentObject*> joints = getJoints(false);
|
||||
removeUnconnectedJoints(joints, groundedObjs);
|
||||
|
||||
return buildSolveContext(joints);
|
||||
}
|
||||
|
||||
int AssemblyObject::solve(bool enableRedo, bool updateJCS)
|
||||
{
|
||||
ensureIdentityPlacements();
|
||||
|
||||
@@ -102,6 +102,10 @@ public:
|
||||
|
||||
void exportAsASMT(std::string fileName);
|
||||
|
||||
/// Build the assembly constraint graph without solving.
|
||||
/// Returns an empty SolveContext if no parts are grounded.
|
||||
KCSolve::SolveContext getSolveContext();
|
||||
|
||||
bool validateNewPlacements();
|
||||
void setNewPlacements();
|
||||
static void redrawJointPlacements(std::vector<App::DocumentObject*> joints);
|
||||
|
||||
@@ -4,10 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from Base.Metadata import constmethod, export
|
||||
|
||||
from App.Part import Part
|
||||
from App.DocumentObject import DocumentObject
|
||||
from App.Part import Part
|
||||
from Base.Metadata import constmethod, export
|
||||
|
||||
@export(Include="Mod/Assembly/App/AssemblyObject.h", Namespace="Assembly")
|
||||
class AssemblyObject(Part):
|
||||
@@ -119,7 +118,9 @@ class AssemblyObject(Part):
|
||||
...
|
||||
|
||||
@constmethod
|
||||
def isJointConnectingPartToGround(self, joint: DocumentObject, prop_name: str, /) -> Any:
|
||||
def isJointConnectingPartToGround(
|
||||
self, joint: DocumentObject, prop_name: str, /
|
||||
) -> Any:
|
||||
"""
|
||||
Check if a joint is connecting a part to the ground.
|
||||
|
||||
@@ -153,6 +154,16 @@ class AssemblyObject(Part):
|
||||
"""
|
||||
...
|
||||
|
||||
@constmethod
|
||||
def getSolveContext(self) -> dict:
|
||||
"""Build the assembly constraint graph as a serializable dict.
|
||||
|
||||
Returns a dict matching kcsolve.SolveContext.to_dict() format,
|
||||
or an empty dict if the assembly has no grounded parts.
|
||||
Does NOT trigger a solve.
|
||||
"""
|
||||
...
|
||||
|
||||
@constmethod
|
||||
def getDownstreamParts(
|
||||
self, start_part: DocumentObject, joint_to_ignore: DocumentObject, /
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,24 @@ def _manifest_enrich_hook(doc, filename, entries):
|
||||
register_pre_reinject(_manifest_enrich_hook)
|
||||
|
||||
|
||||
def _solver_context_hook(doc, filename, entries):
|
||||
"""Pack solver context into silo/solver/context.json for assemblies."""
|
||||
try:
|
||||
for obj in doc.Objects:
|
||||
if obj.TypeId == "Assembly::AssemblyObject":
|
||||
ctx = obj.getSolveContext()
|
||||
if ctx: # non-empty means we have grounded parts
|
||||
entries["silo/solver/context.json"] = (
|
||||
json.dumps(ctx, indent=2) + "\n"
|
||||
).encode("utf-8")
|
||||
break # one assembly per document
|
||||
except Exception as exc:
|
||||
FreeCAD.Console.PrintWarning(f"kc_format: solver context hook failed: {exc}\n")
|
||||
|
||||
|
||||
register_pre_reinject(_solver_context_hook)
|
||||
|
||||
|
||||
KC_VERSION = "1.0"
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ _KNOWN_ENTRIES = [
|
||||
"Dependencies",
|
||||
("links", lambda v: isinstance(v, list) and len(v) > 0),
|
||||
),
|
||||
(
|
||||
"silo/solver/context.json",
|
||||
"SiloSolverContext",
|
||||
"Solver Context",
|
||||
("parts", lambda v: isinstance(v, list) and len(v) > 0),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user