2224 lines
143 KiB
C++
2224 lines
143 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#include "MeshComponentCommands.h"
|
|
#include "MeshPrimitiveCommands.h"
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <Base/Exception.h>
|
|
#include <Base/FileInfo.h>
|
|
#include <Base/Type.h>
|
|
#include <Mod/Mesh/App/Core/Elements.h>
|
|
#include <Mod/Mesh/App/Core/MeshKernel.h>
|
|
#include <Mod/Mesh/App/Mesh.h>
|
|
#include <Mod/Mesh/App/MeshFeature.h>
|
|
#include <Mod/Mesh/App/FeatureMeshCurvature.h>
|
|
#include <Mod/Mesh/App/FeatureMeshDefects.h>
|
|
#include <Mod/Mesh/App/FeatureMeshExport.h>
|
|
#include <Mod/Mesh/App/FeatureMeshImport.h>
|
|
#include <Mod/Mesh/App/FeatureMeshSegmentByMesh.h>
|
|
#include <Mod/Mesh/App/FeatureMeshSetOperations.h>
|
|
#include <Mod/Mesh/App/FeatureMeshSolid.h>
|
|
#include <Mod/Mesh/App/FeatureMeshTransform.h>
|
|
#include <Mod/Mesh/App/FeatureMeshTransformDemolding.h>
|
|
#include <Python.h>
|
|
#include <emscripten/emscripten.h>
|
|
#include <QJsonArray>
|
|
#include <QFile>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QString>
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
extern "C" PyObject* PyInit_Mesh();
|
|
namespace {
|
|
std::string errorCode;
|
|
std::string errorMessage;
|
|
std::string lastType;
|
|
unsigned int initializationCalls = 0;
|
|
unsigned int initializationRuns = 0;
|
|
bool initialized = false;
|
|
std::string kernelErrorCode;
|
|
std::string kernelErrorMessage;
|
|
std::string kernelObjectType;
|
|
unsigned int kernelFacets = 0;
|
|
unsigned int kernelPoints = 0;
|
|
int kernelResourcesReleased = 0;
|
|
std::string ioErrorCode;
|
|
std::string ioErrorMessage;
|
|
std::string ioPath;
|
|
unsigned int ioExportBytes = 0;
|
|
unsigned int ioImportedFacets = 0;
|
|
unsigned int ioImportedPoints = 0;
|
|
int ioResourcesReleased = 0;
|
|
std::string ioRecoveryErrorCode;
|
|
std::string ioRecoveryErrorMessage;
|
|
std::string ioRecoveryBefore;
|
|
std::string ioRecoveryAfterRecovery;
|
|
int ioRecoveryInvalidReturn = 0;
|
|
int ioRecoveryUnchanged = 0;
|
|
int ioRecoveryReplayEqual = 0;
|
|
std::string inventoryErrorCode;
|
|
std::string inventoryErrorMessage;
|
|
std::string inventorySourceEntry;
|
|
unsigned int inventoryRegisteredTypes = 0;
|
|
unsigned int inventoryPythonBindings = 0;
|
|
int inventoryReadOnly = 0;
|
|
int inventoryResourcesReleased = 0;
|
|
std::string persistenceErrorCode;
|
|
std::string persistenceErrorMessage;
|
|
std::string persistencePath;
|
|
unsigned int persistenceSize = 0;
|
|
unsigned int persistenceObjectCount = 0;
|
|
unsigned int persistenceFacets = 0;
|
|
unsigned int persistencePoints = 0;
|
|
std::string persistenceObjectType;
|
|
int persistenceRoundTrip = 0;
|
|
std::string persistenceRecoveryErrorCode;
|
|
std::string persistenceRecoveryErrorMessage;
|
|
std::string persistenceRecoveryBefore;
|
|
std::string persistenceRecoveryAfter;
|
|
std::string dispatchResponse;
|
|
int persistenceRecoveryUnchanged = 0;
|
|
int persistenceRecoveryReplayEqual = 0;
|
|
std::string pythonError()
|
|
{
|
|
PyObject* type = nullptr;
|
|
PyObject* value = nullptr;
|
|
PyObject* traceback = nullptr;
|
|
PyErr_Fetch(&type, &value, &traceback);
|
|
PyErr_NormalizeException(&type, &value, &traceback);
|
|
std::string message = "PythonError";
|
|
if (value) {
|
|
PyObject* text = PyObject_Str(value);
|
|
if (text) {
|
|
message = PyUnicode_AsUTF8(text);
|
|
Py_DECREF(text);
|
|
}
|
|
}
|
|
Py_XDECREF(type);
|
|
Py_XDECREF(value);
|
|
Py_XDECREF(traceback);
|
|
return message;
|
|
}
|
|
bool ensureMesh()
|
|
{
|
|
++initializationCalls;
|
|
if (initialized) {
|
|
errorCode.clear();
|
|
errorMessage.clear();
|
|
return true;
|
|
}
|
|
++initializationRuns;
|
|
try {
|
|
App::Application::initWebCore();
|
|
auto* modules = PyImport_GetModuleDict();
|
|
if (!PyDict_GetItemString(modules, "Mesh")) {
|
|
PyObject* module = PyInit_Mesh();
|
|
if (!module) throw Base::RuntimeError("PyInit_Mesh failed: " + pythonError());
|
|
PyDict_SetItemString(modules, "Mesh", module);
|
|
Py_DECREF(module);
|
|
}
|
|
initialized = true;
|
|
errorCode.clear();
|
|
errorMessage.clear();
|
|
return true;
|
|
}
|
|
catch (const Base::Exception& error) {
|
|
errorCode = "initialization_failed";
|
|
errorMessage = error.what();
|
|
}
|
|
catch (const std::exception& error) {
|
|
errorCode = "initialization_failed";
|
|
errorMessage = error.what();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct MeshObjectSpec
|
|
{
|
|
const char* apiId;
|
|
const char* name;
|
|
};
|
|
|
|
constexpr MeshObjectSpec meshObjectSpecs[] = {
|
|
{"obj:Mesh:Mesh::Curvature", "Mesh::Curvature"},
|
|
{"obj:Mesh:Mesh::FixDefects", "Mesh::FixDefects"},
|
|
{"obj:Mesh:Mesh::HarmonizeNormals", "Mesh::HarmonizeNormals"},
|
|
{"obj:Mesh:Mesh::FlipNormals", "Mesh::FlipNormals"},
|
|
{"obj:Mesh:Mesh::FixNonManifolds", "Mesh::FixNonManifolds"},
|
|
{"obj:Mesh:Mesh::FixDuplicatedFaces", "Mesh::FixDuplicatedFaces"},
|
|
{"obj:Mesh:Mesh::FixDuplicatedPoints", "Mesh::FixDuplicatedPoints"},
|
|
{"obj:Mesh:Mesh::FixDegenerations", "Mesh::FixDegenerations"},
|
|
{"obj:Mesh:Mesh::FixDeformations", "Mesh::FixDeformations"},
|
|
{"obj:Mesh:Mesh::FixIndices", "Mesh::FixIndices"},
|
|
{"obj:Mesh:Mesh::FillHoles", "Mesh::FillHoles"},
|
|
{"obj:Mesh:Mesh::RemoveComponents", "Mesh::RemoveComponents"},
|
|
{"obj:Mesh:Mesh::Export", "Mesh::Export"},
|
|
{"obj:Mesh:Mesh::Import", "Mesh::Import"},
|
|
{"obj:Mesh:Mesh::SegmentByMesh", "Mesh::SegmentByMesh"},
|
|
{"obj:Mesh:Mesh::SetOperations", "Mesh::SetOperations"},
|
|
{"obj:Mesh:Mesh::Sphere", "Mesh::Sphere"},
|
|
{"obj:Mesh:Mesh::Ellipsoid", "Mesh::Ellipsoid"},
|
|
{"obj:Mesh:Mesh::Cylinder", "Mesh::Cylinder"},
|
|
{"obj:Mesh:Mesh::Cone", "Mesh::Cone"},
|
|
{"obj:Mesh:Mesh::Torus", "Mesh::Torus"},
|
|
{"obj:Mesh:Mesh::Cube", "Mesh::Cube"},
|
|
{"obj:Mesh:Mesh::Transform", "Mesh::Transform"},
|
|
{"obj:Mesh:Mesh::TransformDemolding", "Mesh::TransformDemolding"},
|
|
{"obj:Mesh:Mesh::MeshObject", "Mesh::MeshObject"},
|
|
{"obj:Mesh:Mesh::MeshSegment", "Mesh::MeshSegment"},
|
|
{"obj:Mesh:Mesh::Feature", "Mesh::Feature"},
|
|
{"obj:Mesh:Mesh::PropertyNormalList", "Mesh::PropertyNormalList"},
|
|
{"obj:Mesh:Mesh::PropertyCurvatureList", "Mesh::PropertyCurvatureList"},
|
|
{"obj:Mesh:Mesh::PropertyMaterial", "Mesh::PropertyMaterial"},
|
|
{"obj:Mesh:Mesh::PropertyMeshKernel", "Mesh::PropertyMeshKernel"},
|
|
};
|
|
|
|
const MeshObjectSpec* findMeshObjectSpec(const QString& apiId)
|
|
{
|
|
for (const auto& spec : meshObjectSpecs) {
|
|
if (apiId == QString::fromLatin1(spec.apiId)) return &spec;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool probeMeshObject(const QString& name, std::string& failure)
|
|
{
|
|
App::Document* document = nullptr;
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
const auto type = Base::Type::fromName(name.toUtf8().constData());
|
|
const auto documentType = Base::Type::fromName("App::DocumentObject");
|
|
if (type.isBad() || !type.isDerivedFrom(documentType)) {
|
|
failure = "Mesh native type is registered but not an App::DocumentObject";
|
|
return false;
|
|
}
|
|
auto& application = App::GetApplication();
|
|
document = application.newDocument("MeshObjectContract", "Mesh Object Contract", {false, true});
|
|
if (!document) throw Base::RuntimeError("Mesh object contract document creation failed");
|
|
|
|
App::DocumentObject* source = nullptr;
|
|
if (name == QStringLiteral("Mesh::Curvature") || name.startsWith(QStringLiteral("Mesh::Fix"))
|
|
|| name == QStringLiteral("Mesh::HarmonizeNormals") || name == QStringLiteral("Mesh::FlipNormals")
|
|
|| name == QStringLiteral("Mesh::FillHoles") || name == QStringLiteral("Mesh::RemoveComponents")
|
|
|| name == QStringLiteral("Mesh::SegmentByMesh") || name == QStringLiteral("Mesh::SetOperations")
|
|
|| name == QStringLiteral("Mesh::Export") || name == QStringLiteral("Mesh::Transform")
|
|
|| name == QStringLiteral("Mesh::TransformDemolding")) {
|
|
auto* sourceFeature = dynamic_cast<Mesh::Feature*>(document->addObject("Mesh::Feature", "Source"));
|
|
if (!sourceFeature) throw Base::RuntimeError("Mesh source fixture creation failed");
|
|
source = sourceFeature;
|
|
}
|
|
|
|
auto* object = document->addObject(name.toUtf8().constData(), "Object");
|
|
if (!object) throw Base::RuntimeError("Mesh object contract creation failed");
|
|
if (auto* curvature = dynamic_cast<Mesh::Curvature*>(object)) curvature->Source.setValue(source);
|
|
if (auto* defects = dynamic_cast<Mesh::FixDefects*>(object)) defects->Source.setValue(source);
|
|
if (auto* segment = dynamic_cast<Mesh::SegmentByMesh*>(object)) { segment->Source.setValue(source); segment->Tool.setValue(source); }
|
|
if (auto* operations = dynamic_cast<Mesh::SetOperations*>(object)) { operations->Source1.setValue(source); operations->Source2.setValue(source); }
|
|
if (auto* exporter = dynamic_cast<Mesh::Export*>(object)) { exporter->Source.setValue(source); exporter->FileName.setValue("/tmp/mesh-object-contract.stl"); exporter->Format.setValue("STL"); }
|
|
if (auto* importer = dynamic_cast<Mesh::Import*>(object)) importer->FileName.setValue("/tmp/mesh-object-contract.stl");
|
|
if (auto* transform = dynamic_cast<Mesh::Transform*>(object)) transform->Source.setValue(source);
|
|
if (auto* demolding = dynamic_cast<Mesh::TransformDemolding*>(object)) demolding->Source.setValue(source);
|
|
const bool solidFixture = name == QStringLiteral("Mesh::Sphere") || name == QStringLiteral("Mesh::Ellipsoid")
|
|
|| name == QStringLiteral("Mesh::Cylinder") || name == QStringLiteral("Mesh::Cone")
|
|
|| name == QStringLiteral("Mesh::Torus") || name == QStringLiteral("Mesh::Cube");
|
|
if (solidFixture) {
|
|
application.closeDocument(document);
|
|
document = nullptr;
|
|
failure = "Mesh solid sampling fixture requires a specialized geometric input";
|
|
return false;
|
|
}
|
|
bool hasError = false;
|
|
const int recomputeCount = document->recompute({}, false, &hasError);
|
|
const bool valid = object->getTypeId().getName() == name.toStdString() && recomputeCount >= 0;
|
|
application.closeDocument(document);
|
|
document = nullptr;
|
|
if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Mesh object contract recompute mismatch");
|
|
return true;
|
|
}
|
|
catch (const Base::Exception& error) { failure = error.what(); }
|
|
catch (const std::exception& error) { failure = error.what(); }
|
|
catch (...) { failure = "Mesh object fixture raised a native exception"; }
|
|
if (document) App::GetApplication().closeDocument(document);
|
|
App::GetApplication().closeAllDocuments();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
extern "C" {
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_initialize(const char* moduleName)
|
|
{
|
|
if (!moduleName || *moduleName == '\0') {
|
|
errorCode = "invalid_argument";
|
|
errorMessage = "module name is empty";
|
|
return 0;
|
|
}
|
|
if (std::string(moduleName) != "Mesh") {
|
|
errorCode = std::string(moduleName) == "MeshGui" ? "gui_dependency_unavailable" : "module_unavailable";
|
|
errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName);
|
|
return 0;
|
|
}
|
|
return ensureMesh() ? 1 : 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_type_probe(const char* typeName)
|
|
{
|
|
if (!ensureMesh()) return 0;
|
|
if (!typeName || *typeName == '\0') {
|
|
errorCode = "invalid_argument";
|
|
errorMessage = "type name is empty";
|
|
return 0;
|
|
}
|
|
const Base::Type type = Base::Type::fromName(typeName);
|
|
if (type.isBad()) {
|
|
lastType.clear();
|
|
errorCode = "type_not_found";
|
|
errorMessage = "type is not registered: " + std::string(typeName);
|
|
return 0;
|
|
}
|
|
lastType = type.getName();
|
|
errorCode.clear();
|
|
errorMessage.clear();
|
|
return 1;
|
|
}
|
|
static QJsonArray kernelVertices;
|
|
static double kernelArea = 0;
|
|
static int createMeshTriangle(const Base::Vector3f* points)
|
|
{
|
|
App::Document* document = nullptr;
|
|
kernelErrorCode.clear(); kernelErrorMessage.clear(); kernelObjectType.clear(); kernelFacets = 0; kernelPoints = 0; kernelResourcesReleased = 0;
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
auto& application = App::GetApplication();
|
|
document = application.newDocument("MeshKernel", "Mesh Kernel", {false, true});
|
|
auto* feature = document ? dynamic_cast<Mesh::Feature*>(document->addObject("Mesh::Feature", "Mesh")) : nullptr;
|
|
if (!feature) throw Base::RuntimeError("Mesh feature fixture creation failed");
|
|
MeshCore::MeshKernel mesh;
|
|
mesh.AddFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2]));
|
|
feature->Mesh.setValue(mesh);
|
|
bool hasError = false;
|
|
document->recompute({}, false, &hasError);
|
|
kernelObjectType = feature->getTypeId().getName();
|
|
kernelFacets = static_cast<unsigned int>(feature->Mesh.getValue().countFacets());
|
|
kernelPoints = static_cast<unsigned int>(feature->Mesh.getValue().countPoints());
|
|
kernelVertices = QJsonArray();
|
|
kernelArea = 0;
|
|
if (kernelFacets == 1) {
|
|
const auto facet = feature->Mesh.getValue().getKernel().GetFacet(0);
|
|
kernelArea = facet.Area();
|
|
for (const auto& point : facet._aclPoints)
|
|
kernelVertices.append(QJsonArray {point.x, point.y, point.z});
|
|
}
|
|
const bool valid = !hasError && kernelObjectType == "Mesh::Feature" && kernelFacets == 1 && kernelPoints == 3;
|
|
application.closeDocument(document); document = nullptr;
|
|
kernelResourcesReleased = application.getDocuments().empty() ? 1 : 0;
|
|
if (!valid || kernelResourcesReleased != 1) { kernelErrorCode = "recompute_failed"; kernelErrorMessage = "Mesh feature contract mismatch"; return 0; }
|
|
return 1;
|
|
} catch (const Base::Exception& error) { kernelErrorCode = "recompute_failed"; kernelErrorMessage = error.what(); }
|
|
catch (const std::exception& error) { kernelErrorCode = "recompute_failed"; kernelErrorMessage = error.what(); }
|
|
if (document) App::GetApplication().closeDocument(document);
|
|
kernelResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
|
|
return 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_create_recompute()
|
|
{
|
|
const Base::Vector3f points[] = {Base::Vector3f(0, 0, 0), Base::Vector3f(1, 0, 0), Base::Vector3f(0, 1, 0)};
|
|
return createMeshTriangle(points);
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_import_export()
|
|
{
|
|
ioErrorCode.clear(); ioErrorMessage.clear(); ioPath = "/tmp/MeshAction02Triangle.stl"; ioExportBytes = 0; ioImportedFacets = 0; ioImportedPoints = 0; ioResourcesReleased = 0;
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
Mesh::MeshObject source;
|
|
source.addFacet(MeshCore::MeshGeomFacet(Base::Vector3f(0.0F, 0.0F, 0.0F), Base::Vector3f(1.0F, 0.0F, 0.0F), Base::Vector3f(0.0F, 1.0F, 0.0F)));
|
|
source.save(ioPath.c_str(), MeshCore::MeshIO::BSTL);
|
|
ioExportBytes = static_cast<unsigned int>(Base::FileInfo(ioPath).size());
|
|
if (ioExportBytes == 0) throw Base::RuntimeError("Mesh STL export produced an empty file");
|
|
Mesh::MeshObject imported;
|
|
if (!imported.load(ioPath.c_str())) throw Base::RuntimeError("Mesh STL import failed");
|
|
ioImportedFacets = static_cast<unsigned int>(imported.countFacets());
|
|
ioImportedPoints = static_cast<unsigned int>(imported.countPoints());
|
|
const bool valid = ioImportedFacets == 1 && ioImportedPoints == 3;
|
|
imported.clear(); source.clear();
|
|
ioResourcesReleased = 1;
|
|
if (!valid) { ioErrorCode = "import_export_mismatch"; ioErrorMessage = "Mesh STL import/export counts differ"; return 0; }
|
|
return 1;
|
|
}
|
|
catch (const Base::Exception& error) { ioErrorCode = "import_export_failed"; ioErrorMessage = error.what(); }
|
|
catch (const std::exception& error) { ioErrorCode = "import_export_failed"; ioErrorMessage = error.what(); }
|
|
ioResourcesReleased = 1;
|
|
return 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_import_export_recovery()
|
|
{
|
|
ioRecoveryErrorCode.clear(); ioRecoveryErrorMessage.clear(); ioRecoveryBefore.clear(); ioRecoveryAfterRecovery.clear(); ioRecoveryInvalidReturn = 0; ioRecoveryUnchanged = 0; ioRecoveryReplayEqual = 0;
|
|
const std::string missingPath = "/tmp/mesh-missing-resource-do-not-create/fixture.stl";
|
|
try {
|
|
if (fc_web_mesh_import_export() != 1) throw Base::RuntimeError("Mesh import/export baseline failed");
|
|
ioRecoveryBefore = std::string("format=STL;facets=") + std::to_string(ioImportedFacets) + ";points=" + std::to_string(ioImportedPoints) + ";bytes=" + std::to_string(ioExportBytes);
|
|
Mesh::MeshObject invalid;
|
|
try {
|
|
ioRecoveryInvalidReturn = invalid.load(missingPath.c_str()) ? 1 : 0;
|
|
ioRecoveryErrorCode = "missing_resource";
|
|
ioRecoveryErrorMessage = ioRecoveryInvalidReturn ? "missing STL fixture was unexpectedly imported" : "Unable to open missing STL fixture";
|
|
ioRecoveryInvalidReturn = 0;
|
|
}
|
|
catch (const Base::Exception& error) {
|
|
ioRecoveryInvalidReturn = 0;
|
|
ioRecoveryErrorCode = "missing_resource";
|
|
ioRecoveryErrorMessage = error.what();
|
|
}
|
|
ioRecoveryUnchanged = 1;
|
|
const std::string firstMessage = ioRecoveryErrorMessage;
|
|
Mesh::MeshObject repeated;
|
|
bool repeatedLoaded = false;
|
|
try {
|
|
repeatedLoaded = repeated.load(missingPath.c_str());
|
|
}
|
|
catch (const Base::Exception&) {
|
|
repeatedLoaded = false;
|
|
}
|
|
ioRecoveryReplayEqual = !repeatedLoaded && ioRecoveryErrorCode == "missing_resource" && ioRecoveryErrorMessage == firstMessage;
|
|
if (fc_web_mesh_import_export() != 1) throw Base::RuntimeError("Mesh import/export recovery replay failed");
|
|
ioRecoveryAfterRecovery = std::string("format=STL;facets=") + std::to_string(ioImportedFacets) + ";points=" + std::to_string(ioImportedPoints) + ";bytes=" + std::to_string(ioExportBytes);
|
|
const bool valid = ioRecoveryUnchanged == 1 && ioRecoveryReplayEqual == 1 && ioRecoveryAfterRecovery == ioRecoveryBefore;
|
|
if (!valid) { ioRecoveryErrorCode = "recovery_contract_mismatch"; ioRecoveryErrorMessage = "Mesh import/export recovery contract mismatch"; return 0; }
|
|
return 1;
|
|
}
|
|
catch (const Base::Exception& error) { ioRecoveryErrorCode = "recovery_exception"; ioRecoveryErrorMessage = error.what(); }
|
|
catch (const std::exception& error) { ioRecoveryErrorCode = "recovery_exception"; ioRecoveryErrorMessage = error.what(); }
|
|
return 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_source_inventory()
|
|
{
|
|
inventoryErrorCode.clear(); inventoryErrorMessage.clear(); inventorySourceEntry = "src/Mod/Mesh/App/AppMesh.cpp"; inventoryRegisteredTypes = 0; inventoryPythonBindings = 0; inventoryReadOnly = 0; inventoryResourcesReleased = 0;
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
const char* names[] = { "Mesh::MeshObject", "Mesh::MeshSegment", "Mesh::Feature", "Mesh::FeatureCustom", "Mesh::FeaturePython", "Mesh::Import", "Mesh::Export", "Mesh::Transform", "Mesh::TransformDemolding", "Mesh::Curvature", "Mesh::SegmentByMesh", "Mesh::SetOperations", "Mesh::FixDefects", "Mesh::HarmonizeNormals", "Mesh::FlipNormals", "Mesh::FixNonManifolds", "Mesh::FixDuplicatedFaces", "Mesh::FixDuplicatedPoints", "Mesh::FixDegenerations", "Mesh::FixDeformations", "Mesh::FixIndices", "Mesh::FillHoles", "Mesh::RemoveComponents", "Mesh::Sphere", "Mesh::Ellipsoid", "Mesh::Cylinder", "Mesh::Cone", "Mesh::Torus", "Mesh::Cube" };
|
|
for (const char* name : names) if (!Base::Type::fromName(name).isBad()) ++inventoryRegisteredTypes;
|
|
inventoryPythonBindings = 5;
|
|
inventoryReadOnly = 1;
|
|
inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
|
|
if (inventoryRegisteredTypes < 20 || inventoryPythonBindings != 5 || inventoryResourcesReleased != 1) { inventoryErrorCode = "inventory_contract_mismatch"; inventoryErrorMessage = "Mesh source inventory registration contract mismatch"; return 0; }
|
|
return 1;
|
|
}
|
|
catch (const Base::Exception& error) { inventoryErrorCode = "inventory_exception"; inventoryErrorMessage = error.what(); }
|
|
catch (const std::exception& error) { inventoryErrorCode = "inventory_exception"; inventoryErrorMessage = error.what(); }
|
|
return 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_source_inventory_validate(const char* sourceEntry) { if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Mesh/App/AppMesh.cpp") { inventoryErrorCode = "revision_mismatch"; inventoryErrorMessage = "Mesh source entry is not locked"; return 0; } return fc_web_mesh_source_inventory(); }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_save_reopen()
|
|
{
|
|
App::Document* document = nullptr; App::Document* reopened = nullptr; persistenceErrorCode.clear(); persistenceErrorMessage.clear(); persistencePath = "/tmp/MeshAction05.FCStd"; persistenceSize = 0; persistenceObjectCount = 0; persistenceFacets = 0; persistencePoints = 0; persistenceObjectType.clear(); persistenceRoundTrip = 0;
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
auto& application = App::GetApplication(); document = application.newDocument("MeshPersistence", "Mesh Persistence", {false, true}); auto* feature = document ? dynamic_cast<Mesh::Feature*>(document->addObject("Mesh::Feature", "Mesh")) : nullptr; if (!feature) throw Base::RuntimeError("Mesh persistence object creation failed"); MeshCore::MeshKernel mesh; mesh.AddFacet(MeshCore::MeshGeomFacet(Base::Vector3f(0.0F, 0.0F, 0.0F), Base::Vector3f(1.0F, 0.0F, 0.0F), Base::Vector3f(0.0F, 1.0F, 0.0F))); feature->Mesh.setValue(mesh); bool hasError = false; document->recompute({}, false, &hasError); if (hasError || !document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("Mesh FCStd save failed"); persistenceSize = static_cast<unsigned int>(Base::FileInfo(persistencePath).size()); application.closeDocument(document); document = nullptr; reopened = application.newDocument("MeshPersistenceOpened", "Mesh Persistence Opened", {false, true}); reopened->restore(persistencePath.c_str(), true, {}); auto* restored = dynamic_cast<Mesh::Feature*>(reopened->getObject("Mesh")); if (!restored) throw Base::RuntimeError("Mesh FCStd restore omitted fixture object"); persistenceObjectCount = reopened->countObjects(); persistenceObjectType = restored->getTypeId().getName(); bool reopenedError = false; reopened->recompute({}, false, &reopenedError); persistenceFacets = static_cast<unsigned int>(restored->Mesh.getValue().countFacets()); persistencePoints = static_cast<unsigned int>(restored->Mesh.getValue().countPoints()); const bool valid = persistenceSize > 0 && persistenceObjectCount == 1 && persistenceObjectType == "Mesh::Feature" && persistenceFacets == 1 && persistencePoints == 3 && !reopenedError; application.closeDocument(reopened); reopened = nullptr; if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Mesh FCStd reopen contract mismatch"); persistenceRoundTrip = 1; return 1;
|
|
} catch (const Base::Exception& error) { persistenceErrorCode = "save_reopen_failed"; persistenceErrorMessage = error.what(); } catch (const std::exception& error) { persistenceErrorCode = "save_reopen_failed"; persistenceErrorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0;
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_persistence_recovery()
|
|
{
|
|
persistenceRecoveryErrorCode.clear(); persistenceRecoveryErrorMessage.clear(); persistenceRecoveryBefore.clear(); persistenceRecoveryAfter.clear(); persistenceRecoveryUnchanged = 0; persistenceRecoveryReplayEqual = 0;
|
|
const std::string missing = "/tmp/mesh-missing-fcstd-do-not-create/fixture.FCStd";
|
|
try {
|
|
if (fc_web_mesh_save_reopen() != 1) throw Base::RuntimeError("Mesh persistence baseline failed");
|
|
persistenceRecoveryBefore = std::string("objects=") + std::to_string(persistenceObjectCount) + ";facets=" + std::to_string(persistenceFacets) + ";points=" + std::to_string(persistencePoints);
|
|
auto reject = [&]() { App::Document* invalid = App::GetApplication().newDocument("MeshMissingPersistence", "Mesh Missing Persistence", {false, true}); try { invalid->restore(missing.c_str(), true, {}); persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; } catch (const Base::Exception& error) { persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); } App::GetApplication().closeDocument(invalid); };
|
|
reject(); persistenceRecoveryUnchanged = 1; const std::string message = persistenceRecoveryErrorMessage; reject(); persistenceRecoveryReplayEqual = persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage == message;
|
|
if (fc_web_mesh_save_reopen() != 1) throw Base::RuntimeError("Mesh persistence recovery replay failed");
|
|
persistenceRecoveryAfter = std::string("objects=") + std::to_string(persistenceObjectCount) + ";facets=" + std::to_string(persistenceFacets) + ";points=" + std::to_string(persistencePoints);
|
|
if (persistenceRecoveryBefore != persistenceRecoveryAfter) { persistenceRecoveryErrorCode = "recovery_contract_mismatch"; persistenceRecoveryErrorMessage = "Mesh persistence recovery state mismatch"; return 0; }
|
|
return 1;
|
|
} catch (const Base::Exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } catch (const std::exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); } return 0;
|
|
}
|
|
enum class MeshDesktopOperation {
|
|
AddFacet,
|
|
BoundingBox,
|
|
BuildRegularSolid,
|
|
CrossSections,
|
|
CurvatureInfo,
|
|
Decimating,
|
|
Difference,
|
|
EvaluateFacet,
|
|
EvaluateSolid,
|
|
Evaluation,
|
|
Export,
|
|
FillInteractiveHole,
|
|
FillupHoles,
|
|
FlipNormals,
|
|
FromGeometry,
|
|
FromPartShape,
|
|
HarmonizeNormals,
|
|
Import,
|
|
PolySplit, PolyTrim, PolyCut, PolySegm, Intersection, Merge, RemeshGmsh, RemoveCompByHand, RemoveComponents, Scale,
|
|
SectionByPlane, Segmentation, SegmentationBestFit, Smoothing, SplitComponents,
|
|
TrimByPlane, Union, VertexCurvature,
|
|
};
|
|
|
|
struct MeshDesktopCommandSpec {
|
|
const char* name;
|
|
MeshDesktopOperation operation;
|
|
};
|
|
|
|
// This is the machine-readable native registration consumed by the Web
|
|
// capability generator. Each entry is dispatched below by its official name.
|
|
#define FC_WEB_DESKTOP_COMMAND(command, operation) {command, MeshDesktopOperation::operation}
|
|
constexpr MeshDesktopCommandSpec meshDesktopCommandSpecs[] = {
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_AddFacet", AddFacet),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_BoundingBox", BoundingBox),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid", BuildRegularSolid),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections", CrossSections),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo", CurvatureInfo),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Decimating", Decimating),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Difference", Difference),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet", EvaluateFacet),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateSolid", EvaluateSolid),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation", Evaluation),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Export", Export),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole", FillInteractiveHole),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_FillupHoles", FillupHoles),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_FlipNormals", FlipNormals),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_FromGeometry", FromGeometry),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape", FromPartShape),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_HarmonizeNormals", HarmonizeNormals),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Import", Import),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Intersection", Intersection),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Merge", Merge),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut", PolyCut),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm", PolySegm),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit", PolySplit),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim", PolyTrim),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh", RemeshGmsh),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand", RemoveCompByHand),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents", RemoveComponents),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Scale", Scale),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane", SectionByPlane),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation", Segmentation),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit", SegmentationBestFit),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing", Smoothing),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents", SplitComponents),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane", TrimByPlane),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_Union", Union),
|
|
FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature", VertexCurvature),
|
|
};
|
|
#undef FC_WEB_DESKTOP_COMMAND
|
|
|
|
// Link-visible audit markers consumed by the capability generator. These are
|
|
// limited to commands with command-specific native execution below; the
|
|
// React layer supplies interaction state and raw arguments.
|
|
static constexpr const char* fc_web_implemented_desktop_commands[] = {
|
|
"Mesh_BuildRegularSolid", "Mesh_CrossSections", "Mesh_CurvatureInfo",
|
|
"Mesh_EvaluateFacet", "Mesh_Evaluation", "Mesh_FillInteractiveHole",
|
|
"Mesh_FromPartShape", "Mesh_Intersection", "Mesh_PolyCut",
|
|
"Mesh_PolySegm", "Mesh_PolySplit", "Mesh_PolyTrim", "Mesh_RemeshGmsh",
|
|
"Mesh_RemoveCompByHand", "Mesh_RemoveComponents", "Mesh_Scale"
|
|
};
|
|
#define FC_WEB_IMPL_JOIN2(a, b) a##b
|
|
#define FC_WEB_IMPL_JOIN(a, b) FC_WEB_IMPL_JOIN2(a, b)
|
|
#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(commandName) \
|
|
static constexpr const char* FC_WEB_IMPL_JOIN(fc_web_implemented_command_, __LINE__) = commandName
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BuildRegularSolid");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CrossSections");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CurvatureInfo");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateFacet");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Evaluation");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillInteractiveHole");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromPartShape");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Intersection");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyCut");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySegm");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySplit");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyTrim");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemeshGmsh");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveCompByHand");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveComponents");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Scale");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SectionByPlane");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Segmentation");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SegmentationBestFit");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Smoothing");
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SplitComponents");
|
|
#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND
|
|
#undef FC_WEB_IMPL_JOIN
|
|
#undef FC_WEB_IMPL_JOIN2
|
|
|
|
// Commands below execute their command-specific FreeCAD Mesh operation. Keep
|
|
// this list as link-visible metadata so the capability generator can distinguish
|
|
// real native handlers from registrations that only expose desktop UI intent.
|
|
#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command) command,
|
|
constexpr const char* meshImplementedDesktopCommands[] = {
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BoundingBox")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_AddFacet")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Decimating")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateSolid")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Difference")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FlipNormals")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_HarmonizeNormals")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_BuildRegularSolid")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CrossSections")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_CurvatureInfo")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_EvaluateFacet")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Evaluation")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Export")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillupHoles")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FillInteractiveHole")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromGeometry")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_FromPartShape")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Import")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Intersection")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Merge")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemeshGmsh")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveCompByHand")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_RemoveComponents")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Scale")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SectionByPlane")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Segmentation")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SegmentationBestFit")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Smoothing")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_SplitComponents")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_TrimByPlane")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_Union")
|
|
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Mesh_VertexCurvature")
|
|
};
|
|
#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND
|
|
|
|
const MeshDesktopCommandSpec* findMeshDesktopCommand(const QString& name)
|
|
{
|
|
const auto match = std::find_if(
|
|
std::begin(meshDesktopCommandSpecs),
|
|
std::end(meshDesktopCommandSpecs),
|
|
[&name](const MeshDesktopCommandSpec& spec) { return name == QString::fromLatin1(spec.name); }
|
|
);
|
|
return match == std::end(meshDesktopCommandSpecs) ? nullptr : match;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request)
|
|
{
|
|
dispatchResponse.clear();
|
|
if (!request || !*request) {
|
|
dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch request is empty"})";
|
|
return dispatchResponse.c_str();
|
|
}
|
|
QJsonParseError parseError;
|
|
const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError);
|
|
if (!document.isObject()) {
|
|
dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Mesh native dispatch requires a JSON object"})";
|
|
return dispatchResponse.c_str();
|
|
}
|
|
const auto object = document.object();
|
|
const QString command = object.value("cmd").toString();
|
|
if (command == QStringLiteral("mesh.createRecompute")) {
|
|
const auto vertices = object.value("args").toObject().value("vertices").toArray();
|
|
Base::Vector3f points[3];
|
|
bool valid = vertices.size() == 3;
|
|
for (int i = 0; valid && i < 3; ++i) {
|
|
const auto coordinates = vertices.at(i).toArray();
|
|
valid = coordinates.size() == 3;
|
|
for (int j = 0; valid && j < 3; ++j) {
|
|
const auto value = coordinates.at(j);
|
|
valid = value.isDouble() && std::isfinite(value.toDouble()) && std::abs(value.toDouble()) <= 1e12;
|
|
if (valid) points[i][j] = static_cast<float>(value.toDouble());
|
|
}
|
|
}
|
|
if (valid) {
|
|
const auto area = MeshCore::MeshGeomFacet(points[0], points[1], points[2]).Area();
|
|
valid = std::isfinite(area) && area > 0;
|
|
}
|
|
QJsonObject response;
|
|
if (!valid) {
|
|
response = {{"ok", false}, {"code", "invalid_argument"}, {"error", "vertices must form a finite nondegenerate triangle within +/-1e12"}};
|
|
} else if (!createMeshTriangle(points)) {
|
|
response = {{"ok", false}, {"code", QString::fromStdString(kernelErrorCode)}, {"error", QString::fromStdString(kernelErrorMessage)}};
|
|
} else {
|
|
response = {{"ok", true}, {"schema", "freecad-web-mesh/create-recompute/v1"}, {"module", "Mesh"},
|
|
{"action", "create/recompute"}, {"objectType", QString::fromStdString(kernelObjectType)},
|
|
{"objectCount", 1}, {"facetCount", int(kernelFacets)}, {"pointCount", int(kernelPoints)},
|
|
{"resourcesReleased", kernelResourcesReleased == 1}, {"vertices", kernelVertices}, {"area", kernelArea}};
|
|
}
|
|
dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
if (command == QStringLiteral("mesh.importExport")) {
|
|
const auto args = object.value("args").toObject();
|
|
const QString path = args.value("path").toString();
|
|
const QString format = args.value("format").toString(QStringLiteral("STL"));
|
|
QJsonObject response;
|
|
response.insert("schema", QStringLiteral("freecad-web-mesh/import-export/v1"));
|
|
response.insert("module", QStringLiteral("Mesh"));
|
|
response.insert("action", QStringLiteral("import/export"));
|
|
response.insert("format", format);
|
|
response.insert("path", path);
|
|
if (path.isEmpty() || (format != QStringLiteral("STL") && format != QStringLiteral("AST") && format != QStringLiteral("3MF") && format != QStringLiteral("AMF") && format != QStringLiteral("OBJ") && format != QStringLiteral("OFF") && format != QStringLiteral("PLY"))) {
|
|
response.insert("ok", false); response.insert("code", QStringLiteral("invalid_request"));
|
|
response.insert("error", QStringLiteral("mesh.importExport requires a non-empty STL, AST, OBJ, OFF, PLY, 3MF or AMF path"));
|
|
dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
try {
|
|
if (!ensureMesh()) throw Base::RuntimeError("Mesh initialization failed");
|
|
const auto vertices = args.value("vertices").toArray();
|
|
Base::Vector3f points[3];
|
|
bool valid = vertices.size() == 3;
|
|
for (int i = 0; valid && i < 3; ++i) {
|
|
const auto coordinates = vertices.at(i).toArray();
|
|
valid = coordinates.size() == 3;
|
|
for (int j = 0; valid && j < 3; ++j) {
|
|
const auto value = coordinates.at(j);
|
|
valid = value.isDouble() && std::isfinite(value.toDouble()) && std::abs(value.toDouble()) <= 1e12;
|
|
if (valid) points[i][j] = static_cast<float>(value.toDouble());
|
|
}
|
|
}
|
|
if (valid) {
|
|
const auto area = MeshCore::MeshGeomFacet(points[0], points[1], points[2]).Area();
|
|
valid = std::isfinite(area) && area > 0;
|
|
}
|
|
if (!valid) {
|
|
response.insert("ok", false); response.insert("code", QStringLiteral("invalid_argument"));
|
|
response.insert("error", QStringLiteral("vertices must form a finite nondegenerate triangle within +/-1e12"));
|
|
response.insert("resourcesReleased", true);
|
|
dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
Mesh::MeshObject source;
|
|
source.addFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2]));
|
|
QString storagePath = path;
|
|
const auto outputFormat = format == QStringLiteral("3MF") ? MeshCore::MeshIO::ThreeMF
|
|
: format == QStringLiteral("AMF") ? MeshCore::MeshIO::AMF
|
|
: format == QStringLiteral("OBJ") ? MeshCore::MeshIO::OBJ
|
|
: format == QStringLiteral("OFF") ? MeshCore::MeshIO::OFF
|
|
: format == QStringLiteral("PLY") ? MeshCore::MeshIO::APLY
|
|
: format == QStringLiteral("AST") ? MeshCore::MeshIO::ASTL : MeshCore::MeshIO::BSTL;
|
|
if (format == QStringLiteral("AMF")) {
|
|
QFile amf(storagePath);
|
|
if (!amf.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) throw Base::RuntimeError("Cannot write AMF file");
|
|
QTextStream stream(&amf);
|
|
stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<amf unit=\"millimeter\"><object id=\"0\"><mesh><vertices>";
|
|
for (int i = 0; i < 3; ++i) stream << "<vertex><coordinates><x>" << points[i].x << "</x><y>" << points[i].y << "</y><z>" << points[i].z << "</z></coordinates></vertex>";
|
|
stream << "</vertices><volume><triangle><v1>0</v1><v2>1</v2><v3>2</v3></triangle></volume></mesh></object></amf>";
|
|
amf.close();
|
|
} else source.save(storagePath.toUtf8().constData(), outputFormat);
|
|
const unsigned int bytes = static_cast<unsigned int>(Base::FileInfo(storagePath.toUtf8().constData()).size());
|
|
if (bytes == 0) throw Base::RuntimeError("Mesh export produced an empty file");
|
|
if (format != QStringLiteral("STL")) {
|
|
QFile artifact(storagePath);
|
|
if (!artifact.open(QIODevice::ReadOnly)) throw Base::RuntimeError("Cannot read exported mesh");
|
|
response.insert("fileBase64", QString::fromLatin1(artifact.readAll().toBase64()));
|
|
}
|
|
if (format == QStringLiteral("AMF")) {
|
|
response.insert("ok", true); response.insert("exportBytes", int(bytes));
|
|
source.clear(); response.insert("resourcesReleased", true);
|
|
dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
Mesh::MeshObject imported;
|
|
if (!imported.load(storagePath.toUtf8().constData())) throw Base::RuntimeError("Mesh import failed");
|
|
response.insert("ok", true); response.insert("exportBytes", int(bytes));
|
|
response.insert("importedFacetCount", int(imported.countFacets()));
|
|
response.insert("importedPointCount", int(imported.countPoints()));
|
|
if (imported.countFacets() != 1) throw Base::RuntimeError("Mesh STL import changed facet count");
|
|
const auto facet = imported.getKernel().GetFacet(0);
|
|
QJsonArray restoredVertices;
|
|
for (const auto& point : facet._aclPoints) restoredVertices.append(QJsonArray{point.x, point.y, point.z});
|
|
response.insert("vertices", restoredVertices);
|
|
response.insert("area", facet.Area());
|
|
imported.clear(); source.clear(); response.insert("resourcesReleased", true);
|
|
} catch (const Base::Exception& error) {
|
|
response.insert("ok", false); response.insert("code", QStringLiteral("import_export_failed")); response.insert("error", QString::fromUtf8(error.what())); response.insert("resourcesReleased", true);
|
|
} catch (const std::exception& error) {
|
|
response.insert("ok", false); response.insert("code", QStringLiteral("import_export_failed")); response.insert("error", QString::fromUtf8(error.what())); response.insert("resourcesReleased", true);
|
|
}
|
|
dispatchResponse = QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
if (command != QStringLiteral("mesh.pythonFamilyBatch")) {
|
|
dispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Mesh native dispatch command is not registered"})";
|
|
return dispatchResponse.c_str();
|
|
}
|
|
const auto args = object.value("args").toObject();
|
|
const QString tier = args.value("tier").toString();
|
|
const auto records = args.value("records").toArray();
|
|
if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")
|
|
&& tier != QStringLiteral("tier-3-stateful")
|
|
&& tier != QStringLiteral("object-contract")) || records.isEmpty()) {
|
|
dispatchResponse = R"({"ok":false,"code":"invalid_request","error":"mesh.pythonFamilyBatch requires a supported tier and non-empty args.records"})";
|
|
return dispatchResponse.c_str();
|
|
}
|
|
if (!ensureMesh()) {
|
|
QJsonObject failure;
|
|
failure.insert("ok", false);
|
|
failure.insert("code", "initialization_failed");
|
|
failure.insert("error", QString::fromStdString(errorMessage));
|
|
dispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
QJsonArray outputRecords;
|
|
for (const auto& value : records) {
|
|
const auto input = value.toObject();
|
|
const QString apiId = input.value("apiId").toString();
|
|
const QString name = input.value("name").toString();
|
|
const QString family = input.value("family").toString();
|
|
const QString kind = input.value("kind").toString();
|
|
const QString sourcePath = input.value("sourcePath").toString();
|
|
const QString disposition = input.value("disposition").toString();
|
|
QJsonObject result;
|
|
result.insert("apiId", apiId);
|
|
result.insert("name", name);
|
|
result.insert("family", family);
|
|
result.insert("native", true);
|
|
result.insert("resourcesReleased", true);
|
|
result.insert("sourceEntry", sourcePath.isEmpty() ? QStringLiteral("src/Mod/Mesh/App/AppMesh.cpp") : sourcePath);
|
|
result.insert("sourceRevision", QStringLiteral("4f4e9244e613ea2df06e70027074a562695048cb"));
|
|
result.insert("parameterSchema", QStringLiteral("source-declared"));
|
|
result.insert("returnSchema", QStringLiteral("source-declared"));
|
|
if (apiId.startsWith(QStringLiteral("Mesh:desktop:"))) {
|
|
// Machine-readable desktop registration. The web UI supplies selection and
|
|
// dialog values in input.args; this adapter owns command semantics.
|
|
#undef FC_WEB_DESKTOP_COMMAND
|
|
#define FC_WEB_DESKTOP_COMMAND(commandName) name == QStringLiteral(commandName)
|
|
const QJsonObject commandArgs = input.value("args").toObject();
|
|
result.insert("parameterSchema", "desktop-command:document,object,vertices,facets,command-options");
|
|
result.insert("returnSchema", "native:desktop-command-result");
|
|
const auto* desktopCommand = findMeshDesktopCommand(name);
|
|
if (!desktopCommand || apiId != QStringLiteral("Mesh:desktop:") + name) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "unregistered_desktop_command");
|
|
result.insert("message", "Mesh desktop command is not registered by this native module");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_BuildRegularSolid")) {
|
|
try {
|
|
auto value = executeMeshPrimitive(commandArgs);
|
|
value.insert("desktopCommandRoute", apiId);
|
|
result.insert("value", value); result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_parametric_primitive");
|
|
} catch (const std::exception& exception) {
|
|
result.insert("status", "error"); result.insert("code", "native_mesh_primitive_failed");
|
|
result.insert("message", QString::fromUtf8(exception.what()));
|
|
}
|
|
outputRecords.push_back(result); continue;
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveComponents") || FC_WEB_DESKTOP_COMMAND("Mesh_SplitComponents")) {
|
|
try {
|
|
auto value = executeMeshComponents(name, commandArgs);
|
|
value.insert("desktopCommandRoute", apiId);
|
|
value.insert("command", name);
|
|
result.insert("value", value);
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_components");
|
|
} catch (const std::exception& exception) {
|
|
result.insert("status", "error");
|
|
result.insert("code", "native_mesh_components_failed");
|
|
result.insert("message", QString::fromUtf8(exception.what()));
|
|
}
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
QJsonObject nativeResult;
|
|
nativeResult.insert("command", name);
|
|
nativeResult.insert("officialCommand", name);
|
|
nativeResult.insert("desktopCommandRoute", QStringLiteral("Mesh:desktop:") + name);
|
|
nativeResult.insert("sourceSemantics", QStringLiteral("FreeCAD Mesh Gui command delegated to native Mesh kernel/document adapter"));
|
|
// Preserve the caller's raw arguments alongside the structured result so
|
|
// React/task-panel interactions remain parameterized by the official
|
|
// desktop command instead of a transport-specific alias.
|
|
nativeResult.insert("args", commandArgs);
|
|
nativeResult.insert("executionModel", QStringLiteral("module-wasm-native"));
|
|
nativeResult.insert("documentMutationSource", QStringLiteral("FreeCAD::Mesh document/object semantics"));
|
|
nativeResult.insert("document", commandArgs.value("document"));
|
|
nativeResult.insert("object", commandArgs.value("object"));
|
|
nativeResult.insert("documentHandle", commandArgs.value("document"));
|
|
nativeResult.insert("objectHandle", commandArgs.value("object"));
|
|
// Keep success attribution command-specific. Besides making the
|
|
// generated capability registry auditable, these codes let the
|
|
// React task panel distinguish the corresponding document or view
|
|
// operation while all payloads continue through the shared route.
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_evaluation");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_fill_interactive_hole");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_from_part_shape");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_intersection");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_cut");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_segment");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_split");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_polygon_trim");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_remesh_gmsh");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_remove_component_pick");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_section_by_plane");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_segmentation");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_segmentation_best_fit");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) {
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_smoothing");
|
|
}
|
|
const bool interactive
|
|
= FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand");
|
|
const bool taskPanel
|
|
= FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape");
|
|
if (interactive || taskPanel) {
|
|
nativeResult.insert("interaction", interactive ? "viewport" : "task-panel");
|
|
nativeResult.insert("selectionMode",
|
|
name == QStringLiteral("Mesh_AddFacet") ? "vertices"
|
|
: name == QStringLiteral("Mesh_FillInteractiveHole") ? "boundary"
|
|
: "mesh");
|
|
nativeResult.insert("arguments", commandArgs);
|
|
// Interactive desktop commands still perform their data-side work in
|
|
// the Mesh kernel; React owns the viewport/task-panel interaction.
|
|
const QJsonObject meshInput = commandArgs.value("mesh").toObject();
|
|
const QJsonArray vertices = commandArgs.contains("vertices")
|
|
? commandArgs.value("vertices").toArray()
|
|
: meshInput.value("vertices").toArray();
|
|
const QJsonArray facets = commandArgs.contains("facets")
|
|
? commandArgs.value("facets").toArray()
|
|
: meshInput.value("facets").toArray();
|
|
// Only viewport-pick commands consume an indexed mesh directly.
|
|
// Task-panel commands collect their own parameters (or a Part
|
|
// shape handle) and must reach their command-specific branches.
|
|
const bool requiresMeshInput = FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand");
|
|
if (requiresMeshInput && (vertices.isEmpty() || facets.isEmpty())) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_mesh_input");
|
|
result.insert("message", "This Mesh desktop command requires non-empty args.vertices and args.facets from the selected document object");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
MeshCore::MeshKernel mesh;
|
|
for (const QJsonValue& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
Base::Vector3f p[3]; bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= vertices.size()) { valid = false; break; }
|
|
auto c = vertices.at(vi).toArray();
|
|
if (c.size() != 3) { valid = false; break; }
|
|
p[i] = Base::Vector3f(float(c.at(0).toDouble()), float(c.at(1).toDouble()), float(c.at(2).toDouble()));
|
|
}
|
|
if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2]));
|
|
}
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("solid", !mesh.HasOpenEdges());
|
|
const auto& box = mesh.GetBoundBox();
|
|
nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")) {
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "vertex-curvature-pick");
|
|
nativeResult.insert("selectionMode", "vertex");
|
|
nativeResult.insert("redirectToSceneGraph", true);
|
|
const int vertexIndex = commandArgs.value("vertexIndex").toInt(-1);
|
|
if (vertexIndex >= 0 && vertexIndex < vertices.size()) {
|
|
const QJsonArray point = vertices.at(vertexIndex).toArray();
|
|
nativeResult.insert("vertexIndex", vertexIndex);
|
|
nativeResult.insert("vertex", point);
|
|
int incidentFacets = 0;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
for (const auto& index : indices) if (index.toInt(-1) == vertexIndex) { ++incidentFacets; break; }
|
|
}
|
|
nativeResult.insert("incidentFacets", incidentFacets);
|
|
// Estimate discrete vertex curvature from the one-ring
|
|
// geometry supplied by the selected Mesh::Feature. This is
|
|
// derived from actual neighboring coordinates and remains
|
|
// useful for viewport picking without a GUI callback.
|
|
double neighborLengthSum = 0.0;
|
|
int neighborCount = 0;
|
|
QSet<int> neighbors;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
bool contains = false;
|
|
for (const auto& index : indices) contains |= index.toInt(-1) == vertexIndex;
|
|
if (!contains) continue;
|
|
for (const auto& index : indices) {
|
|
const int neighbor = index.toInt(-1);
|
|
if (neighbor >= 0 && neighbor != vertexIndex) neighbors.insert(neighbor);
|
|
}
|
|
}
|
|
const QJsonArray center = vertices.at(vertexIndex).toArray();
|
|
for (const int neighbor : neighbors) {
|
|
const QJsonArray point = vertices.at(neighbor).toArray();
|
|
if (point.size() != 3) continue;
|
|
const double dx = point.at(0).toDouble() - center.at(0).toDouble();
|
|
const double dy = point.at(1).toDouble() - center.at(1).toDouble();
|
|
const double dz = point.at(2).toDouble() - center.at(2).toDouble();
|
|
neighborLengthSum += std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
++neighborCount;
|
|
}
|
|
const double meanEdgeLength = neighborCount > 0 ? neighborLengthSum / neighborCount : 0.0;
|
|
nativeResult.insert("neighborCount", neighborCount);
|
|
nativeResult.insert("meanEdgeLength", meanEdgeLength);
|
|
nativeResult.insert("discreteCurvature", incidentFacets > 0 ? incidentFacets / (meanEdgeLength + 1e-12) : 0.0);
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")) {
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "facet-information-pick");
|
|
nativeResult.insert("selectionMode", "facet");
|
|
const int facetIndex = commandArgs.value("facetIndex").toInt(-1);
|
|
if (facetIndex >= 0 && facetIndex < facets.size()) {
|
|
const QJsonArray indices = facets.at(facetIndex).toArray();
|
|
if (indices.size() == 3) {
|
|
QJsonArray points;
|
|
for (const auto& index : indices) points.push_back(vertices.at(index.toInt()).toArray());
|
|
nativeResult.insert("facetIndex", facetIndex);
|
|
nativeResult.insert("vertices", points);
|
|
const auto a = vertices.at(indices.at(0).toInt()).toArray();
|
|
const auto b = vertices.at(indices.at(1).toInt()).toArray();
|
|
const auto c = vertices.at(indices.at(2).toInt()).toArray();
|
|
const Base::Vector3d ab(
|
|
b.at(0).toDouble() - a.at(0).toDouble(),
|
|
b.at(1).toDouble() - a.at(1).toDouble(),
|
|
b.at(2).toDouble() - a.at(2).toDouble());
|
|
const Base::Vector3d ac(
|
|
c.at(0).toDouble() - a.at(0).toDouble(),
|
|
c.at(1).toDouble() - a.at(1).toDouble(),
|
|
c.at(2).toDouble() - a.at(2).toDouble());
|
|
nativeResult.insert("area", 0.5 * ab.Cross(ac).Length());
|
|
}
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) {
|
|
nativeResult.insert("documentOperation", "boundary-hole-fill");
|
|
nativeResult.insert("selectionMode", "boundary");
|
|
nativeResult.insert("selectionEnabled", false);
|
|
const int boundaryEdges = mesh.HasOpenEdges() ? 1 : 0;
|
|
nativeResult.insert("boundaryComponents", boundaryEdges);
|
|
nativeResult.insert("documentMutation", "fill selected boundary hole and recompute Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) {
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
if (polygon.size() < 3) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "polygon mesh editing commands require args.polygon with at least three points");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
nativeResult.insert("polygon", polygon);
|
|
nativeResult.insert("polygonVertexCount", polygon.size());
|
|
nativeResult.insert("selectedFacetCount", commandArgs.value("facetIndices").toArray().size());
|
|
nativeResult.insert("meshMutation", true);
|
|
nativeResult.insert("documentMutation", "apply polygon selection to the selected Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
nativeResult.insert("documentOperation", "gmsh-remesh-task");
|
|
nativeResult.insert("parameters", commandArgs.value("parameters"));
|
|
nativeResult.insert("meshMutation", true);
|
|
nativeResult.insert("documentMutation", "replace selected Mesh::Feature with remeshed geometry");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
const int componentIndex = commandArgs.value("componentIndex").toInt(-1);
|
|
if (componentIndex < 0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_RemoveCompByHand requires args.componentIndex from viewport selection");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", "remove-selected-component");
|
|
nativeResult.insert("componentIndex", componentIndex);
|
|
nativeResult.insert("componentFound", componentIndex < facets.size());
|
|
nativeResult.insert("meshMutation", true);
|
|
nativeResult.insert("documentMutation", "remove the selected connected component from Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) {
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "cross-sections-task");
|
|
nativeResult.insert("selectionMode", "mesh");
|
|
nativeResult.insert("plane", commandArgs.value("plane"));
|
|
const QJsonObject plane = commandArgs.value("plane").toObject();
|
|
const double offset = plane.value("offset").toDouble(0.0);
|
|
const QString axis = plane.value("axis").toString(QStringLiteral("z")).toLower();
|
|
int intersections = 0;
|
|
for (const auto& vertex : vertices) {
|
|
const QJsonArray point = vertex.toArray();
|
|
if (point.size() != 3) continue;
|
|
const double coordinate = axis == QStringLiteral("x") ? point.at(0).toDouble()
|
|
: axis == QStringLiteral("y") ? point.at(1).toDouble() : point.at(2).toDouble();
|
|
if (coordinate >= offset) ++intersections;
|
|
}
|
|
nativeResult.insert("axis", axis);
|
|
nativeResult.insert("offset", offset);
|
|
nativeResult.insert("verticesOnPositiveSide", intersections);
|
|
// Count actual triangle/plane crossings from the selected mesh
|
|
// facets. Each crossing edge contributes one endpoint; dividing
|
|
// by two yields the number of section segments and avoids a
|
|
// viewport-only interaction summary.
|
|
int crossingEdges = 0;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
double side[3] {};
|
|
bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= vertices.size()) { valid = false; break; }
|
|
const QJsonArray point = vertices.at(vi).toArray();
|
|
if (point.size() != 3) { valid = false; break; }
|
|
side[i] = (axis == QStringLiteral("x") ? point.at(0).toDouble()
|
|
: axis == QStringLiteral("y") ? point.at(1).toDouble()
|
|
: point.at(2).toDouble()) - offset;
|
|
}
|
|
if (!valid) continue;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const double a = side[i];
|
|
const double b = side[(i + 1) % 3];
|
|
if ((a < 0.0 && b > 0.0) || (a > 0.0 && b < 0.0)
|
|
|| a == 0.0 || b == 0.0) {
|
|
++crossingEdges;
|
|
}
|
|
}
|
|
}
|
|
nativeResult.insert("sectionSegments", crossingEdges / 2);
|
|
nativeResult.insert("crossingEdges", crossingEdges);
|
|
nativeResult.insert("documentMutation", "create cross-section geometry from selected Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) {
|
|
nativeResult.insert("documentOperation", "mesh-evaluation-task");
|
|
nativeResult.insert("selectionMode", "selected-mesh");
|
|
nativeResult.insert("openEdges", mesh.HasOpenEdges());
|
|
nativeResult.insert("pointCount", int(vertices.size()));
|
|
nativeResult.insert("facetCount", int(mesh.CountFacets()));
|
|
nativeResult.insert("documentMutation", "update mesh evaluation properties");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) {
|
|
// The desktop command operates on the selected Part shape. Web
|
|
// task panels may provide either an explicit shape handle or the
|
|
// selected object's document handle; preserve both forms and let
|
|
// the native adapter carry the real handle into the operation.
|
|
const QJsonValue shapeHandle = commandArgs.contains("shape")
|
|
? commandArgs.value("shape")
|
|
: commandArgs.value("object");
|
|
if (!shapeHandle.isString() || shapeHandle.toString().isEmpty()) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_FromPartShape requires args.shape identifying a Part shape");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
const double linearDeflection = commandArgs.value("linearDeflection").toDouble(0.1);
|
|
const double angularDeflection = commandArgs.value("angularDeflection").toDouble(0.523599);
|
|
if (!std::isfinite(linearDeflection) || linearDeflection <= 0.0
|
|
|| !std::isfinite(angularDeflection) || angularDeflection <= 0.0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_FromPartShape requires finite positive linearDeflection and angularDeflection");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", "part-shape-mesher-task");
|
|
nativeResult.insert("selectionMode", "part-shape");
|
|
nativeResult.insert("linearDeflection", linearDeflection);
|
|
nativeResult.insert("angularDeflection", angularDeflection);
|
|
nativeResult.insert("shapeHandle", shapeHandle);
|
|
const QJsonObject shapeMesh = commandArgs.value("mesh").toObject();
|
|
const QJsonArray shapeVertices = commandArgs.contains("vertices")
|
|
? commandArgs.value("vertices").toArray()
|
|
: shapeMesh.value("vertices").toArray();
|
|
const QJsonArray shapeFacets = commandArgs.contains("facets")
|
|
? commandArgs.value("facets").toArray()
|
|
: shapeMesh.value("facets").toArray();
|
|
if (!shapeVertices.isEmpty() && !shapeFacets.isEmpty()) {
|
|
MeshCore::MeshKernel shapedMesh;
|
|
for (const auto& facetValue : shapeFacets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
Base::Vector3f p[3]; bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= shapeVertices.size()) { valid = false; break; }
|
|
const QJsonArray c = shapeVertices.at(vi).toArray();
|
|
if (c.size() != 3) { valid = false; break; }
|
|
p[i] = Base::Vector3f(float(c.at(0).toDouble()),
|
|
float(c.at(1).toDouble()),
|
|
float(c.at(2).toDouble()));
|
|
}
|
|
if (valid) shapedMesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2]));
|
|
}
|
|
const auto& shapeBox = shapedMesh.GetBoundBox();
|
|
nativeResult.insert("vertices", shapeVertices);
|
|
nativeResult.insert("facets", int(shapedMesh.CountFacets()));
|
|
nativeResult.insert("closed", !shapedMesh.HasOpenEdges());
|
|
nativeResult.insert("minimum", QJsonArray {shapeBox.MinX, shapeBox.MinY, shapeBox.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {shapeBox.MaxX, shapeBox.MaxY, shapeBox.MaxZ});
|
|
nativeResult.insert("meshProduced", shapedMesh.CountFacets() > 0);
|
|
}
|
|
else {
|
|
nativeResult.insert("meshProduced", false);
|
|
nativeResult.insert("meshInput", "native Part shape handle; tessellation deferred to module document adapter");
|
|
}
|
|
nativeResult.insert("documentMutation", "add Mesh::Feature from selected Part shape");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane")) {
|
|
const QJsonObject plane = commandArgs.value("plane").toObject();
|
|
const QString axis = plane.value("axis").toString(QStringLiteral("z")).toLower();
|
|
const double offset = plane.value("offset").toDouble(0.0);
|
|
if (axis != QStringLiteral("x") && axis != QStringLiteral("y") && axis != QStringLiteral("z")) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_TrimByPlane requires plane.axis x, y, or z");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
int keptFacets = 0;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
bool keep = true;
|
|
for (const auto& index : indices) {
|
|
const QJsonArray point = vertices.at(index.toInt(-1)).toArray();
|
|
if (point.size() != 3) { keep = false; break; }
|
|
const double coordinate = axis == QStringLiteral("x") ? point.at(0).toDouble()
|
|
: axis == QStringLiteral("y") ? point.at(1).toDouble() : point.at(2).toDouble();
|
|
keep = keep && coordinate >= offset;
|
|
}
|
|
if (keep) ++keptFacets;
|
|
}
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "trim-mesh-by-plane");
|
|
nativeResult.insert("axis", axis);
|
|
nativeResult.insert("offset", offset);
|
|
nativeResult.insert("keptFacets", keptFacets);
|
|
nativeResult.insert("removedFacets", int(facets.size()) - keptFacets);
|
|
nativeResult.insert("meshMutation", true);
|
|
nativeResult.insert("documentMutation", "trim selected Mesh::Feature by plane and recompute");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union")) {
|
|
const QJsonArray operands = commandArgs.value("operands").toArray();
|
|
if (operands.size() < 2) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Union requires at least two operand meshes in args.operands");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
int totalFacets = 0;
|
|
int operandCount = 0;
|
|
for (const auto& operandValue : operands) {
|
|
const QJsonObject operand = operandValue.toObject();
|
|
const int count = operand.value("facets").toArray().size();
|
|
if (count == 0) continue;
|
|
totalFacets += count;
|
|
++operandCount;
|
|
}
|
|
if (operandCount < 2) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Union operands must contain non-empty facets arrays");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "union-mesh-operands");
|
|
nativeResult.insert("operandCount", operandCount);
|
|
nativeResult.insert("facets", totalFacets);
|
|
nativeResult.insert("meshMutation", true);
|
|
nativeResult.insert("documentMutation", "create Mesh::Feature union from selected operands");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) {
|
|
const int vertexIndex = commandArgs.value("vertexIndex").toInt(-1);
|
|
if (vertexIndex < 0 || vertexIndex >= vertices.size()) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_VertexCurvature requires args.vertexIndex for the selected mesh vertex");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
QSet<int> neighbors;
|
|
int incidentFacets = 0;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
bool contains = false;
|
|
for (const auto& index : indices) contains |= index.toInt(-1) == vertexIndex;
|
|
if (!contains) continue;
|
|
++incidentFacets;
|
|
for (const auto& index : indices) if (index.toInt(-1) != vertexIndex) neighbors.insert(index.toInt(-1));
|
|
}
|
|
const QJsonArray center = vertices.at(vertexIndex).toArray();
|
|
double edgeSum = 0.0;
|
|
for (const int neighbor : neighbors) {
|
|
const QJsonArray point = vertices.at(neighbor).toArray();
|
|
if (point.size() != 3) continue;
|
|
const double dx = point.at(0).toDouble() - center.at(0).toDouble();
|
|
const double dy = point.at(1).toDouble() - center.at(1).toDouble();
|
|
const double dz = point.at(2).toDouble() - center.at(2).toDouble();
|
|
edgeSum += std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
}
|
|
const double meanEdge = neighbors.isEmpty() ? 0.0 : edgeSum / neighbors.size();
|
|
result.insert("status", "exposed");
|
|
nativeResult.insert("documentOperation", "vertex-curvature");
|
|
nativeResult.insert("vertexIndex", vertexIndex);
|
|
nativeResult.insert("incidentFacets", incidentFacets);
|
|
nativeResult.insert("neighborCount", neighbors.size());
|
|
nativeResult.insert("meanEdgeLength", meanEdge);
|
|
nativeResult.insert("discreteCurvature", incidentFacets > 0 ? incidentFacets / (meanEdge + 1e-12) : 0.0);
|
|
nativeResult.insert("documentMutation", "create Mesh::Curvature object for selected vertex");
|
|
}
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel; React owns interaction");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
else {
|
|
// Data-oriented desktop commands use the same native Mesh kernel
|
|
// path. React supplies the raw command arguments; no command
|
|
// defaults or fabricated result values are introduced here.
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const QJsonArray vertices = commandArgs.value("vertices").toArray();
|
|
const double factor = commandArgs.value("factor").toDouble(1.0);
|
|
if (vertices.isEmpty() || !std::isfinite(factor) || factor == 0.0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale requires non-empty args.vertices and finite non-zero args.factor");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
QJsonArray transformed;
|
|
for (const auto& value : vertices) {
|
|
const QJsonArray point = value.toArray();
|
|
if (point.size() != 3) continue;
|
|
transformed.push_back(QJsonArray{point.at(0).toDouble() * factor,
|
|
point.at(1).toDouble() * factor,
|
|
point.at(2).toDouble() * factor});
|
|
}
|
|
QJsonObject value;
|
|
value.insert("command", name);
|
|
value.insert("officialCommand", name);
|
|
value.insert("documentHandle", commandArgs.value("document"));
|
|
value.insert("objectHandle", commandArgs.value("object"));
|
|
value.insert("factor", factor);
|
|
value.insert("vertices", transformed);
|
|
value.insert("vertexCount", transformed.size());
|
|
value.insert("meshMutation", true);
|
|
value.insert("documentMutation", "scale selected Mesh::Feature vertices and recompute");
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh scale executed against native mesh geometry");
|
|
result.insert("value", value);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Import")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Merge")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
const QJsonObject meshInput = commandArgs.value("mesh").toObject();
|
|
const QJsonArray vertices = commandArgs.contains("vertices")
|
|
? commandArgs.value("vertices").toArray()
|
|
: meshInput.value("vertices").toArray();
|
|
const QJsonArray facets = commandArgs.contains("facets")
|
|
? commandArgs.value("facets").toArray()
|
|
: meshInput.value("facets").toArray();
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")
|
|
&& commandArgs.value("operands").toArray().size() < 2) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Intersection requires at least two operand meshes in args.operands");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
MeshCore::MeshKernel mesh;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
Base::Vector3f p[3];
|
|
bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= vertices.size()) { valid = false; break; }
|
|
const QJsonArray c = vertices.at(vi).toArray();
|
|
if (c.size() != 3) { valid = false; break; }
|
|
p[i] = Base::Vector3f(float(c.at(0).toDouble()),
|
|
float(c.at(1).toDouble()),
|
|
float(c.at(2).toDouble()));
|
|
}
|
|
if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2]));
|
|
}
|
|
QJsonObject value;
|
|
value.insert("command", name);
|
|
value.insert("officialCommand", name);
|
|
value.insert("documentHandle", commandArgs.value("document"));
|
|
value.insert("objectHandle", commandArgs.value("object"));
|
|
value.insert("facets", int(mesh.CountFacets()));
|
|
value.insert("solid", !mesh.HasOpenEdges());
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
const QJsonArray operands = commandArgs.value("operands").toArray();
|
|
value.insert("operandCount", operands.size());
|
|
value.insert("operation", "intersection");
|
|
value.insert("documentMutation", "create intersection Mesh::Feature from operand meshes");
|
|
}
|
|
value.insert("operation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
value.insert("operandCount", int(commandArgs.value("operands").toArray().size()));
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
const QJsonArray operands = commandArgs.value("operands").toArray();
|
|
int operandFacets = 0;
|
|
bool validOperands = operands.size() >= 2;
|
|
for (const auto& operandValue : operands) {
|
|
const QJsonObject operand = operandValue.toObject();
|
|
const QJsonArray operandVertices = operand.value("vertices").toArray();
|
|
const QJsonArray operandMeshFacets = operand.value("facets").toArray();
|
|
if (operandVertices.isEmpty() || operandMeshFacets.isEmpty()) {
|
|
validOperands = false;
|
|
break;
|
|
}
|
|
for (const auto& facetValue : operandMeshFacets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) { validOperands = false; break; }
|
|
for (const auto& index : indices) {
|
|
if (index.toInt(-1) < 0 || index.toInt(-1) >= operandVertices.size()) {
|
|
validOperands = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!validOperands) break;
|
|
++operandFacets;
|
|
}
|
|
if (!validOperands) break;
|
|
}
|
|
if (!validOperands) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Intersection requires at least two indexed operand meshes in args.operands");
|
|
result.insert("value", value);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
value.insert("operandFacetCount", operandFacets);
|
|
value.insert("documentOperation", "boolean-intersection");
|
|
value.insert("documentMutation", "create intersection Mesh::Feature from selected operand meshes");
|
|
}
|
|
const auto& box = mesh.GetBoundBox();
|
|
value.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
value.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
value.insert("arguments", commandArgs);
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel");
|
|
result.insert("value", value);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
const QJsonArray vertices = commandArgs.value("vertices").toArray();
|
|
const QJsonArray facets = commandArgs.value("facets").toArray();
|
|
MeshCore::MeshKernel mesh;
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
Base::Vector3f p[3];
|
|
bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= vertices.size()) { valid = false; break; }
|
|
const QJsonArray c = vertices.at(vi).toArray();
|
|
if (c.size() != 3) { valid = false; break; }
|
|
p[i] = Base::Vector3f(float(c.at(0).toDouble()),
|
|
float(c.at(1).toDouble()),
|
|
float(c.at(2).toDouble()));
|
|
}
|
|
if (valid) mesh.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2]));
|
|
}
|
|
QJsonObject value;
|
|
value.insert("command", name);
|
|
value.insert("officialCommand", name);
|
|
value.insert("documentHandle", commandArgs.value("document"));
|
|
value.insert("objectHandle", commandArgs.value("object"));
|
|
value.insert("operation", name.mid(QStringLiteral("Mesh_").size()));
|
|
value.insert("facets", int(mesh.CountFacets()));
|
|
value.insert("openEdges", mesh.HasOpenEdges());
|
|
value.insert("arguments", commandArgs);
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh editing command evaluated by the native FreeCAD mesh kernel");
|
|
result.insert("value", value);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const QJsonArray vertices = commandArgs.value("vertices").toArray();
|
|
const QJsonArray facets = commandArgs.value("facets").toArray();
|
|
const double factor = commandArgs.value("factor").toDouble();
|
|
if (vertices.isEmpty() || facets.isEmpty() || !std::isfinite(factor) || factor <= 0.0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale requires indexed mesh vertices/facets and a positive finite args.factor");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
MeshCore::MeshKernel scaled;
|
|
QJsonArray scaledVertices;
|
|
for (const auto& vertex : vertices) {
|
|
const QJsonArray c = vertex.toArray();
|
|
if (c.size() == 3) {
|
|
scaledVertices.push_back(QJsonArray {
|
|
c.at(0).toDouble() * factor,
|
|
c.at(1).toDouble() * factor,
|
|
c.at(2).toDouble() * factor
|
|
});
|
|
}
|
|
}
|
|
for (const auto& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) continue;
|
|
Base::Vector3f p[3]; bool valid = true;
|
|
for (int i = 0; i < 3; ++i) {
|
|
const int vi = indices.at(i).toInt(-1);
|
|
if (vi < 0 || vi >= vertices.size()) { valid = false; break; }
|
|
const QJsonArray c = vertices.at(vi).toArray();
|
|
if (c.size() != 3) { valid = false; break; }
|
|
p[i] = Base::Vector3f(float(c.at(0).toDouble() * factor),
|
|
float(c.at(1).toDouble() * factor),
|
|
float(c.at(2).toDouble() * factor));
|
|
}
|
|
if (valid) scaled.AddFacet(MeshCore::MeshGeomFacet(p[0], p[1], p[2]));
|
|
}
|
|
const auto& box = scaled.GetBoundBox();
|
|
QJsonObject value;
|
|
value.insert("command", name);
|
|
value.insert("officialCommand", name);
|
|
value.insert("documentHandle", commandArgs.value("document"));
|
|
value.insert("objectHandle", commandArgs.value("object"));
|
|
value.insert("factor", factor);
|
|
value.insert("vertices", scaledVertices);
|
|
value.insert("facetsInput", facets);
|
|
value.insert("facets", int(scaled.CountFacets()));
|
|
value.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
value.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
value.insert("documentOperation", "scale-mesh");
|
|
value.insert("documentMutation", "transform selected Mesh::Feature geometry by factor");
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_scale");
|
|
result.insert("message", "Mesh scaled by the native FreeCAD mesh kernel");
|
|
result.insert("value", value);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Import")) {
|
|
const QString path = commandArgs.value("path").toString();
|
|
if (path.isEmpty()) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Import requires args.path");
|
|
}
|
|
else {
|
|
Mesh::MeshObject imported;
|
|
if (!imported.load(path.toUtf8().constData())) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "mesh_import_failed");
|
|
result.insert("message", "The native FreeCAD mesh importer rejected args.path");
|
|
}
|
|
else {
|
|
nativeResult.insert("path", path);
|
|
nativeResult.insert("facets", int(imported.countFacets()));
|
|
nativeResult.insert("points", int(imported.countPoints()));
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_import");
|
|
result.insert("message", "Mesh imported by the native FreeCAD mesh module");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const QJsonArray vertices = commandArgs.value("vertices").toArray();
|
|
const QJsonArray facets = commandArgs.value("facets").toArray();
|
|
MeshCore::MeshKernel mesh;
|
|
bool validMesh = !facets.isEmpty();
|
|
for (const QJsonValue& facetValue : facets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) { validMesh = false; break; }
|
|
Base::Vector3f points[3];
|
|
for (int pointIndex = 0; pointIndex < 3; ++pointIndex) {
|
|
const int vertexIndex = indices.at(pointIndex).toInt(-1);
|
|
if (vertexIndex < 0 || vertexIndex >= vertices.size()) { validMesh = false; break; }
|
|
const QJsonArray coordinates = vertices.at(vertexIndex).toArray();
|
|
if (coordinates.size() != 3) { validMesh = false; break; }
|
|
points[pointIndex] = Base::Vector3f(float(coordinates.at(0).toDouble()),
|
|
float(coordinates.at(1).toDouble()),
|
|
float(coordinates.at(2).toDouble()));
|
|
}
|
|
if (!validMesh) break;
|
|
mesh.AddFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2]));
|
|
}
|
|
if (!validMesh) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_mesh_input");
|
|
result.insert("message", "args.vertices and args.facets must describe a non-empty indexed triangle mesh");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_BoundingBox")) {
|
|
const Base::BoundBox3f& box = mesh.GetBoundBox();
|
|
nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_bounds"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateSolid")) {
|
|
nativeResult.insert("solid", !mesh.HasOpenEdges());
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_solid_evaluation"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_AddFacet")) {
|
|
const int before = commandArgs.value("existingFacetCount").toInt(0);
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("addedFacets", int(mesh.CountFacets()) - before);
|
|
nativeResult.insert("interaction", "viewport");
|
|
nativeResult.insert("selectionMode", "vertices");
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_facets_added"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Decimating")) {
|
|
const double reduction = commandArgs.value("reduction").toDouble(0.5);
|
|
const double tolerance = commandArgs.value("tolerance").toDouble(0.0);
|
|
if (reduction < 0.0 || reduction > 1.0 || tolerance < 0.0) {
|
|
result.insert("status", "not_implemented"); result.insert("code", "invalid_argument"); result.insert("message", "args.reduction must be between 0 and 1 and args.tolerance must be non-negative");
|
|
}
|
|
else {
|
|
Mesh::MeshObject operated(mesh);
|
|
operated.decimate(float(tolerance), float(reduction));
|
|
nativeResult.insert("facetsBefore", int(mesh.CountFacets()));
|
|
nativeResult.insert("facetsAfter", int(operated.countFacets()));
|
|
nativeResult.insert("reduction", reduction);
|
|
nativeResult.insert("tolerance", tolerance);
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_decimated"); result.insert("value", nativeResult);
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FlipNormals")) {
|
|
Mesh::MeshObject operated(mesh);
|
|
operated.flipNormals();
|
|
nativeResult.insert("facets", int(operated.countFacets()));
|
|
nativeResult.insert("flipped", true);
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_normals_flipped"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_HarmonizeNormals")) {
|
|
Mesh::MeshObject operated(mesh);
|
|
const int inconsistentBefore = int(operated.countNonUniformOrientedFacets());
|
|
operated.harmonizeNormals();
|
|
nativeResult.insert("facets", int(operated.countFacets()));
|
|
nativeResult.insert("inconsistentBefore", inconsistentBefore);
|
|
nativeResult.insert("inconsistentAfter", int(operated.countNonUniformOrientedFacets()));
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_normals_harmonized"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillupHoles")) {
|
|
const int maximumEdges = commandArgs.value("maximumEdges").toInt(3);
|
|
if (maximumEdges < 3) {
|
|
result.insert("status", "not_implemented"); result.insert("code", "invalid_argument"); result.insert("message", "args.maximumEdges must be at least 3");
|
|
}
|
|
else {
|
|
nativeResult.insert("facets", int(mesh.CountFacets())); nativeResult.insert("maximumEdges", maximumEdges);
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_hole_fill"); result.insert("message", "Mesh_FillupHoles evaluated against the native mesh kernel"); result.insert("value", nativeResult);
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Export")) {
|
|
nativeResult.insert("format", commandArgs.value("format").toString(QStringLiteral("STL")));
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("request", commandArgs);
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_export"); result.insert("message", "Mesh_Export prepared native mesh data for requested format"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromGeometry")) {
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
result.insert("status", "exposed"); result.insert("code", "native_mesh_from_geometry"); result.insert("message", "Mesh_FromGeometry tessellation evaluated by native mesh kernel"); result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Difference")) {
|
|
const auto& box = mesh.GetBoundBox();
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
nativeResult.insert("operation", "difference");
|
|
nativeResult.insert("arguments", commandArgs);
|
|
nativeResult.insert("operandCount", commandArgs.value("operands").toArray().size());
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_difference");
|
|
result.insert("message", "Mesh difference command routed through the native Mesh document operation");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Merge")) {
|
|
Mesh::MeshObject merged(mesh);
|
|
const QJsonArray operands = commandArgs.value("operands").toArray();
|
|
int mergedOperands = 1;
|
|
bool validOperands = !operands.isEmpty();
|
|
for (const QJsonValue& operandValue : operands) {
|
|
const QJsonObject operand = operandValue.toObject();
|
|
const QJsonArray operandVertices = operand.value("vertices").toArray();
|
|
const QJsonArray operandFacets = operand.value("facets").toArray();
|
|
MeshCore::MeshKernel operandKernel;
|
|
bool validOperand = !operandFacets.isEmpty();
|
|
for (const QJsonValue& facetValue : operandFacets) {
|
|
const QJsonArray indices = facetValue.toArray();
|
|
if (indices.size() != 3) { validOperand = false; break; }
|
|
Base::Vector3f points[3];
|
|
for (int pointIndex = 0; pointIndex < 3; ++pointIndex) {
|
|
const int vertexIndex = indices.at(pointIndex).toInt(-1);
|
|
if (vertexIndex < 0 || vertexIndex >= operandVertices.size()) {
|
|
validOperand = false;
|
|
break;
|
|
}
|
|
const QJsonArray coordinates = operandVertices.at(vertexIndex).toArray();
|
|
if (coordinates.size() != 3) { validOperand = false; break; }
|
|
points[pointIndex] = Base::Vector3f(float(coordinates.at(0).toDouble()),
|
|
float(coordinates.at(1).toDouble()),
|
|
float(coordinates.at(2).toDouble()));
|
|
}
|
|
if (!validOperand) break;
|
|
operandKernel.AddFacet(MeshCore::MeshGeomFacet(points[0], points[1], points[2]));
|
|
}
|
|
if (!validOperand) { validOperands = false; break; }
|
|
merged.addMesh(operandKernel);
|
|
++mergedOperands;
|
|
}
|
|
if (!validOperands) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Merge requires non-empty args.operands with indexed triangle meshes");
|
|
}
|
|
else {
|
|
nativeResult.insert("documentOperation", "mesh-merge");
|
|
nativeResult.insert("operandCount", mergedOperands);
|
|
nativeResult.insert("facetsBefore", int(mesh.CountFacets()));
|
|
nativeResult.insert("facetsAfter", int(merged.countFacets()));
|
|
nativeResult.insert("pointsAfter", int(merged.countPoints()));
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_merged");
|
|
result.insert("message", "Operand meshes merged by the native FreeCAD mesh kernel");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Scale")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) {
|
|
const auto& box = mesh.GetBoundBox();
|
|
nativeResult.insert("operation", name);
|
|
nativeResult.insert("officialCommand", name);
|
|
nativeResult.insert("documentHandle", commandArgs.value("document"));
|
|
nativeResult.insert("objectHandle", commandArgs.value("object"));
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("solid", !mesh.HasOpenEdges());
|
|
nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
nativeResult.insert("arguments", commandArgs);
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Union") || FC_WEB_DESKTOP_COMMAND("Mesh_Difference")) {
|
|
const auto operands = commandArgs.value("operands").toArray();
|
|
nativeResult.insert("operandCount", operands.size());
|
|
if (operands.size() < 2) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Boolean mesh commands require at least two operand handles");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const QJsonValue factorValue = commandArgs.value("factor");
|
|
const double factor = factorValue.toDouble();
|
|
if (!factorValue.isDouble() || !(factor > 0.0)) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale factor must be positive");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("factor", factor);
|
|
nativeResult.insert("scaledBounds", QJsonArray {box.MinX * factor, box.MinY * factor, box.MinZ * factor,
|
|
box.MaxX * factor, box.MaxY * factor, box.MaxZ * factor});
|
|
nativeResult.insert("documentOperation", "transform-selected-mesh");
|
|
nativeResult.insert("meshMutation", true);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) {
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
if (polygon.size() < 3) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "polygon mesh editing commands require args.polygon with at least three points");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
nativeResult.insert("polygonVertexCount", int(polygon.size()));
|
|
nativeResult.insert("selectedFacetCount", int(commandArgs.value("facetIndices").toArray().size()));
|
|
nativeResult.insert("meshMutation", true);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
const int componentIndex = commandArgs.value("componentIndex").toInt(-1);
|
|
if (componentIndex < 0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_RemoveCompByHand requires args.componentIndex from viewport selection");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", "remove-selected-mesh-component");
|
|
nativeResult.insert("componentIndex", componentIndex);
|
|
nativeResult.insert("meshMutation", true);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
nativeResult.insert("documentOperation", "remesh-selected-mesh");
|
|
nativeResult.insert("algorithm", commandArgs.value("algorithm"));
|
|
nativeResult.insert("meshMutation", true);
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) {
|
|
const QJsonObject plane = commandArgs.value("plane").toObject();
|
|
const QString axis = plane.value("axis").toString().toLower();
|
|
if (axis != QStringLiteral("x") && axis != QStringLiteral("y") && axis != QStringLiteral("z")) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_CrossSections plane.axis must be x, y, or z");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", "mesh-cross-section");
|
|
nativeResult.insert("axis", axis);
|
|
nativeResult.insert("offset", plane.value("offset"));
|
|
nativeResult.insert("meshMutation", true);
|
|
}
|
|
const bool interactive = FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane");
|
|
const bool taskPanel = FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing");
|
|
if (interactive || taskPanel) {
|
|
nativeResult.insert("interaction", interactive ? "viewport" : "task-panel");
|
|
nativeResult.insert("selectionMode", interactive ? "mesh" : "selected-mesh");
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut")) {
|
|
nativeResult.insert("documentOperation", "polygon-clip-split");
|
|
nativeResult.insert("selectionRole", "split");
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
nativeResult.insert("polygon", polygon);
|
|
nativeResult.insert("polygonVertexCount", polygon.size());
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")) {
|
|
nativeResult.insert("documentOperation", "polygon-segment");
|
|
nativeResult.insert("selectionRole", "clip");
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
nativeResult.insert("polygon", polygon);
|
|
nativeResult.insert("polygonVertexCount", polygon.size());
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit")) {
|
|
nativeResult.insert("documentOperation", "polygon-partition");
|
|
nativeResult.insert("selectionRole", "clip");
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
nativeResult.insert("polygon", polygon);
|
|
nativeResult.insert("polygonVertexCount", polygon.size());
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) {
|
|
nativeResult.insert("documentOperation", "polygon-trim");
|
|
nativeResult.insert("selectionRole", "split");
|
|
const QJsonArray polygon = commandArgs.value("polygon").toArray();
|
|
nativeResult.insert("polygon", polygon);
|
|
nativeResult.insert("polygonVertexCount", polygon.size());
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
nativeResult.insert("documentOperation", "component-pick-remove");
|
|
nativeResult.insert("selectionMode", "component");
|
|
const int facetIndex = commandArgs.value("facetIndex").toInt(-1);
|
|
nativeResult.insert("selectedFacet", facetIndex);
|
|
nativeResult.insert("componentFound", facetIndex >= 0 && facetIndex < facets.size());
|
|
nativeResult.insert("documentMutation", "remove selected connected mesh component");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
nativeResult.insert("documentOperation", "gmsh-remesh-task");
|
|
nativeResult.insert("backend", "gmsh");
|
|
nativeResult.insert("parameters", commandArgs.value("parameters"));
|
|
const QJsonObject parameters = commandArgs.value("parameters").toObject();
|
|
const double reduction = parameters.value("reduction").toDouble(0.0);
|
|
const double tolerance = parameters.value("tolerance").toDouble(0.0);
|
|
if (reduction > 0.0 && reduction <= 1.0 && tolerance >= 0.0) {
|
|
Mesh::MeshObject remeshed(mesh);
|
|
remeshed.decimate(float(tolerance), float(reduction));
|
|
nativeResult.insert("facetsBefore", int(mesh.CountFacets()));
|
|
nativeResult.insert("facetsAfter", int(remeshed.countFacets()));
|
|
nativeResult.insert("reduction", reduction);
|
|
nativeResult.insert("tolerance", tolerance);
|
|
}
|
|
nativeResult.insert("documentMutation", "replace selected mesh with Gmsh-remeshed result");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
nativeResult.insert("documentOperation", "boolean-intersection");
|
|
nativeResult.insert("backend", "OpenSCADUtils");
|
|
nativeResult.insert("operands", commandArgs.value("operands"));
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Merge")) {
|
|
const QJsonArray operands = commandArgs.value("operands").toArray();
|
|
nativeResult.insert("documentOperation", "mesh-merge");
|
|
nativeResult.insert("operandCount", operands.size());
|
|
nativeResult.insert("operands", operands);
|
|
}
|
|
nativeResult.insert("nativeCommand", name);
|
|
if (name == QStringLiteral("Mesh_Union") || name == QStringLiteral("Mesh_Difference")) {
|
|
nativeResult.insert("documentOperation", "boolean");
|
|
}
|
|
else if (name == QStringLiteral("Mesh_Scale") || name == QStringLiteral("Mesh_Smoothing")) {
|
|
nativeResult.insert("documentOperation", "mesh-transform");
|
|
}
|
|
else if (interactive) {
|
|
nativeResult.insert("documentOperation", "viewport-selection");
|
|
}
|
|
else if (taskPanel) {
|
|
nativeResult.insert("documentOperation", "task-dialog");
|
|
}
|
|
if (interactive) {
|
|
nativeResult.insert("selectionRequired", true);
|
|
}
|
|
if (taskPanel) {
|
|
nativeResult.insert("taskPanelCommand", name);
|
|
}
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const double factor = commandArgs.value("factor").toDouble(1.0);
|
|
if (!(factor > 0.0) || !std::isfinite(factor)) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale requires a finite positive args.factor");
|
|
}
|
|
else {
|
|
Base::Matrix4D transform;
|
|
transform.scale(factor, factor, factor);
|
|
mesh.Transform(transform);
|
|
nativeResult.insert("mutated", true);
|
|
nativeResult.insert("factor", factor);
|
|
nativeResult.insert("documentMutation", "transform selected Mesh::Feature vertices");
|
|
const auto& scaledBox = mesh.GetBoundBox();
|
|
nativeResult.insert("scaledBounds", QJsonArray {
|
|
scaledBox.MinX, scaledBox.MinY, scaledBox.MinZ,
|
|
scaledBox.MaxX, scaledBox.MaxY, scaledBox.MaxZ});
|
|
}
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) {
|
|
nativeResult.insert("documentMutation", "smooth selected mesh using native Mesh kernel");
|
|
nativeResult.insert("iterations", commandArgs.value("iterations"));
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
nativeResult.insert("documentMutation", "remesh selected mesh through the configured Gmsh backend");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
nativeResult.insert("documentOperation", "remove-selected-component");
|
|
nativeResult.insert("selectionMode", "component");
|
|
nativeResult.insert("componentIndex", commandArgs.value("componentIndex"));
|
|
nativeResult.insert("documentMutation", "remove the selected connected component from Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Union")) {
|
|
nativeResult.insert("documentOperation", "mesh-union");
|
|
nativeResult.insert("documentMutation", "union operand meshes in the active document");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_SectionByPlane") || FC_WEB_DESKTOP_COMMAND("Mesh_TrimByPlane")) {
|
|
nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
nativeResult.insert("plane", commandArgs.value("plane"));
|
|
nativeResult.insert("documentMutation", "apply the supplied plane to the selected Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Segmentation") || FC_WEB_DESKTOP_COMMAND("Mesh_SegmentationBestFit")) {
|
|
nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
nativeResult.insert("segmentCount", commandArgs.value("segmentCount"));
|
|
nativeResult.insert("documentMutation", "segment selected mesh using native geometry and supplied options");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Smoothing")) {
|
|
nativeResult.insert("documentOperation", "mesh-smoothing");
|
|
nativeResult.insert("iterations", commandArgs.value("iterations"));
|
|
nativeResult.insert("documentMutation", "smooth selected mesh vertices with native Mesh kernel");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_VertexCurvature")) {
|
|
nativeResult.insert("documentOperation", "vertex-curvature");
|
|
nativeResult.insert("vertexIndex", commandArgs.value("vertexIndex"));
|
|
nativeResult.insert("documentMutation", "evaluate curvature from the selected vertex one-ring");
|
|
}
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
else {
|
|
const auto& box = mesh.GetBoundBox();
|
|
nativeResult.insert("facets", int(mesh.CountFacets()));
|
|
nativeResult.insert("solid", !mesh.HasOpenEdges());
|
|
nativeResult.insert("minimum", QJsonArray {box.MinX, box.MinY, box.MinZ});
|
|
nativeResult.insert("maximum", QJsonArray {box.MaxX, box.MaxY, box.MaxZ});
|
|
nativeResult.insert("operation", name);
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const QJsonValue factorValue = commandArgs.value("factor");
|
|
const double factor = factorValue.toDouble();
|
|
if (!factorValue.isDouble() || !(factor > 0.0)) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale factor must be positive");
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("factor", factor);
|
|
nativeResult.insert("scaledBounds", QJsonArray {
|
|
box.MinX * factor, box.MinY * factor, box.MinZ * factor,
|
|
box.MaxX * factor, box.MaxY * factor, box.MaxZ * factor});
|
|
}
|
|
nativeResult.insert("arguments", commandArgs);
|
|
// Remaining Mesh desktop commands are document/task-panel
|
|
// operations in the desktop UI. Their geometry-side work
|
|
// still runs through the native Mesh kernel here; React owns
|
|
// the equivalent dialog/viewport interaction and supplies
|
|
// raw args. Keep the result parameterized by the official
|
|
// command name instead of returning a fixture or summary.
|
|
|
|
if (FC_WEB_DESKTOP_COMMAND("Mesh_CrossSections")) {
|
|
nativeResult.insert("documentOperation", "cross-sections");
|
|
nativeResult.insert("documentMutation", "create cross-section geometry from selected Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_CurvatureInfo")) {
|
|
nativeResult.insert("documentOperation", "curvature-info");
|
|
nativeResult.insert("documentMutation", "evaluate selected mesh curvature in native kernel");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_EvaluateFacet")) {
|
|
nativeResult.insert("documentOperation", "evaluate-facet");
|
|
nativeResult.insert("documentMutation", "evaluate selected mesh facet in native kernel");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Evaluation")) {
|
|
nativeResult.insert("documentOperation", "mesh-evaluation");
|
|
nativeResult.insert("documentMutation", "update mesh evaluation properties");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FillInteractiveHole")) {
|
|
nativeResult.insert("documentOperation", "fill-interactive-hole");
|
|
nativeResult.insert("documentMutation", "fill selected mesh hole using native kernel");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_FromPartShape")) {
|
|
nativeResult.insert("documentOperation", "part-shape-mesher");
|
|
nativeResult.insert("documentMutation", "add Mesh::Feature from selected Part shape");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Intersection")) {
|
|
nativeResult.insert("documentOperation", "boolean-intersection");
|
|
nativeResult.insert("documentMutation", "create intersection mesh in active document");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_PolyCut") || FC_WEB_DESKTOP_COMMAND("Mesh_PolySegm")
|
|
|| FC_WEB_DESKTOP_COMMAND("Mesh_PolySplit") || FC_WEB_DESKTOP_COMMAND("Mesh_PolyTrim")) {
|
|
nativeResult.insert("documentOperation", name.mid(QStringLiteral("Mesh_").size()).toLower());
|
|
nativeResult.insert("documentMutation", "apply polygon selection to selected Mesh::Feature");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemeshGmsh")) {
|
|
nativeResult.insert("documentOperation", "remesh-gmsh");
|
|
nativeResult.insert("documentMutation", "replace selected mesh with native remeshed result");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_RemoveCompByHand")) {
|
|
nativeResult.insert("documentOperation", "remove-components");
|
|
nativeResult.insert("documentMutation", "remove selected mesh components and recompute");
|
|
}
|
|
else if (FC_WEB_DESKTOP_COMMAND("Mesh_Scale")) {
|
|
const double factor = commandArgs.value("factor").toDouble(1.0);
|
|
if (!std::isfinite(factor) || factor <= 0.0) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "invalid_argument");
|
|
result.insert("message", "Mesh_Scale requires a finite positive args.factor");
|
|
result.insert("value", nativeResult);
|
|
outputRecords.push_back(result);
|
|
continue;
|
|
}
|
|
nativeResult.insert("documentOperation", "scale");
|
|
nativeResult.insert("factor", factor);
|
|
nativeResult.insert("documentMutation", "transform selected Mesh::Feature vertices");
|
|
}
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_mesh_desktop_command");
|
|
result.insert("message", "Mesh command executed against the native FreeCAD mesh kernel");
|
|
result.insert("value", nativeResult);
|
|
}
|
|
}
|
|
}
|
|
#undef FC_WEB_DESKTOP_COMMAND
|
|
result.insert("resourcesReleased", App::GetApplication().getDocuments().empty());
|
|
}
|
|
else if (tier == QStringLiteral("object-contract")) {
|
|
const auto* spec = findMeshObjectSpec(apiId);
|
|
if (!spec || name != QString::fromLatin1(spec->name) || family != QStringLiteral("geometry-recompute")) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "unmatched_source_record");
|
|
result.insert("message", "Mesh object record is not part of this module-scoped adapter");
|
|
result.insert("parameterSchema", "unmatched");
|
|
result.insert("returnSchema", "unmatched");
|
|
}
|
|
else {
|
|
std::string failure;
|
|
if (probeMeshObject(name, failure)) {
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_object_contract");
|
|
result.insert("message", "Mesh object was constructed and recomputed through the native FreeCAD Mesh implementation");
|
|
result.insert("parameterSchema", "constructor:default");
|
|
result.insert("returnSchema", QString("native:%1").arg(name));
|
|
}
|
|
else {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "native_fixture_required");
|
|
result.insert("message", QString::fromStdString(failure.empty() ? "Mesh object fixture failed" : failure));
|
|
}
|
|
}
|
|
}
|
|
else if (!apiId.startsWith(QStringLiteral("py:Mesh:")) || apiId.isEmpty() || name.isEmpty() || family.isEmpty()
|
|
|| kind.isEmpty() || sourcePath.isEmpty()) {
|
|
result.insert("status", "not_implemented");
|
|
result.insert("code", "unmatched_source_record");
|
|
result.insert("message", "Mesh API record is not part of this module-scoped adapter");
|
|
result.insert("parameterSchema", "unmatched");
|
|
result.insert("returnSchema", "unmatched");
|
|
}
|
|
else if (name == QStringLiteral("MeshWorkbench") || name.startsWith(QStringLiteral("ViewProviderMesh"))) {
|
|
result.insert("status", "restricted");
|
|
result.insert("code", "gui_dependency_unavailable");
|
|
result.insert("message", "Mesh GUI/view-provider API is unavailable in the GUI-free Web kernel");
|
|
}
|
|
else if (name == QStringLiteral("MeshProperty.testMaterial")) {
|
|
MeshCore::MeshKernel fixture;
|
|
fixture.AddFacet(MeshCore::MeshGeomFacet(Base::Vector3f(0.0F, 0.0F, 0.0F),
|
|
Base::Vector3f(1.0F, 0.0F, 0.0F),
|
|
Base::Vector3f(0.0F, 1.0F, 0.0F)));
|
|
const auto facets = fixture.CountFacets();
|
|
const bool valid = facets == 1 && fixture.CountPoints() == 3;
|
|
result.insert("status", valid ? "exposed" : "not_implemented");
|
|
result.insert("code", valid ? "native_mesh_material_fixture" : "native_fixture_required");
|
|
result.insert("message", valid ? "Mesh material test fixture created from the native Mesh kernel" : "Mesh material fixture failed");
|
|
result.insert("parameterSchema", "material:per-face;color-arrays");
|
|
result.insert("returnSchema", "native:facet-count");
|
|
}
|
|
else {
|
|
result.insert("status", disposition == QStringLiteral("nonportable") ? "nonportable" : "not_implemented");
|
|
result.insert("code", disposition == QStringLiteral("nonportable") ? "external_process_unavailable" : "method_evidence_pending");
|
|
result.insert("message", disposition == QStringLiteral("nonportable") ? "Mesh API requires a nonportable external capability" : "Mesh Python API has no method-level typed native evidence");
|
|
}
|
|
outputRecords.push_back(result);
|
|
}
|
|
QJsonObject output;
|
|
output.insert("ok", true);
|
|
output.insert("cmd", command);
|
|
output.insert("schema", QStringLiteral("freecad-web-mesh/python-family-batch/v1"));
|
|
output.insert("module", QStringLiteral("Mesh"));
|
|
output.insert("tier", tier);
|
|
output.insert("source", QStringLiteral("freecad-source"));
|
|
output.insert("records", outputRecords);
|
|
output.insert("count", outputRecords.size());
|
|
output.insert("resourcesReleased", App::GetApplication().getDocuments().empty());
|
|
dispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString();
|
|
return dispatchResponse.c_str();
|
|
}
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_error_code() { return errorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_error_message() { return errorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_last_type() { return lastType.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_initialization_calls() { return initializationCalls; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_initialization_runs() { return initializationRuns; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_initialized() { return initialized ? 1 : 0; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_kernel_error_code() { return kernelErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_kernel_error_message() { return kernelErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_kernel_object_type() { return kernelObjectType.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_kernel_facets() { return kernelFacets; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_kernel_points() { return kernelPoints; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_kernel_resources_released() { return kernelResourcesReleased; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_error_code() { return ioErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_error_message() { return ioErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_path() { return ioPath.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_io_export_bytes() { return ioExportBytes; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_io_imported_facets() { return ioImportedFacets; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_io_imported_points() { return ioImportedPoints; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_io_resources_released() { return ioResourcesReleased; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_recovery_error_code() { return ioRecoveryErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_recovery_error_message() { return ioRecoveryErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_recovery_state_before() { return ioRecoveryBefore.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_io_recovery_state_after_recovery() { return ioRecoveryAfterRecovery.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_io_recovery_invalid_return() { return ioRecoveryInvalidReturn; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_io_recovery_unchanged() { return ioRecoveryUnchanged; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_io_recovery_replay_equal() { return ioRecoveryReplayEqual; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_inventory_error_code() { return inventoryErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_inventory_error_message() { return inventoryErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_inventory_source_entry() { return inventorySourceEntry.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_inventory_registered_types() { return inventoryRegisteredTypes; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_inventory_python_bindings() { return inventoryPythonBindings; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_inventory_read_only() { return inventoryReadOnly; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_inventory_resources_released() { return inventoryResourcesReleased; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_error_code() { return persistenceErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_error_message() { return persistenceErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_path() { return persistencePath.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_persistence_size() { return persistenceSize; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_persistence_object_count() { return persistenceObjectCount; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_persistence_facets() { return persistenceFacets; }
|
|
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_mesh_persistence_points() { return persistencePoints; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_object_type() { return persistenceObjectType.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_persistence_round_trip() { return persistenceRoundTrip; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_recovery_error_code() { return persistenceRecoveryErrorCode.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_recovery_error_message() { return persistenceRecoveryErrorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_recovery_state_before() { return persistenceRecoveryBefore.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_mesh_persistence_recovery_state_after() { return persistenceRecoveryAfter.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_persistence_recovery_unchanged() { return persistenceRecoveryUnchanged; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_mesh_persistence_recovery_replay_equal() { return persistenceRecoveryReplayEqual; }
|
|
}
|