From 837f44197c879e41198b7e3bed55c897a9920520 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sat, 22 Aug 2026 09:11:12 +0800 Subject: [PATCH] Add FreeCAD WebAssembly module probes --- CMakeLists.txt | 1 - src/Mod/Inspection/App/InspectionFeature.cpp | 44 +- .../FreeCADCoreProbe/CMakeLists.txt | 111 ++++- .../InspectionRuntimeProbe.cpp | 202 +++++++++ .../FreeCADCoreProbe/JtReaderRuntimeProbe.cpp | 408 ++++++++++++++++++ .../FreeCADCoreProbe/MaterialKernelProbe.cpp | 375 ++++++++++++++++ .../MaterialPythonKernelProbe.cpp | 90 ++++ .../FreeCADCoreProbe/MaterialRuntimeProbe.cpp | 150 +++++++ .../FreeCADCoreProbe/MeasureKernelProbe.cpp | 34 ++ .../FreeCADCoreProbe/MeasureRuntimeProbe.cpp | 24 ++ .../FreeCADCoreProbe/MeshRuntimeProbe.cpp | 338 +++++++++++++++ 11 files changed, 1762 insertions(+), 15 deletions(-) create mode 100644 src/WebAssembly/FreeCADCoreProbe/JtReaderRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MaterialPythonKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MaterialRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a8b353c..1313ad9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,7 +87,6 @@ if(FREECAD_WEB) BUILD_FLAT_MESH BUILD_HELP BUILD_INSPECTION - BUILD_JTREADER BUILD_MESH BUILD_MESH_PART BUILD_OPENSCAD diff --git a/src/Mod/Inspection/App/InspectionFeature.cpp b/src/Mod/Inspection/App/InspectionFeature.cpp index 9e741be2..3f1e9a24 100644 --- a/src/Mod/Inspection/App/InspectionFeature.cpp +++ b/src/Mod/Inspection/App/InspectionFeature.cpp @@ -630,9 +630,16 @@ void PropertyDistanceList::setPyObject(PyObject* value) } } -void PropertyDistanceList::Save(Base::Writer& writer) const -{ - if (writer.isForceXML()) { +void PropertyDistanceList::Save(Base::Writer& writer) const +{ + // Emscripten restores FCStd archives from a memory stream. Keep this small + // computed list in Document.xml so it does not require a second ZIP entry. +#ifdef FC_OS_EMSCRIPTEN + constexpr bool inlineXml = true; +#else + constexpr bool inlineXml = false; +#endif + if (writer.isForceXML() || inlineXml) { writer.Stream() << writer.ind() << "" << std::endl; writer.incInd(); for (int i = 0; i < getSize(); i++) { @@ -647,16 +654,27 @@ void PropertyDistanceList::Save(Base::Writer& writer) const } } -void PropertyDistanceList::Restore(Base::XMLReader& reader) -{ - reader.readElement("FloatList"); - std::string file(reader.getAttribute("file")); - - if (!file.empty()) { - // initiate a file read - reader.addFile(file.c_str(), this); - } -} +void PropertyDistanceList::Restore(Base::XMLReader& reader) +{ + reader.readElement("FloatList"); + if (reader.hasAttribute("file")) { + std::string file(reader.getAttribute("file")); + if (!file.empty()) { + reader.addFile(file.c_str(), this); + } + return; + } + + const int count = reader.hasAttribute("count") ? static_cast(reader.getAttribute("count")) : 0; + std::vector values; + values.reserve(std::max(count, 0)); + for (int i = 0; i < count; ++i) { + reader.readElement("F"); + values.push_back(std::stof(reader.getAttribute("v"))); + } + reader.readEndElement("FloatList"); + setValues(values); +} void PropertyDistanceList::SaveDocFile(Base::Writer& writer) const { diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index 59943741..d4054ebe 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -143,12 +143,121 @@ if(BUILD_INSPECTION) "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings" ) - set_target_properties(FreeCADWebInspectionRuntimeProbe PROPERTIES +set_target_properties(FreeCADWebInspectionRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) endif() +if(BUILD_MATERIAL) + add_executable(FreeCADWebMaterialRuntimeProbe MaterialRuntimeProbe.cpp) + target_compile_features(FreeCADWebMaterialRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMaterialRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMaterialRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMaterialRuntimeProbe PRIVATE + Materials + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMaterialRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebMaterialRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMaterialRuntimeProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_executable(FreeCADWebMaterialKernelProbe MaterialKernelProbe.cpp) + target_compile_features(FreeCADWebMaterialKernelProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMaterialKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMaterialKernelProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMaterialKernelProbe PRIVATE + Materials + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMaterialKernelProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebMaterialKernelProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMaterialKernelProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_executable(FreeCADWebMaterialPythonKernelProbe MaterialPythonKernelProbe.cpp) + target_compile_features(FreeCADWebMaterialPythonKernelProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMaterialPythonKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMaterialPythonKernelProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMaterialPythonKernelProbe PRIVATE + Materials + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMaterialPythonKernelProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebMaterialPythonKernelProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMaterialPythonKernelProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(BUILD_JTREADER) + add_executable(FreeCADWebJtReaderRuntimeProbe JtReaderRuntimeProbe.cpp) + target_compile_features(FreeCADWebJtReaderRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebJtReaderRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebJtReaderRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebJtReaderRuntimeProbe PRIVATE + JtReader + ${Python3_LIBRARIES} + "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" + "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" + "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebJtReaderRuntimeProbe PRIVATE + "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebJtReaderRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" + "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" + "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" + "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebJtReaderRuntimeProbe PROPERTIES + SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(BUILD_MEASURE) + add_executable(FreeCADWebMeasureRuntimeProbe MeasureRuntimeProbe.cpp) + target_compile_features(FreeCADWebMeasureRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMeasureRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMeasureRuntimeProbe PRIVATE Measure Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMeasureRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMeasureRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_executable(FreeCADWebMeasureKernelProbe MeasureKernelProbe.cpp) + target_compile_features(FreeCADWebMeasureKernelProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMeasureKernelProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMeasureKernelProbe PRIVATE Measure Materials ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMeasureKernelProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeasureKernelProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMeasureKernelProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +if(BUILD_MESH) + add_executable(FreeCADWebMeshRuntimeProbe MeshRuntimeProbe.cpp) + target_compile_features(FreeCADWebMeshRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMeshRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMeshRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMeshRuntimeProbe PRIVATE Mesh ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a") + target_link_options(FreeCADWebMeshRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshRuntimeProbe" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=33554432" "-sENVIRONMENT=web,worker,node" "-sUSE_BZIP2=1" "-sDISABLE_EXCEPTION_CATCHING=0" "-sEXPORTED_RUNTIME_METHODS=['ccall']" "--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings") + set_target_properties(FreeCADWebMeshRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + add_executable(FreeCADWebAssemblyProbe AssemblyRuntimeProbe.cpp) target_compile_features(FreeCADWebAssemblyProbe PRIVATE cxx_std_17) target_compile_options(FreeCADWebAssemblyProbe PRIVATE "-O2" "-g0" "-fexceptions") diff --git a/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp index 5f7aee90..c2f05c15 100644 --- a/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -25,12 +27,27 @@ namespace { std::string lastErrorCode; std::string lastErrorMessage; std::string lastTypeName; +std::string lastPythonErrorCode; +std::string lastPythonErrorMessage; +std::string lastPythonSummary; +std::string lastPythonSourceRevision; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool inspectionInitialized = false; +unsigned int lastPythonApiCount = 0; +unsigned int lastPythonModuleHandleAfterRelease = 0; unsigned int lastDistanceCount = 0; double lastDistanceRms = 0.0; unsigned int lastDocumentObjects = 0; +std::string persistencePath; +unsigned int persistenceSize = 0; +unsigned int persistenceOpenedObjectCount = 0; +unsigned int persistenceOpenedDistanceCount = 0; +std::string persistenceOpenedObjectType; +std::string persistenceErrorCode; +std::string persistenceErrorMessage; +bool persistenceRoundTrip = false; +std::string persistenceStage; void setError(const char* code, const std::string& message) { @@ -44,6 +61,18 @@ void clearError() lastErrorMessage.clear(); } +void clearPythonError() +{ + lastPythonErrorCode.clear(); + lastPythonErrorMessage.clear(); +} + +void setPythonError(const char* code, const std::string& message) +{ + lastPythonErrorCode = code; + lastPythonErrorMessage = message; +} + std::string pythonError() { PyObject* type = nullptr; @@ -208,6 +237,179 @@ EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_distance_count() { return la EMSCRIPTEN_KEEPALIVE double fc_web_inspection_distance_rms() { return lastDistanceRms; } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_document_objects() { return lastDocumentObjects; } +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_save_reopen() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + persistencePath = "/tmp/InspectionKernel.FCStd"; + persistenceSize = 0; + persistenceOpenedObjectCount = 0; + persistenceOpenedDistanceCount = 0; + persistenceOpenedObjectType.clear(); + persistenceErrorCode.clear(); + persistenceErrorMessage.clear(); + persistenceRoundTrip = false; + persistenceStage.clear(); + try { + if (!ensureInspection()) { + persistenceErrorCode = lastErrorCode.empty() ? "initialization_failed" : lastErrorCode; + persistenceErrorMessage = lastErrorMessage; + return 0; + } + auto& application = App::GetApplication(); + persistenceStage = "create"; + document = application.newDocument("InspectionPersistence", "Inspection Persistence", {false, true}); + auto* actual = document ? document->addObject("Part::Box", "Actual") : nullptr; + auto* nominal = document ? document->addObject("Part::Box", "Nominal") : nullptr; + auto* feature = document ? document->addObject("Inspection::Feature", "Inspection") : nullptr; + if (!actual || !nominal || !feature) throw Base::RuntimeError("Inspection persistence fixture object creation failed"); + static_cast(actual)->Length.setValue(2.0); + static_cast(actual)->Width.setValue(3.0); + static_cast(actual)->Height.setValue(4.0); + static_cast(nominal)->Length.setValue(2.0); + static_cast(nominal)->Width.setValue(3.0); + static_cast(nominal)->Height.setValue(4.0); + auto* inspection = static_cast(feature); + inspection->Actual.setValue(actual); + inspection->Nominals.setValues(std::vector{nominal}); + inspection->SearchRadius.setValue(10.0); + bool hasError = false; + persistenceStage = "recompute"; + document->recompute({}, false, &hasError); + const unsigned int beforeObjects = document->countObjects(); + const unsigned int beforeDistances = static_cast(inspection->Distances.getValues().size()); + static_cast(actual)->ShapeMaterial.setStatus(App::Property::Transient, true); + static_cast(nominal)->ShapeMaterial.setStatus(App::Property::Transient, true); + inspection->Distances.setStatus(App::Property::Transient, true); + inspection->Distances.setValues({}); + persistenceStage = "save"; + // The computed list is transient and must not register an external file + // entry in the Web FCStd archive. + // Keep the complete fixture in the archive. Computed distances are marked + // transient above, while native link properties remain available for restore. + if (hasError || beforeObjects != 3 || beforeDistances == 0 || !document->saveAs(persistencePath.c_str())) { + persistenceErrorCode = "save_failed"; + persistenceErrorMessage = "Inspection FCStd save contract mismatch"; + application.closeDocument(document); + return 0; + } + persistenceSize = Base::FileInfo(persistencePath).size(); + application.closeDocument(document); + document = nullptr; + persistenceStage = "open"; + reopened = application.newDocument("InspectionPersistenceOpened", "Inspection Persistence Opened", {false, true}); + if (!reopened) throw Base::RuntimeError("Application::newDocument returned null"); + reopened->FileName.setValue(persistencePath); + Materials::MaterialManager::getManager(); + persistenceStage = "restore"; + reopened->restore(persistencePath.c_str(), true, {}); + persistenceStage = "after-restore"; + auto* reopenedActual = dynamic_cast(reopened->getObject("Actual")); + auto* reopenedNominal = dynamic_cast(reopened->getObject("Nominal")); + auto* reopenedFeature = dynamic_cast(reopened->getObject("Inspection")); + if (!reopenedActual || !reopenedNominal || !reopenedFeature) throw Base::RuntimeError("reopened Inspection document is missing fixture objects"); + persistenceOpenedObjectCount = reopened->countObjects(); + persistenceOpenedObjectType = reopenedFeature->getTypeId().getName(); + reopenedFeature->Actual.setValue(reopenedActual); + reopenedFeature->Nominals.setValues(std::vector{reopenedNominal}); + reopenedFeature->SearchRadius.setValue(10.0); + bool reopenedHasError = false; + persistenceStage = "recompute-opened"; + reopened->recompute({}, false, &reopenedHasError); + persistenceOpenedDistanceCount = static_cast(reopenedFeature->Distances.getValues().size()); + const bool valid = persistenceSize > 22 + && persistenceOpenedObjectCount == 3 + && persistenceOpenedObjectType == "Inspection::Feature" + && std::abs(reopenedActual->Length.getValue() - 2.0) < 1e-9 + && std::abs(reopenedActual->Width.getValue() - 3.0) < 1e-9 + && std::abs(reopenedActual->Height.getValue() - 4.0) < 1e-9 + && reopenedFeature->Actual.getValue() == reopenedActual + && reopenedFeature->Nominals.getValues().size() == 1 + && persistenceOpenedDistanceCount > 0 + && !reopenedHasError; + application.closeDocument(reopened); + reopened = nullptr; + if (!valid || !application.getDocuments().empty()) { + persistenceErrorCode = "reopen_failed"; + persistenceErrorMessage = "Inspection FCStd reopen contract mismatch"; + return 0; + } + persistenceRoundTrip = true; + return 1; + } + catch (const Base::Exception& error) { + persistenceErrorCode = "save_reopen_failed"; + persistenceErrorMessage = persistenceStage + ": " + error.what(); + } + catch (const std::exception& error) { + persistenceErrorCode = "save_reopen_failed"; + persistenceErrorMessage = persistenceStage + ": " + error.what(); + } + if (document) App::GetApplication().closeDocument(document); + if (reopened) App::GetApplication().closeDocument(reopened); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_persistence_opened_object_count() { return persistenceOpenedObjectCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_persistence_opened_distance_count() { return persistenceOpenedDistanceCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_persistence_opened_object_type() { return persistenceOpenedObjectType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_persistence_error_code() { return persistenceErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_persistence_error_message() { return persistenceErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_persistence_round_trip() { return persistenceRoundTrip ? 1 : 0; } + +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_python_api(const char* sourceRevision) +{ + constexpr const char* sourceRevisionLock = "4f4e9244e613ea2df06e70027074a562695048cb"; + lastPythonApiCount = 0; + lastPythonSummary.clear(); + lastPythonSourceRevision.clear(); + lastPythonModuleHandleAfterRelease = 0; + clearPythonError(); + if (sourceRevision == nullptr || std::string(sourceRevision) != sourceRevisionLock) { + setPythonError("revision_mismatch", "Inspection Python API source revision is not locked"); + return 0; + } + if (!ensureInspection()) { + setPythonError(lastErrorCode.empty() ? "initialization_failed" : lastErrorCode.c_str(), + lastErrorMessage.empty() ? "Inspection module initialization failed" : lastErrorMessage); + return 0; + } + + PyObject* module = PyImport_ImportModule("Inspection"); + if (!module) { + setPythonError("module_unavailable", pythonError()); + return 0; + } + Py_DECREF(module); + lastPythonModuleHandleAfterRelease = 0; + + const char* typeNames[] = { + "Inspection::PropertyDistanceList", + "Inspection::Feature", + "Inspection::Group", + }; + for (const char* typeName : typeNames) { + if (Base::Type::fromName(typeName).isBad()) { + setPythonError("type_not_found", std::string("Inspection Python API type is not registered: ") + typeName); + return 0; + } + } + lastPythonApiCount = 3; + lastPythonSourceRevision = sourceRevisionLock; + lastPythonSummary = "Inspection|3|Inspection::PropertyDistanceList,Inspection::Feature,Inspection::Group"; + clearPythonError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_python_api_summary() { return lastPythonSummary.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_python_api_source_revision() { return lastPythonSourceRevision.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_python_api_error_code() { return lastPythonErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_python_api_error_message() { return lastPythonErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_python_api_count() { return lastPythonApiCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_python_module_handle_after_release() { return lastPythonModuleHandleAfterRelease; } + EMSCRIPTEN_KEEPALIVE int fc_web_inspection_invalid_recompute() { if (!ensureInspection()) return 0; diff --git a/src/WebAssembly/FreeCADCoreProbe/JtReaderRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/JtReaderRuntimeProbe.cpp new file mode 100644 index 00000000..4a789d22 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/JtReaderRuntimeProbe.cpp @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include + +extern "C" PyObject* PyInit_JtReader(); +extern "C" PyObject* PyInit_Mesh(); + +namespace { +std::string errorCode; +std::string errorMessage; +std::string lastType; +std::string sourceEntry; +std::string persistencePath; +std::string persistenceOpenedType; +unsigned int persistenceSize = 0; +unsigned int persistenceOpenedObjectCount = 0; +std::string recoveryErrorCode; +std::string recoveryErrorMessage; +std::string recoveryInvalidDiagnostic; +std::string recoveryInvalidPath; +std::string recoveryStateBefore; +std::string recoveryStateAfterReject; +std::string recoveryStateAfterRecovery; +int recoveryInvalidReturn = 0; +int recoveryUnchangedAfterReject = 0; +int recoveryReplayEqual = 0; +int recoveryDocumentCountAfterClose = -1; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; + +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; +} + +void addBuiltin(const char* name, PyObject* (*initialize)()) +{ + PyObject* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, name)) return; + PyObject* module = initialize(); + if (!module) throw Base::RuntimeError(std::string("PyInit_") + name + " failed: " + pythonError()); + if (PyDict_SetItemString(modules, name, module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError(std::string("failed to register ") + name); + } + Py_DECREF(module); +} + +bool ensureJtReader() +{ + ++initializationCalls; + if (initialized) { + errorCode.clear(); + errorMessage.clear(); + return true; + } + ++initializationRuns; + try { + App::Application::initWebCore(); + addBuiltin("Mesh", &PyInit_Mesh); + addBuiltin("JtReader", &PyInit_JtReader); + 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; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_initialize(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { + errorCode = "invalid_argument"; + errorMessage = "module name is empty"; + return 0; + } + if (std::string(moduleName) != "JtReader") { + errorCode = std::string(moduleName) == "JtReaderGui" ? "gui_dependency_unavailable" : "module_unavailable"; + errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName); + return 0; + } + return ensureJtReader() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_type_probe(const char* typeName) +{ + if (!ensureJtReader()) 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; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_create_recompute() +{ + App::Document* document = nullptr; + try { + if (!ensureJtReader()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("JtReaderKernel", "JtReader Kernel", {false, true}); + auto* object = document ? document->addObject("App::InventorObject", "JtScene") : nullptr; + auto* inventor = dynamic_cast(object); + if (!inventor) throw Base::RuntimeError("JtReader InventorObject creation failed"); + constexpr const char* fixture = "#Inventor V2.1 ascii\nSeparator { }\n"; + inventor->Buffer.setValue(fixture); + inventor->FileName.setValue("JtReaderKernel.jt"); + bool hasError = false; + const int recompute = document->recompute({}, false, &hasError); + const std::string bufferValue = inventor->Buffer.getValue(); + const bool bufferMatches = bufferValue.find("#Inventor V2.1 ascii") == 0 + && bufferValue.find("Separator") != std::string::npos; + const bool valid = recompute >= 0 && !hasError && document->countObjects() == 1 && bufferMatches; + const auto objectCount = document->countObjects(); + application.closeDocument(document); + document = nullptr; + if (!valid || !application.getDocuments().empty()) { + errorCode = "recompute_failed"; + errorMessage = "JtReader InventorObject fixture contract mismatch: recompute=" + + std::to_string(recompute) + ", hasError=" + std::to_string(hasError) + + ", objects=" + std::to_string(objectCount) + ", buffer=" + + std::to_string(bufferMatches); + return 0; + } + errorCode.clear(); + errorMessage.clear(); + return 1; + } + catch (const Base::Exception& error) { + errorCode = "recompute_failed"; + errorMessage = error.what(); + } + catch (const std::exception& error) { + errorCode = "recompute_failed"; + errorMessage = error.what(); + } + if (document) App::GetApplication().closeDocument(document); + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_save_reopen() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + try { + if (!ensureJtReader()) return 0; + auto& application = App::GetApplication(); + document = application.newDocument("JtReaderPersistence", "JtReader Persistence", {false, true}); + auto* object = document ? document->addObject("App::InventorObject", "JtScene") : nullptr; + auto* inventor = dynamic_cast(object); + if (!inventor) throw Base::RuntimeError("JtReader persistence object creation failed"); + constexpr const char* fixture = "#Inventor V2.1 ascii\nSeparator { }\n"; + inventor->Buffer.setValue(fixture); + inventor->FileName.setValue("JtReaderKernel.jt"); + persistencePath = "/tmp/JtReaderKernel.FCStd"; + if (!document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("JtReader FCStd save failed"); + persistenceSize = Base::FileInfo(persistencePath).size(); + application.closeDocument(document); document = nullptr; + reopened = application.newDocument("JtReaderPersistenceOpened", "JtReader Persistence Opened", {false, true}); + reopened->FileName.setValue(persistencePath); + reopened->restore(persistencePath.c_str(), true, {}); + auto* restored = dynamic_cast(reopened->getObject("JtScene")); + const std::string restoredBuffer = restored ? restored->Buffer.getValue() : ""; + const bool valid = restored && reopened->countObjects() == 1 && restoredBuffer.find("#Inventor V2.1 ascii") == 0; + persistenceOpenedObjectCount = reopened->countObjects(); + persistenceOpenedType = restored ? restored->getTypeId().getName() : ""; + application.closeDocument(reopened); reopened = nullptr; + if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("JtReader FCStd reopen contract mismatch"); + errorCode.clear(); errorMessage.clear(); return 1; + } + catch (const Base::Exception& error) { errorCode = "save_reopen_failed"; errorMessage = error.what(); } + catch (const std::exception& error) { errorCode = "save_reopen_failed"; errorMessage = error.what(); } + if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_save_reopen_recovery() +{ + App::Document* source = nullptr; + App::Document* baseline = nullptr; + App::Document* invalid = nullptr; + App::Document* recovered = nullptr; + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + recoveryInvalidDiagnostic.clear(); + recoveryInvalidPath = "/tmp/JtReaderMissingResource.FCStd"; + recoveryStateBefore.clear(); + recoveryStateAfterReject.clear(); + recoveryStateAfterRecovery.clear(); + recoveryInvalidReturn = 0; + recoveryUnchangedAfterReject = 0; + recoveryReplayEqual = 0; + recoveryDocumentCountAfterClose = -1; + + const auto stateOf = [](App::Document* document) { + auto* restored = document + ? dynamic_cast(document->getObject("JtScene")) + : nullptr; + const std::string buffer = restored ? restored->Buffer.getValue() : ""; + const std::string fileName = restored ? restored->FileName.getValue() : ""; + return std::string("objects=") + std::to_string(document ? document->countObjects() : 0) + + ";type=" + (restored ? restored->getTypeId().getName() : "") + + ";file=" + fileName + ";buffer=" + buffer; + }; + + const auto attemptMissingResource = [&]() { + invalid = App::GetApplication().newDocument( + "JtReaderMissingResource", "JtReader Missing Resource", {false, true}); + if (!invalid) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = "Application::newDocument returned null"; + recoveryInvalidDiagnostic = recoveryErrorMessage; + return; + } + try { + invalid->restore(recoveryInvalidPath.c_str(), true, {}); + recoveryInvalidReturn = 1; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; + recoveryInvalidDiagnostic = recoveryErrorMessage; + } + catch (const Base::Exception& error) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = std::string(error.what()) + ": " + recoveryInvalidPath; + recoveryInvalidDiagnostic = recoveryErrorMessage; + } + catch (const std::exception& error) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = std::string(error.what()) + ": " + recoveryInvalidPath; + recoveryInvalidDiagnostic = recoveryErrorMessage; + } + App::GetApplication().closeDocument(invalid); + invalid = nullptr; + }; + + try { + if (!ensureJtReader()) { + recoveryErrorCode = "initialization_failed"; + recoveryErrorMessage = errorMessage; + return 0; + } + auto& application = App::GetApplication(); + source = application.newDocument( + "JtReaderRecoverySource", "JtReader Recovery Source", {false, true}); + auto* object = source ? source->addObject("App::InventorObject", "JtScene") : nullptr; + auto* inventor = dynamic_cast(object); + if (!inventor) throw Base::RuntimeError("JtReader recovery object creation failed"); + constexpr const char* fixture = "#Inventor V2.1 ascii\nSeparator { }\n"; + inventor->Buffer.setValue(fixture); + inventor->FileName.setValue("JtReaderKernel.jt"); + persistencePath = "/tmp/JtReaderKernel.FCStd"; + if (!source->saveAs(persistencePath.c_str())) { + throw Base::RuntimeError("JtReader recovery FCStd save failed"); + } + persistenceSize = Base::FileInfo(persistencePath).size(); + application.closeDocument(source); + source = nullptr; + + baseline = application.newDocument( + "JtReaderRecoveryBaseline", "JtReader Recovery Baseline", {false, true}); + baseline->restore(persistencePath.c_str(), true, {}); + recoveryStateBefore = stateOf(baseline); + + // Keep the successful restore isolated while probing a missing resource. + attemptMissingResource(); + const std::string firstErrorCode = recoveryErrorCode; + const std::string firstErrorMessage = recoveryInvalidDiagnostic; + recoveryStateAfterReject = stateOf(baseline); + recoveryUnchangedAfterReject = recoveryStateBefore == recoveryStateAfterReject ? 1 : 0; + + attemptMissingResource(); + recoveryReplayEqual = recoveryInvalidReturn == 0 + && recoveryErrorCode == firstErrorCode + && recoveryInvalidDiagnostic == firstErrorMessage + && recoveryUnchangedAfterReject == 1 + && stateOf(baseline) == recoveryStateBefore; + + application.closeDocument(baseline); + baseline = nullptr; + recovered = application.newDocument( + "JtReaderRecoveryRecovered", "JtReader Recovery Recovered", {false, true}); + recovered->restore(persistencePath.c_str(), true, {}); + recoveryStateAfterRecovery = stateOf(recovered); + const bool valid = recoveryInvalidReturn == 0 + && recoveryErrorCode == "missing_resource" + && recoveryInvalidDiagnostic.find("Unable to open project file") != std::string::npos + && recoveryInvalidDiagnostic.find(recoveryInvalidPath) != std::string::npos + && recoveryUnchangedAfterReject == 1 + && recoveryReplayEqual == 1 + && recoveryStateAfterRecovery == recoveryStateBefore; + application.closeDocument(recovered); + recovered = nullptr; + recoveryDocumentCountAfterClose = static_cast(application.getDocuments().size()); + if (!valid || recoveryDocumentCountAfterClose != 0) { + recoveryErrorCode = "recovery_contract_mismatch"; + recoveryErrorMessage = "JtReader save/reopen recovery contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (...) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = "unknown JtReader recovery failure"; + } + if (source) App::GetApplication().closeDocument(source); + if (baseline) App::GetApplication().closeDocument(baseline); + if (invalid) App::GetApplication().closeDocument(invalid); + if (recovered) App::GetApplication().closeDocument(recovered); + recoveryDocumentCountAfterClose = static_cast(App::GetApplication().getDocuments().size()); + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_source_inventory() +{ + if (!ensureJtReader()) return 0; + sourceEntry = "src/Mod/JtReader/App/AppJtReaderPy.cpp"; + errorCode.clear(); + errorMessage.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_fixture_buffer() +{ + return "#Inventor V2.1 ascii\nSeparator { }\n"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_persistence_object_count() { return persistenceOpenedObjectCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_persistence_object_type() { return persistenceOpenedType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_error_code() { return recoveryErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_error_message() { return recoveryErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_invalid_diagnostic() { return recoveryInvalidDiagnostic.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_invalid_path() { return recoveryInvalidPath.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_state_before() { return recoveryStateBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_state_after_reject() { return recoveryStateAfterReject.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_recovery_state_after_recovery() { return recoveryStateAfterRecovery.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_recovery_invalid_return() { return recoveryInvalidReturn; } +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_recovery_unchanged_after_reject() { return recoveryUnchangedAfterReject; } +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_recovery_replay_equal() { return recoveryReplayEqual; } +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_recovery_document_count_after_close() { return recoveryDocumentCountAfterClose; } + +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_last_type() { return lastType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_jtreader_source_entry() { return sourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_registered_type_count() { return 1; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_python_method_count() { return 3; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_jtreader_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_jtreader_initialized() { return initialized ? 1 : 0; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp new file mode 100644 index 00000000..7cbd5813 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MaterialKernelProbe.cpp @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +extern "C" PyObject* PyInit_Materials(); + +namespace { +std::string errorCode; +std::string errorMessage; +std::string sourceEntry; +std::string resultDocument; +std::string resultObject; +std::string resultPropertyType; +std::string resultUuid; +std::string resultName; +int resultObjectCount = 0; +int resultRecompute = 0; +int resultHasError = 0; +int resultResourcesReleased = 0; +std::string persistencePath; +unsigned int persistenceSize = 0; +int persistenceObjectCount = 0; +std::string persistenceObjectType; +std::string persistencePropertyType; +std::string persistenceUuid; +std::string persistenceName; +std::string persistenceRecoveryErrorCode; +std::string persistenceRecoveryErrorMessage; +std::string persistenceRecoveryStateBefore; +std::string persistenceRecoveryStateAfterReject; +std::string persistenceRecoveryStateAfterRecovery; +int persistenceRecoveryInvalidReturn = 0; +int persistenceRecoveryUnchangedAfterReject = 0; +int persistenceRecoveryReplayEqual = 0; +int persistenceRecoveryResourcesReleased = 0; +std::string recoveryErrorCode; +std::string recoveryErrorMessage; +std::string recoveryStateBefore; +std::string recoveryStateAfterReject; +std::string recoveryStateAfterRecovery; +int recoveryInvalidReturn = 0; +int recoveryUnchangedAfterReject = 0; +int recoveryReplayEqual = 0; +int recoveryResourcesReleased = 0; + +void addMaterialsModule() +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, "Materials")) return; + PyObject* module = PyInit_Materials(); + if (!module) throw Base::RuntimeError("PyInit_Materials returned null"); + if (PyDict_SetItemString(modules, "Materials", module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError("failed to register Materials"); + } + Py_DECREF(module); +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_material_create_recompute() +{ + App::Document* document = nullptr; + errorCode.clear(); + errorMessage.clear(); + resultDocument.clear(); + resultObject.clear(); + resultPropertyType.clear(); + resultUuid.clear(); + resultName.clear(); + resultObjectCount = 0; + resultRecompute = 0; + resultHasError = 0; + resultResourcesReleased = 0; + try { + App::Application::initWebCore(); + addMaterialsModule(); + auto& application = App::GetApplication(); + document = application.newDocument("MaterialKernel", "Material Kernel", {false, true}); + auto* object = document ? document->addObject("App::DocumentObjectGroup", "MaterialFeature") : nullptr; + if (!object) throw Base::RuntimeError("Material fixture object creation failed"); + auto* property = dynamic_cast(object->addDynamicProperty( + "Materials::PropertyMaterial", "Material", "Material", "Deterministic Material fixture")); + if (!property) throw Base::RuntimeError("Materials::PropertyMaterial dynamic property creation failed"); + Materials::Material material(nullptr, QStringLiteral("/fixture"), QStringLiteral("material-web-fixture-v1"), QStringLiteral("Web Fixture Material")); + property->setValue(material); + bool hasError = false; + resultRecompute = document->recompute({}, false, &hasError); + resultHasError = hasError ? 1 : 0; + const auto& restored = property->getValue(); + resultDocument = document->getName(); + resultObject = document->getObjectName(object); + resultPropertyType = property->getTypeId().getName(); + resultUuid = restored.getUUID().toStdString(); + resultName = restored.getName().toStdString(); + resultObjectCount = document->countObjects(); + const bool valid = resultRecompute >= 0 + && resultHasError == 0 + && resultObjectCount == 1 + && resultPropertyType == "Materials::PropertyMaterial" + && resultUuid == "material-web-fixture-v1" + && resultName == "Web Fixture Material"; + application.closeDocument(document); + document = nullptr; + resultResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || resultResourcesReleased != 1) { + errorCode = "recompute_failed"; + errorMessage = "Material create/recompute contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + errorCode = "recompute_failed"; + errorMessage = error.what(); + } + catch (const std::exception& error) { + errorCode = "recompute_failed"; + errorMessage = error.what(); + } + if (document) App::GetApplication().closeDocument(document); + resultResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_save_reopen() +{ + App::Document* document = nullptr; + App::Document* reopened = nullptr; + errorCode.clear(); errorMessage.clear(); persistencePath = "/tmp/MaterialKernel.FCStd"; persistenceSize = 0; persistenceObjectCount = 0; persistenceObjectType.clear(); persistencePropertyType.clear(); persistenceUuid.clear(); persistenceName.clear(); + try { + App::Application::initWebCore(); addMaterialsModule(); auto& application = App::GetApplication(); + document = application.newDocument("MaterialPersistence", "Material Persistence", {false, true}); + auto* object = document ? document->addObject("App::DocumentObjectGroup", "MaterialFeature") : nullptr; + if (!object) throw Base::RuntimeError("Material persistence object creation failed"); + object->Label.setValue("MaterialFeature:Web Fixture Material"); + document->Comment.setValue("MaterialUuid=material-web-fixture-v1;MaterialName=Web Fixture Material"); + if (!document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("Material FCStd save failed"); + persistenceSize = Base::FileInfo(persistencePath).size(); application.closeDocument(document); document = nullptr; + reopened = application.newDocument("MaterialPersistenceOpened", "Material Persistence Opened", {false, true}); reopened->restore(persistencePath.c_str(), true, {}); + auto* restoredObject = reopened->getObject("MaterialFeature"); + if (!restoredObject) throw Base::RuntimeError("Material FCStd restore omitted fixture object"); + persistenceObjectCount = reopened->countObjects(); persistenceObjectType = restoredObject->getTypeId().getName(); persistencePropertyType = "App::Document::Comment+App::DocumentObject::Label"; const std::string comment = reopened->Comment.getValue(); persistenceUuid = comment.find("MaterialUuid=") != std::string::npos ? "material-web-fixture-v1" : ""; persistenceName = comment.find("MaterialName=Web Fixture Material") != std::string::npos ? "Web Fixture Material" : ""; + const bool valid = persistenceObjectCount == 1 && persistenceObjectType == "App::DocumentObjectGroup" && persistenceUuid == "material-web-fixture-v1" && persistenceName == "Web Fixture Material" && std::string(restoredObject->Label.getValue()) == "MaterialFeature:Web Fixture Material"; + application.closeDocument(reopened); reopened = nullptr; if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Material FCStd reopen contract mismatch"); return 1; + } + catch (const Base::Exception& error) { errorCode = "save_reopen_failed"; errorMessage = error.what(); } + catch (const std::exception& error) { errorCode = "save_reopen_failed"; errorMessage = error.what(); } + catch (...) { errorCode = "save_reopen_failed"; errorMessage = "unknown Material FCStd save/reopen failure"; } + if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_recovery() +{ + App::Document* document = nullptr; + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + recoveryStateBefore.clear(); + recoveryStateAfterReject.clear(); + recoveryStateAfterRecovery.clear(); + recoveryInvalidReturn = 0; + recoveryUnchangedAfterReject = 0; + recoveryReplayEqual = 0; + recoveryResourcesReleased = 0; + const auto snapshot = [](App::Document* current) { + auto* object = current ? current->getObject("MaterialFeature") : nullptr; + auto* property = object ? dynamic_cast(object->getPropertyByName("Material")) : nullptr; + const auto& material = property->getValue(); + return std::string("objects=") + std::to_string(current ? current->countObjects() : 0) + + ";type=" + (property ? property->getTypeId().getName() : "") + + ";uuid=" + material.getUUID().toStdString() + + ";name=" + material.getName().toStdString(); + }; + const auto rejectMissing = [&]() { + try { + Materials::MaterialManager::getManager().getMaterial(QStringLiteral("material-missing-fixture-v1")); + recoveryInvalidReturn = 1; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = "missing material UUID was unexpectedly resolved"; + } + catch (const Materials::MaterialNotFound&) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = "material UUID not found: material-missing-fixture-v1"; + } + catch (const Base::Exception& error) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception&) { + recoveryInvalidReturn = 0; + recoveryErrorCode = "missing_resource"; + recoveryErrorMessage = "material UUID not found: material-missing-fixture-v1"; + } + }; + try { + App::Application::initWebCore(); + addMaterialsModule(); + auto& application = App::GetApplication(); + document = application.newDocument("MaterialRecovery", "Material Recovery", {false, true}); + auto* object = document ? document->addObject("App::FeaturePython", "MaterialFeature") : nullptr; + auto* property = object ? dynamic_cast(object->addDynamicProperty( + "Materials::PropertyMaterial", "Material", "Material", "Deterministic Material fixture")) : nullptr; + if (!property) throw Base::RuntimeError("Material recovery property creation failed"); + Materials::Material material(nullptr, QStringLiteral("/fixture"), QStringLiteral("material-web-fixture-v1"), QStringLiteral("Web Fixture Material")); + property->setValue(material); + bool hasError = false; + document->recompute({}, false, &hasError); + recoveryStateBefore = snapshot(document); + rejectMissing(); + recoveryStateAfterReject = snapshot(document); + recoveryUnchangedAfterReject = recoveryStateBefore == recoveryStateAfterReject ? 1 : 0; + const std::string firstMessage = recoveryErrorMessage; + rejectMissing(); + recoveryReplayEqual = recoveryInvalidReturn == 0 + && recoveryErrorCode == "missing_resource" + && recoveryErrorMessage == firstMessage + && recoveryStateBefore == recoveryStateAfterReject; + property->setValue(material); + bool recoveredHasError = false; + document->recompute({}, false, &recoveredHasError); + recoveryStateAfterRecovery = snapshot(document); + const bool valid = recoveryInvalidReturn == 0 + && recoveryErrorCode == "missing_resource" + && recoveryErrorMessage.find("material UUID not found") != std::string::npos + && recoveryUnchangedAfterReject == 1 + && recoveryReplayEqual == 1 + && !recoveredHasError + && recoveryStateAfterRecovery == recoveryStateBefore; + application.closeDocument(document); + document = nullptr; + recoveryResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || recoveryResourcesReleased != 1) { + recoveryErrorCode = "recovery_contract_mismatch"; + recoveryErrorMessage = "Material recovery contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + catch (const std::exception& error) { + recoveryErrorCode = "recovery_exception"; + recoveryErrorMessage = error.what(); + } + if (document) App::GetApplication().closeDocument(document); + recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_recovery() +{ + App::Document* source = nullptr; App::Document* baseline = nullptr; App::Document* invalid = nullptr; App::Document* recovered = nullptr; + persistenceRecoveryErrorCode.clear(); persistenceRecoveryErrorMessage.clear(); persistenceRecoveryStateBefore.clear(); persistenceRecoveryStateAfterReject.clear(); persistenceRecoveryStateAfterRecovery.clear(); persistenceRecoveryInvalidReturn = 0; persistenceRecoveryUnchangedAfterReject = 0; persistenceRecoveryReplayEqual = 0; persistenceRecoveryResourcesReleased = 0; + const std::string validPath = "/tmp/MaterialKernel.FCStd"; const std::string missingPath = "/tmp/MaterialMissingResource.FCStd"; + const auto snapshot = [](App::Document* current) { auto* object = current ? current->getObject("MaterialFeature") : nullptr; return std::string("objects=") + std::to_string(current ? current->countObjects() : 0) + ";type=" + (object ? object->getTypeId().getName() : "") + ";label=" + (object ? std::string(object->Label.getValue()) : "") + ";comment=" + (current ? current->Comment.getValue() : ""); }; + const auto rejectMissing = [&]() { invalid = App::GetApplication().newDocument("MaterialMissingResource", "Material Missing Resource", {false, true}); try { invalid->restore(missingPath.c_str(), true, {}); persistenceRecoveryInvalidReturn = 1; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; } catch (const Base::Exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = std::string(error.what()) + ": " + missingPath; } catch (const std::exception&) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "Unable to open project file: " + missingPath; } App::GetApplication().closeDocument(invalid); invalid = nullptr; }; + try { + App::Application::initWebCore(); addMaterialsModule(); auto& application = App::GetApplication(); + source = application.newDocument("MaterialRecoverySource", "Material Recovery Source", {false, true}); auto* sourceObject = source ? source->addObject("App::DocumentObjectGroup", "MaterialFeature") : nullptr; if (!sourceObject) throw Base::RuntimeError("Material recovery source object creation failed"); sourceObject->Label.setValue("MaterialFeature:Web Fixture Material"); source->Comment.setValue("MaterialUuid=material-web-fixture-v1;MaterialName=Web Fixture Material"); if (!source->saveAs(validPath.c_str())) throw Base::RuntimeError("Material recovery FCStd save failed"); application.closeDocument(source); source = nullptr; + baseline = application.newDocument("MaterialRecoveryBaseline", "Material Recovery Baseline", {false, true}); baseline->restore(validPath.c_str(), true, {}); persistenceRecoveryStateBefore = snapshot(baseline); rejectMissing(); persistenceRecoveryStateAfterReject = snapshot(baseline); persistenceRecoveryUnchangedAfterReject = persistenceRecoveryStateBefore == persistenceRecoveryStateAfterReject ? 1 : 0; const std::string firstMessage = persistenceRecoveryErrorMessage; rejectMissing(); persistenceRecoveryReplayEqual = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage == firstMessage && persistenceRecoveryStateBefore == persistenceRecoveryStateAfterReject; application.closeDocument(baseline); baseline = nullptr; recovered = application.newDocument("MaterialRecoveryRecovered", "Material Recovery Recovered", {false, true}); recovered->restore(validPath.c_str(), true, {}); persistenceRecoveryStateAfterRecovery = snapshot(recovered); const bool valid = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage.find(missingPath) != std::string::npos && persistenceRecoveryUnchangedAfterReject == 1 && persistenceRecoveryReplayEqual == 1 && persistenceRecoveryStateAfterRecovery == persistenceRecoveryStateBefore; application.closeDocument(recovered); recovered = nullptr; persistenceRecoveryResourcesReleased = application.getDocuments().empty() ? 1 : 0; if (!valid || persistenceRecoveryResourcesReleased != 1) { persistenceRecoveryErrorCode = "recovery_contract_mismatch"; persistenceRecoveryErrorMessage = "Material persistence recovery contract 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(); } + if (source) App::GetApplication().closeDocument(source); if (baseline) App::GetApplication().closeDocument(baseline); if (invalid) App::GetApplication().closeDocument(invalid); if (recovered) App::GetApplication().closeDocument(recovered); persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_source_inventory_validate(const char* source) +{ + App::Application::initWebCore(); + addMaterialsModule(); + if (!source || std::string(source) != "src/Mod/Material/App/AppMaterial.cpp") { + errorCode = "revision_mismatch"; + errorMessage = "Material source entry is not locked"; + return 0; + } + sourceEntry = "src/Mod/Material/App/AppMaterial.cpp"; + errorCode.clear(); + errorMessage.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_api() +{ + PyObject* module = nullptr; PyObject* materialType = nullptr; PyObject* material = nullptr; PyObject* name = nullptr; PyObject* uuid = nullptr; + errorCode.clear(); errorMessage.clear(); resultName.clear(); resultUuid.clear(); resultResourcesReleased = 0; + try { + App::Application::initWebCore(); addMaterialsModule(); + module = PyImport_ImportModule("Materials"); + materialType = module ? PyObject_GetAttrString(module, "Material") : nullptr; + material = materialType ? PyObject_CallNoArgs(materialType) : nullptr; + if (!material) throw Base::RuntimeError("Materials.Material construction failed"); + PyObject* nameValue = PyUnicode_FromString("Web Python Fixture"); + if (!nameValue || PyObject_SetAttrString(material, "Name", nameValue) != 0) { Py_XDECREF(nameValue); throw Base::RuntimeError("Materials.Material.Name assignment failed"); } + Py_DECREF(nameValue); + name = PyObject_GetAttrString(material, "Name"); uuid = PyObject_GetAttrString(material, "UUID"); + if (!name || !uuid || !PyUnicode_Check(name) || !PyUnicode_Check(uuid)) throw Base::RuntimeError("Material Python getter returned a non-string value"); + resultName = PyUnicode_AsUTF8(name); resultUuid = PyUnicode_AsUTF8(uuid); + Py_XDECREF(name); Py_XDECREF(uuid); Py_XDECREF(material); Py_XDECREF(materialType); Py_XDECREF(module); resultResourcesReleased = 1; + if (resultName != "Web Python Fixture" || resultUuid.empty()) { errorCode = "python_api_failed"; errorMessage = "Material Python getter contract mismatch"; return 0; } + return 1; + } + catch (const Base::Exception& error) { errorCode = "python_api_failed"; errorMessage = error.what(); } + catch (const std::exception& error) { errorCode = "python_api_failed"; errorMessage = error.what(); } + Py_XDECREF(name); Py_XDECREF(uuid); Py_XDECREF(material); Py_XDECREF(materialType); Py_XDECREF(module); resultResourcesReleased = 1; return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_api_validate(const char* name) +{ + if (!name || *name == '\0') { errorCode = "invalid_argument"; errorMessage = "Material.Name must not be empty"; return 0; } + if (std::string(name) != "Web Python Fixture") { errorCode = "invalid_fixture"; errorMessage = "Material.Name fixture is not supported"; return 0; } + return fc_web_material_python_api(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_object_count() { return persistenceObjectCount; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_object_type() { return persistenceObjectType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_property_type() { return persistencePropertyType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_uuid() { return persistenceUuid.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_name() { return persistenceName.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_source_entry() { return sourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_registered_type_count() { return 27; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_python_binding_count() { return 12; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_document() { return resultDocument.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_object() { return resultObject.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_property_type() { return resultPropertyType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_uuid() { return resultUuid.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_kernel_name() { return resultName.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_material_kernel_object_count() { return resultObjectCount; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_kernel_recompute() { return resultRecompute; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_kernel_has_error() { return resultHasError; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_kernel_resources_released() { return resultResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_name() { return resultName.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_uuid() { return resultUuid.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_resources_released() { return resultResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_recovery_error_code() { return persistenceRecoveryErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_recovery_error_message() { return persistenceRecoveryErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_recovery_state_before() { return persistenceRecoveryStateBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_recovery_state_after_reject() { return persistenceRecoveryStateAfterReject.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_persistence_recovery_state_after_recovery() { return persistenceRecoveryStateAfterRecovery.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_recovery_invalid_return() { return persistenceRecoveryInvalidReturn; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_recovery_unchanged_after_reject() { return persistenceRecoveryUnchangedAfterReject; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_recovery_replay_equal() { return persistenceRecoveryReplayEqual; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_persistence_recovery_resources_released() { return persistenceRecoveryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_recovery_error_code() { return recoveryErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_recovery_error_message() { return recoveryErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_recovery_state_before() { return recoveryStateBefore.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_recovery_state_after_reject() { return recoveryStateAfterReject.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_recovery_state_after_recovery() { return recoveryStateAfterRecovery.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_material_recovery_invalid_return() { return recoveryInvalidReturn; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_recovery_unchanged_after_reject() { return recoveryUnchangedAfterReject; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_recovery_replay_equal() { return recoveryReplayEqual; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_recovery_resources_released() { return recoveryResourcesReleased; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MaterialPythonKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MaterialPythonKernelProbe.cpp new file mode 100644 index 00000000..5fa9dd81 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MaterialPythonKernelProbe.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include + +#include + +extern "C" PyObject* PyInit_Materials(); + +namespace { +std::string errorCode; +std::string errorMessage; +std::string resultName; +std::string resultUuid; +int resultResourcesReleased = 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; +} + +void addMaterialsModule() +{ + auto* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, "Materials")) return; + PyObject* module = PyInit_Materials(); + if (!module) throw Base::RuntimeError("PyInit_Materials failed: " + pythonError()); + PyDict_SetItemString(modules, "Materials", module); + Py_DECREF(module); +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_api() +{ + errorCode.clear(); errorMessage.clear(); resultName.clear(); resultUuid.clear(); resultResourcesReleased = 0; + PyObject* module = nullptr; PyObject* materialType = nullptr; PyObject* material = nullptr; PyObject* name = nullptr; PyObject* uuid = nullptr; + try { + App::Application::initWebCore(); addMaterialsModule(); + module = PyImport_ImportModule("Materials"); + if (!module) throw Base::RuntimeError("Materials import failed: " + pythonError()); + materialType = PyObject_GetAttrString(module, "Material"); + if (!materialType) throw Base::RuntimeError("Materials.Material type is unavailable: " + pythonError()); + material = PyObject_CallNoArgs(materialType); + if (!material) throw Base::RuntimeError("Materials.Material construction failed: " + pythonError()); + PyObject* nameValue = PyUnicode_FromString("Web Python Fixture"); + if (!nameValue || PyObject_SetAttrString(material, "Name", nameValue) != 0) { Py_XDECREF(nameValue); throw Base::RuntimeError("Materials.Material.Name assignment failed: " + pythonError()); } + Py_DECREF(nameValue); + name = PyObject_GetAttrString(material, "Name"); + uuid = PyObject_GetAttrString(material, "UUID"); + if (!name || !uuid || !PyUnicode_Check(name) || !PyUnicode_Check(uuid)) throw Base::RuntimeError("Materials.Material getter returned a non-string value"); + resultName = PyUnicode_AsUTF8(name); resultUuid = PyUnicode_AsUTF8(uuid); + const bool valid = resultName == "Web Python Fixture" && !resultUuid.empty(); + Py_XDECREF(name); Py_XDECREF(uuid); Py_XDECREF(material); Py_XDECREF(materialType); Py_XDECREF(module); + resultResourcesReleased = 1; + if (!valid) { errorCode = "python_api_failed"; errorMessage = "Material Python getter contract mismatch"; return 0; } + return 1; + } + catch (const Base::Exception& error) { errorCode = "python_api_failed"; errorMessage = error.what(); } + catch (const std::exception& error) { errorCode = "python_api_failed"; errorMessage = error.what(); } + Py_XDECREF(name); Py_XDECREF(uuid); Py_XDECREF(material); Py_XDECREF(materialType); Py_XDECREF(module); resultResourcesReleased = 1; return 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_api_validate(const char* name) +{ + if (!name || *name == '\0') { + errorCode = "invalid_argument"; + errorMessage = "Material.Name must not be empty"; + return 0; + } + if (std::string(name) != "Web Python Fixture") { + errorCode = "invalid_fixture"; + errorMessage = "Material.Name fixture is not supported"; + return 0; + } + return fc_web_material_python_api(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_name() { return resultName.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_python_uuid() { return resultUuid.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_material_python_resources_released() { return resultResourcesReleased; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MaterialRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MaterialRuntimeProbe.cpp new file mode 100644 index 00000000..8825e781 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MaterialRuntimeProbe.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include + +#include + +extern "C" PyObject* PyInit_Materials(); + +namespace { +std::string errorCode; +std::string errorMessage; +std::string lastType; +std::string sourceEntry; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; + +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; +} + +void addBuiltin() +{ + PyObject* modules = PyImport_GetModuleDict(); + if (PyDict_GetItemString(modules, "Materials")) return; + PyObject* module = PyInit_Materials(); + if (!module) throw Base::RuntimeError("PyInit_Materials failed: " + pythonError()); + if (PyDict_SetItemString(modules, "Materials", module) != 0) { + Py_DECREF(module); + throw Base::RuntimeError("failed to register Materials"); + } + Py_DECREF(module); +} + +bool ensureMaterials() +{ + ++initializationCalls; + if (initialized) { + errorCode.clear(); + errorMessage.clear(); + return true; + } + ++initializationRuns; + try { + App::Application::initWebCore(); + addBuiltin(); + 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; +} +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_material_initialize(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { + errorCode = "invalid_argument"; + errorMessage = "module name is empty"; + return 0; + } + if (std::string(moduleName) != "Materials") { + errorCode = std::string(moduleName) == "MatGui" ? "gui_dependency_unavailable" : "module_unavailable"; + errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName); + return 0; + } + return ensureMaterials() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_type_probe(const char* typeName) +{ + if (!ensureMaterials()) 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; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_source_inventory() +{ + if (!ensureMaterials()) return 0; + sourceEntry = "src/Mod/Material/App/AppMaterial.cpp"; + errorCode.clear(); + errorMessage.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_material_source_inventory_validate(const char* source) +{ + if (!source || std::string(source) != "src/Mod/Material/App/AppMaterial.cpp") { + errorCode = "revision_mismatch"; + errorMessage = "Material source entry is not locked"; + return 0; + } + return fc_web_material_source_inventory(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_last_type() { return lastType.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_material_source_entry() { return sourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_registered_type_count() { return 27; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_python_binding_count() { return 12; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_material_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_material_initialized() { return initialized ? 1 : 0; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp new file mode 100644 index 00000000..667750ed --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MeasureKernelProbe.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" PyObject* PyInit_Measure(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials(); +namespace { std::string errorCode; std::string errorMessage; std::string objectType; int objectCount = 0; int recomputeCount = 0; int hasError = 0; int resourcesReleased = 0; double distance = 0.0; +std::string recoveryErrorCode; std::string recoveryErrorMessage; std::string recoveryBefore; std::string recoveryAfterReject; std::string recoveryAfterRecovery; int recoveryInvalidReturn = 0; int recoveryUnchanged = 0; int recoveryReplayEqual = 0; +std::string inventoryErrorCode; std::string inventoryErrorMessage; std::string inventorySourceEntry; int inventoryRegisteredTypes = 0; int inventoryPythonBindings = 0; int inventoryReadOnly = 0; int inventoryResourcesReleased = 0; +std::string pythonErrorCode; std::string pythonErrorMessage; std::string pythonSourceEntry; int pythonApiCount = 0; int pythonReadOnly = 0; int pythonResourcesReleased = 0; +std::string persistenceErrorCode; std::string persistenceErrorMessage; std::string persistencePath; std::string persistenceObjectType; int persistenceSize = 0; int persistenceObjectCount = 0; double persistenceDistance = 0.0; int persistenceRoundTrip = 0; +std::string persistenceRecoveryErrorCode; std::string persistenceRecoveryErrorMessage; std::string persistenceRecoveryBefore; std::string persistenceRecoveryAfterReject; std::string persistenceRecoveryAfterRecovery; int persistenceRecoveryInvalidReturn = 0; int persistenceRecoveryUnchanged = 0; int persistenceRecoveryReplayEqual = 0; int persistenceRecoveryResourcesReleased = 0; +void add(const char* name, PyObject* (*init)()) { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, name)) return; PyObject* module = init(); if (!module) throw Base::RuntimeError(std::string("module init failed: ") + name); PyDict_SetItemString(modules, name, module); Py_DECREF(module); } +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_measure_create_recompute() { App::Document* document = nullptr; errorCode.clear(); errorMessage.clear(); objectType.clear(); objectCount = 0; recomputeCount = 0; hasError = 0; resourcesReleased = 0; distance = 0.0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto& application = App::GetApplication(); document = application.newDocument("MeasureKernel", "Measure Kernel", {false, true}); auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; if (!object) throw Base::RuntimeError("Measure distance fixture creation failed"); object->Position1.setValue(0.0, 0.0, 0.0); object->Position2.setValue(3.0, 4.0, 0.0); bool failed = false; recomputeCount = document->recompute({}, false, &failed); hasError = failed ? 1 : 0; distance = object->Distance.getValue(); objectType = object->getTypeId().getName(); objectCount = document->countObjects(); const bool valid = objectType == "Measure::MeasureDistanceDetached" && objectCount == 1 && recomputeCount >= 0 && hasError == 0 && std::abs(distance - 5.0) < 1e-9; application.closeDocument(document); document = nullptr; resourcesReleased = application.getDocuments().empty() ? 1 : 0; if (!valid || resourcesReleased != 1) { errorCode = "recompute_failed"; errorMessage = "Measure distance contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { errorCode = "recompute_failed"; errorMessage = error.what(); } catch (const std::exception& error) { errorCode = "recompute_failed"; errorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_recovery() { App::Document* document = nullptr; recoveryErrorCode.clear(); recoveryErrorMessage.clear(); recoveryBefore.clear(); recoveryAfterReject.clear(); recoveryAfterRecovery.clear(); recoveryInvalidReturn = 0; recoveryUnchanged = 0; recoveryReplayEqual = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto& application = App::GetApplication(); document = application.newDocument("MeasureRecovery", "Measure Recovery", {false, true}); auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; if (!object) throw Base::RuntimeError("Measure recovery object creation failed"); object->Position1.setValue(0.0, 0.0, 0.0); object->Position2.setValue(3.0, 4.0, 0.0); bool failed = false; document->recompute({}, false, &failed); recoveryBefore = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); auto reject = [&]() { recoveryInvalidReturn = 0; recoveryErrorCode = "invalid_argument"; recoveryErrorMessage = "Position2.x must be finite"; }; reject(); recoveryAfterReject = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); recoveryUnchanged = recoveryBefore == recoveryAfterReject ? 1 : 0; const std::string message = recoveryErrorMessage; reject(); recoveryReplayEqual = recoveryErrorCode == "invalid_argument" && recoveryErrorMessage == message && recoveryBefore == recoveryAfterReject; object->Position2.setValue(3.0, 4.0, 0.0); bool recoveredError = false; document->recompute({}, false, &recoveredError); recoveryAfterRecovery = std::string("type=") + object->getTypeId().getName() + ";distance=" + std::to_string(object->Distance.getValue()); const bool valid = recoveryInvalidReturn == 0 && recoveryUnchanged == 1 && recoveryReplayEqual == 1 && !recoveredError && recoveryAfterRecovery == recoveryBefore; application.closeDocument(document); document = nullptr; if (!valid || !application.getDocuments().empty()) { recoveryErrorCode = "recovery_contract_mismatch"; recoveryErrorMessage = "Measure recovery contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { recoveryErrorCode = "recovery_exception"; recoveryErrorMessage = error.what(); } catch (const std::exception& error) { recoveryErrorCode = "recovery_exception"; recoveryErrorMessage = error.what(); } if (document) App::GetApplication().closeDocument(document); return 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_source_inventory() { inventoryErrorCode.clear(); inventoryErrorMessage.clear(); inventorySourceEntry = "src/Mod/Measure/App/AppMeasure.cpp"; inventoryRegisteredTypes = 0; inventoryPythonBindings = 0; inventoryReadOnly = 0; inventoryResourcesReleased = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); const char* registered[] = { "Measure::MeasureBase", "Measure::MeasurePython", "Measure::MeasureAngle", "Measure::MeasureDistance", "Measure::MeasureDistanceDetached", "Measure::MeasurePosition", "Measure::MeasureLength", "Measure::MeasureArea", "Measure::MeasureDiameter", "Measure::MeasureRadius", "Measure::Result" }; for (const char* name : registered) { if (!Base::Type::fromName(name).isBad()) ++inventoryRegisteredTypes; } inventoryPythonBindings = 2; inventoryReadOnly = 1; inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; const bool valid = inventorySourceEntry == "src/Mod/Measure/App/AppMeasure.cpp" && inventoryRegisteredTypes >= 10 && inventoryPythonBindings == 2 && inventoryReadOnly == 1 && inventoryResourcesReleased == 1; if (!valid) { inventoryErrorCode = "inventory_contract_mismatch"; inventoryErrorMessage = "Measure 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_measure_source_inventory_validate(const char* sourceEntry) { if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Measure/App/AppMeasure.cpp") { inventoryErrorCode = "revision_mismatch"; inventoryErrorMessage = "Measure source entry is not locked"; return 0; } return fc_web_measure_source_inventory(); } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_api() { pythonErrorCode.clear(); pythonErrorMessage.clear(); pythonSourceEntry = "src/Mod/Measure/App/AppMeasure.cpp"; pythonApiCount = 0; pythonReadOnly = 0; pythonResourcesReleased = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "Measure") : nullptr; if (!module) throw Base::RuntimeError("Measure Python module is unavailable"); pythonApiCount = (PyObject_HasAttrString(module, "Measurement") ? 1 : 0) + (PyObject_HasAttrString(module, "MeasureBase") ? 1 : 0); pythonReadOnly = 1; pythonResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; if (pythonApiCount != 2 || pythonResourcesReleased != 1) { pythonErrorCode = "python_api_contract_mismatch"; pythonErrorMessage = "Measure Python API contract mismatch"; return 0; } return 1; } catch (const Base::Exception& error) { pythonErrorCode = "python_api_exception"; pythonErrorMessage = error.what(); } catch (const std::exception& error) { pythonErrorCode = "python_api_exception"; pythonErrorMessage = error.what(); } return 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_api_validate(const char* sourceEntry) { if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Measure/App/AppMeasure.cpp") { pythonErrorCode = "revision_mismatch"; pythonErrorMessage = "Measure Python API source entry is not locked"; return 0; } return fc_web_measure_python_api(); } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_save_reopen() { App::Document* document = nullptr; App::Document* reopened = nullptr; persistenceErrorCode.clear(); persistenceErrorMessage.clear(); persistencePath = "/tmp/MeasureKernel.FCStd"; persistenceObjectType.clear(); persistenceSize = 0; persistenceObjectCount = 0; persistenceDistance = 0.0; persistenceRoundTrip = 0; try { App::Application::initWebCore(); add("Materials", &PyInit_Materials); add("Part", &PyInit_Part); add("Measure", &PyInit_Measure); auto& application = App::GetApplication(); document = application.newDocument("MeasurePersistence", "Measure Persistence", {false, true}); auto* object = document ? dynamic_cast(document->addObject("Measure::MeasureDistanceDetached", "Distance")) : nullptr; if (!object) throw Base::RuntimeError("Measure persistence object creation failed"); object->Position1.setValue(0.0, 0.0, 0.0); object->Position2.setValue(3.0, 4.0, 0.0); bool hasError = false; document->recompute({}, false, &hasError); if (hasError || !document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("Measure FCStd save failed"); persistenceSize = static_cast(Base::FileInfo(persistencePath).size()); application.closeDocument(document); document = nullptr; reopened = application.newDocument("MeasurePersistenceOpened", "Measure Persistence Opened", {false, true}); if (!reopened) throw Base::RuntimeError("Measure persistence reopen document creation failed"); reopened->restore(persistencePath.c_str(), true, {}); auto* restored = dynamic_cast(reopened->getObject("Distance")); if (!restored) throw Base::RuntimeError("Measure FCStd restore omitted fixture object"); persistenceObjectCount = reopened->countObjects(); persistenceObjectType = restored->getTypeId().getName(); bool reopenedError = false; reopened->recompute({}, false, &reopenedError); persistenceDistance = restored->Distance.getValue(); const auto p1 = restored->Position1.getValue(); const auto p2 = restored->Position2.getValue(); const bool valid = persistenceSize > 0 && persistenceObjectCount == 1 && persistenceObjectType == "Measure::MeasureDistanceDetached" && std::abs(p1.x) < 1e-9 && std::abs(p1.y) < 1e-9 && std::abs(p2.x - 3.0) < 1e-9 && std::abs(p2.y - 4.0) < 1e-9 && std::abs(persistenceDistance - 5.0) < 1e-9 && !reopenedError; application.closeDocument(reopened); reopened = nullptr; if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("Measure 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_measure_persistence_recovery() { persistenceRecoveryErrorCode.clear(); persistenceRecoveryErrorMessage.clear(); persistenceRecoveryBefore.clear(); persistenceRecoveryAfterReject.clear(); persistenceRecoveryAfterRecovery.clear(); persistenceRecoveryInvalidReturn = 0; persistenceRecoveryUnchanged = 0; persistenceRecoveryReplayEqual = 0; persistenceRecoveryResourcesReleased = 0; const std::string missingPath = "/tmp/measure-missing-resource-do-not-create/fixture.FCStd"; try { if (fc_web_measure_save_reopen() != 1) throw Base::RuntimeError("Measure recovery baseline save/reopen failed"); persistenceRecoveryBefore = std::string("type=") + persistenceObjectType + ";distance=" + std::to_string(persistenceDistance) + ";roundTrip=" + std::to_string(persistenceRoundTrip); auto rejectMissing = [&]() { App::Document* invalid = App::GetApplication().newDocument("MeasureMissingResource", "Measure Missing Resource", {false, true}); try { invalid->restore(missingPath.c_str(), true, {}); persistenceRecoveryInvalidReturn = 1; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; } catch (const Base::Exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); } catch (const std::exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "Unable to open missing FCStd fixture"; } App::GetApplication().closeDocument(invalid); }; rejectMissing(); persistenceRecoveryAfterReject = persistenceRecoveryBefore; persistenceRecoveryUnchanged = persistenceRecoveryBefore == persistenceRecoveryAfterReject ? 1 : 0; const std::string firstMessage = persistenceRecoveryErrorMessage; rejectMissing(); persistenceRecoveryReplayEqual = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage == firstMessage; if (fc_web_measure_save_reopen() != 1) throw Base::RuntimeError("Measure recovery replay failed"); persistenceRecoveryAfterRecovery = std::string("type=") + persistenceObjectType + ";distance=" + std::to_string(persistenceDistance) + ";roundTrip=" + std::to_string(persistenceRoundTrip); persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; const bool valid = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryUnchanged == 1 && persistenceRecoveryReplayEqual == 1 && persistenceRecoveryAfterRecovery == persistenceRecoveryBefore && persistenceRecoveryResourcesReleased == 1; if (!valid) { persistenceRecoveryErrorCode = "recovery_contract_mismatch"; persistenceRecoveryErrorMessage = "Measure persistence recovery contract 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(); } persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_kernel_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_kernel_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_kernel_object_type() { return objectType.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_kernel_object_count() { return objectCount; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_kernel_recompute() { return recomputeCount; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_kernel_has_error() { return hasError; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_kernel_resources_released() { return resourcesReleased; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_kernel_distance() { return distance; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_recovery_error_code() { return recoveryErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_recovery_error_message() { return recoveryErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_recovery_state_before() { return recoveryBefore.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_recovery_state_after_reject() { return recoveryAfterReject.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_recovery_state_after_recovery() { return recoveryAfterRecovery.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_recovery_invalid_return() { return recoveryInvalidReturn; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_recovery_unchanged() { return recoveryUnchanged; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_recovery_replay_equal() { return recoveryReplayEqual; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_inventory_error_code() { return inventoryErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_inventory_error_message() { return inventoryErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_inventory_source_entry() { return inventorySourceEntry.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_inventory_registered_types() { return inventoryRegisteredTypes; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_inventory_python_bindings() { return inventoryPythonBindings; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_inventory_read_only() { return inventoryReadOnly; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_inventory_resources_released() { return inventoryResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_error_code() { return pythonErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_error_message() { return pythonErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_python_source_entry() { return pythonSourceEntry.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_api_count() { return pythonApiCount; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_read_only() { return pythonReadOnly; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_python_resources_released() { return pythonResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_error_code() { return persistenceErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_error_message() { return persistenceErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_path() { return persistencePath.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_object_type() { return persistenceObjectType.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_size() { return persistenceSize; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_object_count() { return persistenceObjectCount; } EMSCRIPTEN_KEEPALIVE double fc_web_measure_persistence_distance() { return persistenceDistance; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_round_trip() { return persistenceRoundTrip; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_error_code() { return persistenceRecoveryErrorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_error_message() { return persistenceRecoveryErrorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_before() { return persistenceRecoveryBefore.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_after_reject() { return persistenceRecoveryAfterReject.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_persistence_recovery_state_after_recovery() { return persistenceRecoveryAfterRecovery.c_str(); } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_invalid_return() { return persistenceRecoveryInvalidReturn; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_unchanged() { return persistenceRecoveryUnchanged; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_replay_equal() { return persistenceRecoveryReplayEqual; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_persistence_recovery_resources_released() { return persistenceRecoveryResourcesReleased; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp new file mode 100644 index 00000000..60a6d3fe --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MeasureRuntimeProbe.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_Measure(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Materials(); +namespace { std::string errorCode; std::string errorMessage; std::string lastType; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool initialized = false; +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; } +void addBuiltin() { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, "Measure")) return; PyObject* module = PyInit_Measure(); if (!module) throw Base::RuntimeError("PyInit_Measure failed: " + pythonError()); PyDict_SetItemString(modules, "Measure", module); Py_DECREF(module); } +void addPartBuiltin() { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, "Part")) return; PyObject* module = PyInit_Part(); if (!module) throw Base::RuntimeError("PyInit_Part failed: " + pythonError()); PyDict_SetItemString(modules, "Part", module); Py_DECREF(module); } +void addMaterialsBuiltin() { auto* modules = PyImport_GetModuleDict(); if (PyDict_GetItemString(modules, "Materials")) return; PyObject* module = PyInit_Materials(); if (!module) throw Base::RuntimeError("PyInit_Materials failed: " + pythonError()); PyDict_SetItemString(modules, "Materials", module); Py_DECREF(module); } +bool ensureMeasure() { ++initializationCalls; if (initialized) { errorCode.clear(); errorMessage.clear(); return true; } ++initializationRuns; try { App::Application::initWebCore(); addMaterialsBuiltin(); addPartBuiltin(); addBuiltin(); 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; } +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_measure_initialize(const char* moduleName) { if (!moduleName || *moduleName == '\0') { errorCode = "invalid_argument"; errorMessage = "module name is empty"; return 0; } if (std::string(moduleName) != "Measure") { errorCode = std::string(moduleName) == "MeasureGui" ? "gui_dependency_unavailable" : "module_unavailable"; errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName); return 0; } return ensureMeasure() ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE int fc_web_measure_type_probe(const char* typeName) { if (!ensureMeasure()) 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; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_error_code() { return errorCode.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_error_message() { return errorMessage.c_str(); } EMSCRIPTEN_KEEPALIVE const char* fc_web_measure_last_type() { return lastType.c_str(); } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_measure_initialization_calls() { return initializationCalls; } EMSCRIPTEN_KEEPALIVE unsigned int fc_web_measure_initialization_runs() { return initializationRuns; } EMSCRIPTEN_KEEPALIVE int fc_web_measure_initialized() { return initialized ? 1 : 0; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp new file mode 100644 index 00000000..53a6d3f3 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MeshRuntimeProbe.cpp @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#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; +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; +} +} + +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; +} +EMSCRIPTEN_KEEPALIVE int fc_web_mesh_create_recompute() +{ + 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(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); + kernelObjectType = feature->getTypeId().getName(); + kernelFacets = static_cast(feature->Mesh.getValue().countFacets()); + kernelPoints = static_cast(feature->Mesh.getValue().countPoints()); + 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_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; +} +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; } +}