// SPDX-License-Identifier: LGPL-2.1-or-later #include "MeshComponentCommands.h" #include "MeshPrimitiveCommands.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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(object)) curvature->Source.setValue(source); if (auto* defects = dynamic_cast(object)) defects->Source.setValue(source); if (auto* segment = dynamic_cast(object)) { segment->Source.setValue(source); segment->Tool.setValue(source); } if (auto* operations = dynamic_cast(object)) { operations->Source1.setValue(source); operations->Source2.setValue(source); } if (auto* exporter = dynamic_cast(object)) { exporter->Source.setValue(source); exporter->FileName.setValue("/tmp/mesh-object-contract.stl"); exporter->Format.setValue("STL"); } if (auto* importer = dynamic_cast(object)) importer->FileName.setValue("/tmp/mesh-object-contract.stl"); if (auto* transform = dynamic_cast(object)) transform->Source.setValue(source); if (auto* demolding = dynamic_cast(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(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(feature->Mesh.getValue().countFacets()); kernelPoints = static_cast(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(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(imported.countFacets()); ioImportedPoints = static_cast(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(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(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(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(restored->Mesh.getValue().countFacets()); persistencePoints = static_cast(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(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(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 << "\n"; for (int i = 0; i < 3; ++i) stream << "" << points[i].x << "" << points[i].y << "" << points[i].z << ""; stream << "012"; amf.close(); } else source.save(storagePath.toUtf8().constData(), outputFormat); const unsigned int bytes = static_cast(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 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 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; } }