diff --git a/CMakeLists.txt b/CMakeLists.txt index 1313ad9a..b1e0b898 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ option(FREECAD_WEB_ENABLE_ASSEMBLY "Enable the Assembly Web probe and its depend option(FREECAD_WEB_ENABLE_CAM "Enable the CAM Path App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependency closure" OFF) option(FREECAD_WEB_ENABLE_INSPECTION "Enable the Inspection App Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_MESHPART "Enable the MeshPart App Web probe and its dependency closure" OFF) +option(FREECAD_WEB_ENABLE_OPENSCAD "Enable the OpenSCAD App Web probe and its dependency closure" OFF) if(FREECAD_WEB) if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(FATAL_ERROR @@ -122,6 +124,15 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_INSPECTION) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_INSPECTION BUILD_MESH BUILD_POINTS) endif() + if(FREECAD_WEB_ENABLE_MESHPART) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_MESH_PART BUILD_MESH) + endif() + if(FREECAD_WEB_ENABLE_OPENSCAD) + # OpenSCAD is a Python workbench whose source target depends on the + # Draft/TechDraw/Spreadsheet script closure, even when BUILD_GUI is OFF. + list(REMOVE_ITEM _freecad_web_disabled_options + BUILD_OPENSCAD BUILD_DRAFT BUILD_TECHDRAW BUILD_SPREADSHEET) + endif() foreach(_freecad_web_disabled_option IN LISTS _freecad_web_disabled_options) set(${_freecad_web_disabled_option} OFF CACHE BOOL "Disabled by FREECAD_WEB core configuration" FORCE) endforeach() @@ -148,6 +159,16 @@ if(FREECAD_WEB) set(${_freecad_web_inspection_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_INSPECTION" FORCE) endforeach() endif() + if(FREECAD_WEB_ENABLE_MESHPART) + foreach(_freecad_web_meshpart_option BUILD_MESH_PART BUILD_MESH) + set(${_freecad_web_meshpart_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_MESHPART" FORCE) + endforeach() + endif() + if(FREECAD_WEB_ENABLE_OPENSCAD) + foreach(_freecad_web_openscad_option BUILD_OPENSCAD BUILD_DRAFT BUILD_TECHDRAW BUILD_SPREADSHEET) + set(${_freecad_web_openscad_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_OPENSCAD" FORCE) + endforeach() + endif() include(cMake/FreeCADWebDependencies.cmake) endif() diff --git a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake index 7323842f..0140a974 100644 --- a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake +++ b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake @@ -189,6 +189,13 @@ macro(InitializeFreeCADBuildOptions) else() set(BUILD_SMESH ON) endif() + if(FREECAD_WEB AND FREECAD_WEB_ENABLE_MESHPART) + # The Web MeshPart App probe covers the Part/Mesh-backed core + # without the optional SMESH/VTK backend, which is unavailable in + # the GUI-free Web dependency closure. + set(FREECAD_USE_SMESH OFF) + set(BUILD_SMESH OFF) + endif() else() set(FREECAD_USE_SMESH OFF) set(BUILD_SMESH OFF) diff --git a/src/Mod/MeshPart/App/Mesher.cpp b/src/Mod/MeshPart/App/Mesher.cpp index 09e0556e..d22c2dbd 100644 --- a/src/Mod/MeshPart/App/Mesher.cpp +++ b/src/Mod/MeshPart/App/Mesher.cpp @@ -428,8 +428,9 @@ Mesh::MeshObject* Mesher::createMesh() const #endif // HAVE_SMESH } -Mesh::MeshObject* Mesher::createFrom(SMESH_Mesh* mesh) const -{ +#ifdef HAVE_SMESH +Mesh::MeshObject* Mesher::createFrom(SMESH_Mesh* mesh) const +{ // build up the mesh structure SMDS_FaceIteratorPtr aFaceIter = mesh->GetMeshDS()->facesIterator(); SMDS_NodeIteratorPtr aNodeIter = mesh->GetMeshDS()->nodesIterator(); @@ -577,6 +578,12 @@ Mesh::MeshObject* Mesher::createFrom(SMESH_Mesh* mesh) const kernel.Adopt(verts, faces, true); Mesh::MeshObject* meshdata = new Mesh::MeshObject(); - meshdata->swap(kernel); - return meshdata; -} + meshdata->swap(kernel); + return meshdata; +} +#else +Mesh::MeshObject* Mesher::createFrom(SMESH_Mesh*) const +{ + return nullptr; +} +#endif diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index d4054ebe..ff053749 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -38,6 +38,17 @@ set_target_properties(FreeCADWebPartBoxProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +add_executable(FreeCADWebPartIoProbe PartIoProbe.cpp) +target_compile_features(FreeCADWebPartIoProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebPartIoProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebPartIoProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") +target_link_libraries(FreeCADWebPartIoProbe PRIVATE Part ${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(FreeCADWebPartIoProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPartIoProbe" "-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(FreeCADWebPartIoProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + add_executable(FreeCADWebModuleLoaderProbe ModuleLoaderProbe.cpp) target_compile_features(FreeCADWebModuleLoaderProbe PRIVATE cxx_std_17) target_compile_options(FreeCADWebModuleLoaderProbe PRIVATE "-O2" "-g0" "-fexceptions") @@ -74,6 +85,46 @@ set_target_properties(FreeCADWebModuleLoaderProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +if(FREECAD_WEB_ENABLE_OPENSCAD) + add_executable(FreeCADWebOpenSCADRuntimeProbe OpenSCADRuntimeProbe.cpp) + target_compile_features(FreeCADWebOpenSCADRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebOpenSCADRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebOpenSCADRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebOpenSCADRuntimeProbe PRIVATE + Part + ${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(FreeCADWebOpenSCADRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebOpenSCADRuntimeProbe" + "-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" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/Init.py@/freecad-mod/OpenSCAD/Init.py" + "--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/OpenSCAD/OpenSCADUtils.py@/freecad-mod/OpenSCAD/OpenSCADUtils.py" + ) + set_target_properties(FreeCADWebOpenSCADRuntimeProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) +endif() + add_executable(FreeCADWebImportRuntimeProbe ImportRuntimeProbe.cpp) target_compile_features(FreeCADWebImportRuntimeProbe PRIVATE cxx_std_17) target_compile_options(FreeCADWebImportRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") @@ -258,6 +309,16 @@ if(BUILD_MESH) set_target_properties(FreeCADWebMeshRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") endif() +if(BUILD_MESH_PART) + add_executable(FreeCADWebMeshPartRuntimeProbe MeshPartRuntimeProbe.cpp) + target_compile_features(FreeCADWebMeshPartRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebMeshPartRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(FreeCADWebMeshPartRuntimeProbe PRIVATE MeshPart 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(FreeCADWebMeshPartRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebMeshPartRuntimeProbe" "-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(FreeCADWebMeshPartRuntimeProbe 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/MeshPartRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp new file mode 100644 index 00000000..346d0074 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/MeshPartRuntimeProbe.cpp @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" PyObject* PyInit_MeshPart(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Mesh(); +extern "C" PyObject* PyInit_Materials(); +namespace { +std::string errorCode; +std::string errorMessage; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; +std::string kernelErrorCode; +std::string kernelErrorMessage; +unsigned int kernelFacets = 0; +unsigned int kernelPoints = 0; +int kernelResourcesReleased = 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; +int persistenceRoundTrip = 0; +std::string pythonError(); +void addBuiltin(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("PyInit_") + name + " failed: " + pythonError()); + PyDict_SetItemString(modules, name, module); Py_DECREF(module); +} +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 ensureMeshPart() +{ + ++initializationCalls; + if (initialized) { errorCode.clear(); errorMessage.clear(); return true; } + ++initializationRuns; + try { + App::Application::initWebCore(); + auto* modules = PyImport_GetModuleDict(); + addBuiltin("Materials", &PyInit_Materials); + addBuiltin("Part", &PyInit_Part); + addBuiltin("Mesh", &PyInit_Mesh); + if (!PyDict_GetItemString(modules, "MeshPart")) { + PyObject* module = PyInit_MeshPart(); + if (!module) throw Base::RuntimeError("PyInit_MeshPart failed: " + pythonError()); + PyDict_SetItemString(modules, "MeshPart", 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_meshpart_initialize(const char* moduleName) +{ + if (!moduleName || *moduleName == '\0') { errorCode = "invalid_argument"; errorMessage = "module name is empty"; return 0; } + if (std::string(moduleName) != "MeshPart") { errorCode = std::string(moduleName) == "MeshPartGui" ? "gui_dependency_unavailable" : "module_unavailable"; errorMessage = "module is not available in the GUI-free Web kernel: " + std::string(moduleName); return 0; } + return ensureMeshPart() ? 1 : 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_type_probe(const char* typeName) +{ + if (!ensureMeshPart()) return 0; + if (!typeName || *typeName == '\0') { errorCode = "invalid_argument"; errorMessage = "type name is empty"; return 0; } + // MeshPart is primarily a Python algorithm module; its App target does not + // register persistent DocumentObject types. Type visibility is represented + // by the successfully initialized Python module namespace. + if (std::string(typeName) == "MeshPart") { errorCode.clear(); errorMessage.clear(); return 1; } + errorCode = "type_not_found"; errorMessage = "type is not registered: " + std::string(typeName); return 0; +} +EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_create_recompute() +{ + App::Document* document = nullptr; kernelErrorCode.clear(); kernelErrorMessage.clear(); kernelFacets = 0; kernelPoints = 0; kernelResourcesReleased = 0; + try { + if (!ensureMeshPart()) throw Base::RuntimeError("MeshPart initialization failed"); + App::Application::initWebCore(); + auto& application = App::GetApplication(); + document = application.newDocument("MeshPartKernel", "MeshPart Kernel", {false, true}); + auto* box = document ? dynamic_cast(document->addObject("Part::Box", "Box")) : nullptr; + if (!box) throw Base::RuntimeError("MeshPart Part::Box fixture creation failed"); + box->Length.setValue(1.0); box->Width.setValue(1.0); box->Height.setValue(1.0); + bool boxError = false; document->recompute({}, false, &boxError); if (boxError) throw Base::RuntimeError("Part::Box recompute failed"); + MeshPart::Mesher mesher(box->Shape.getValue()); mesher.setMethod(MeshPart::Mesher::Standard); mesher.setDeflection(0.2); auto mesh = std::unique_ptr(mesher.createMesh()); + if (!mesh) throw Base::RuntimeError("MeshPart standard mesher returned no mesh"); + kernelFacets = static_cast(mesh->countFacets()); kernelPoints = static_cast(mesh->countPoints()); + const bool valid = kernelFacets > 0 && kernelPoints > 0; mesh.reset(); application.closeDocument(document); document = nullptr; kernelResourcesReleased = application.getDocuments().empty() ? 1 : 0; + if (!valid || kernelResourcesReleased != 1) { kernelErrorCode = "recompute_failed"; kernelErrorMessage = "MeshPart standard mesher 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_meshpart_save_reopen() +{ + App::Document* document = nullptr; App::Document* reopened = nullptr; persistenceErrorCode.clear(); persistenceErrorMessage.clear(); persistencePath = "/tmp/MeshPartAction04.FCStd"; persistenceSize = 0; persistenceObjectCount = 0; persistenceFacets = 0; persistencePoints = 0; persistenceRoundTrip = 0; + try { + if (fc_web_meshpart_create_recompute() != 1) throw Base::RuntimeError("MeshPart baseline meshing failed"); + auto& application = App::GetApplication(); document = application.newDocument("MeshPartPersistence", "MeshPart Persistence", {false, true}); auto* feature = document ? document->addObject("Mesh::Feature", "Mesh") : nullptr; if (!feature) throw Base::RuntimeError("MeshPart persistence mesh object creation failed"); application.closeDocument(document); document = nullptr; + // A standalone MeshPart shape-to-mesh result has no MeshPart-specific + // DocumentObject; persistence is verified through the native Mesh object. + reopened = application.newDocument("MeshPartPersistenceOpened", "MeshPart Persistence Opened", {false, true}); if (!reopened) throw Base::RuntimeError("MeshPart persistence reopen failed"); + persistenceObjectCount = reopened->countObjects(); application.closeDocument(reopened); reopened = nullptr; persistenceRoundTrip = 0; persistenceErrorCode = "not_applicable"; persistenceErrorMessage = "MeshPart shape-to-mesh first slice does not expose a MeshPart DocumentObject for FCStd save/reopen"; 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 const char* fc_web_meshpart_error_code() { return errorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_error_message() { return errorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_initialized() { return initialized ? 1 : 0; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_kernel_error_code() { return kernelErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_kernel_error_message() { return kernelErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_kernel_facets() { return kernelFacets; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_kernel_points() { return kernelPoints; } +EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_kernel_resources_released() { return kernelResourcesReleased; } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_persistence_error_code() { return persistenceErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_persistence_error_message() { return persistenceErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_meshpart_persistence_path() { return persistencePath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_persistence_size() { return persistenceSize; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_persistence_object_count() { return persistenceObjectCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_persistence_facets() { return persistenceFacets; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_meshpart_persistence_points() { return persistencePoints; } +EMSCRIPTEN_KEEPALIVE int fc_web_meshpart_persistence_round_trip() { return persistenceRoundTrip; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp new file mode 100644 index 00000000..a178a679 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/OpenSCADRuntimeProbe.cpp @@ -0,0 +1,790 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr const char* initScript = "/freecad-mod/OpenSCAD/Init.py"; +constexpr const char* importModule = "importCSG"; +constexpr const char* exportModule = "exportCSG"; +constexpr const char* sourceRevision = "837f44197c879e41198b7e3bed55c897a9920520"; +constexpr const char* pythonApiSource = "/freecad-mod/OpenSCAD/OpenSCADUtils.py"; + +std::string lastErrorCode; +std::string lastErrorMessage; +std::string lastTypeName; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool initialized = false; +std::string inspectErrorCode; +std::string inspectErrorField; +std::string inspectErrorMessage; +std::string inspectSourceRevision; +std::string inspectSummary; +int inspectObjectTypeCount = 0; +int inspectFormatExtensionCount = 0; +int inspectFormatEvidenceCount = 0; +int inspectPythonApiCount = 0; +int inspectGuiPythonApiCount = 0; +int inspectModuleHandleBeforeRelease = 0; +int inspectModuleHandleAfterRelease = 0; +int inspectDocumentCountBefore = 0; +int inspectDocumentCountAfter = 0; +std::string recoveryErrorCode; +std::string recoveryErrorMessage; +std::string recoveryStateBefore; +std::string recoveryStateAfterReject; +std::string recoveryStateAfterRecovery; +int recoveryInvalidReturn = 0; +int recoveryUnchanged = 0; +int recoveryReplayEqual = 0; +int recoveryResourcesReleased = 0; +std::string pythonApiErrorCode; +std::string pythonApiErrorMessage; +std::string pythonApiSourceEntry; +std::string pythonApiSummary; +std::vector pythonApiNames; +int pythonApiReadOnly = 0; +int pythonApiResourcesReleased = 0; +std::string pythonRecoveryErrorCode; +std::string pythonRecoveryErrorMessage; +std::string pythonRecoveryStateBefore; +std::string pythonRecoveryStateAfterReject; +std::string pythonRecoveryStateAfterRecovery; +int pythonRecoveryInvalidReturn = 0; +int pythonRecoveryUnchanged = 0; +int pythonRecoveryReplayEqual = 0; +int pythonRecoveryResourcesReleased = 0; + +void setError(const char* code, const std::string& message) +{ + lastErrorCode = code; + lastErrorMessage = message; +} + +void clearError() +{ + lastErrorCode.clear(); + lastErrorMessage.clear(); +} + +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 contains(const std::vector& values, const char* expected) +{ + for (const auto& value : values) { + if (value == expected) { + return true; + } + } + return false; +} + +bool executeInitScript() +{ + FILE* script = std::fopen(initScript, "rb"); + if (!script) { + setError("dependency_unavailable", "OpenSCAD Init.py is not embedded: " + std::string(initScript)); + return false; + } + + PyObject* globals = PyDict_New(); + if (!globals) { + std::fclose(script); + setError("initialization_failed", "unable to allocate OpenSCAD Init.py globals"); + return false; + } + PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()); + PyObject* name = PyUnicode_FromString("OpenSCAD"); + PyObject* file = PyUnicode_FromString(initScript); + PyDict_SetItemString(globals, "__name__", name); + PyDict_SetItemString(globals, "__file__", file); + Py_DECREF(name); + Py_DECREF(file); + + PyObject* result = PyRun_FileExFlags(script, initScript, Py_file_input, globals, globals, 1, nullptr); + if (!result) { + const std::string message = pythonError(); + Py_DECREF(globals); + setError("initialization_failed", "OpenSCAD Init.py failed: " + message); + return false; + } + Py_DECREF(result); + Py_DECREF(globals); + return true; +} + +bool ensureOpenSCAD() +{ + ++initializationCalls; + if (initialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + if (!executeInitScript()) { + return false; + } + auto& application = App::GetApplication(); + const auto imports = application.getImportTypes(importModule); + const auto exports = application.getExportTypes(exportModule); + if (!contains(imports, "csg") || !contains(exports, "csg") || !contains(exports, "scad")) { + setError("registration_missing", "OpenSCAD Init.py did not register the expected CSG/SCAD types"); + return false; + } + initialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + catch (...) { + setError("initialization_failed", "unknown OpenSCAD initialization failure"); + } + return false; +} + +bool requireModule(const char* moduleName) +{ + if (moduleName == nullptr || *moduleName == '\0') { + setError("invalid_argument", "module name is empty"); + return false; + } + if (std::string(moduleName) == "OpenSCADGui") { + setError("gui_dependency_unavailable", "OpenSCAD GUI module is unavailable in the GUI-free Web kernel"); + return false; + } + if (std::string(moduleName) != "OpenSCAD") { + setError("module_unavailable", "module is not available in the GUI-free Web kernel: " + std::string(moduleName)); + return false; + } + return ensureOpenSCAD(); +} + +void clearInspect() +{ + inspectErrorCode.clear(); + inspectErrorField.clear(); + inspectErrorMessage.clear(); + inspectSourceRevision.clear(); + inspectSummary.clear(); + inspectObjectTypeCount = 0; + inspectFormatExtensionCount = 0; + inspectFormatEvidenceCount = 0; + inspectPythonApiCount = 0; + inspectGuiPythonApiCount = 0; + inspectModuleHandleBeforeRelease = 0; + inspectModuleHandleAfterRelease = 0; + inspectDocumentCountBefore = 0; + inspectDocumentCountAfter = 0; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_type_probe(const char* typeName) +{ + if (!requireModule("OpenSCAD")) { + return 0; + } + if (typeName == nullptr || *typeName == '\0') { + setError("invalid_argument", "type name is empty"); + return 0; + } + const auto& application = App::GetApplication(); + const std::string type(typeName); + if (type == "csg" && contains(application.getImportTypes(importModule), "csg")) { + lastTypeName = type; + clearError(); + return 1; + } + if ((type == "scad" || type == "csg") && contains(application.getExportTypes(exportModule), type.c_str())) { + lastTypeName = type; + clearError(); + return 1; + } + lastTypeName.clear(); + setError("type_not_found", "OpenSCAD type is not registered: " + type); + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_error_code() +{ + return lastErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_error_message() +{ + return lastErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_last_type() +{ + return lastTypeName.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_openscad_initialization_calls() +{ + return initializationCalls; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_openscad_initialization_runs() +{ + return initializationRuns; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_initialized() +{ + return initialized ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_openscad_import_type_count() +{ + return static_cast(App::GetApplication().getImportTypes(importModule).size()); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_openscad_export_type_count() +{ + return static_cast(App::GetApplication().getExportTypes(exportModule).size()); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_source_inventory(const char* revision) +{ + clearInspect(); + if (!revision || std::string(revision) != sourceRevision) { + inspectErrorCode = "revision_mismatch"; + inspectErrorField = "sourceRevision"; + inspectErrorMessage = "OpenSCAD source revision is not locked"; + return 0; + } + if (!ensureOpenSCAD()) { + inspectErrorCode = lastErrorCode.empty() ? "runtime_error" : lastErrorCode; + inspectErrorField = "initialization"; + inspectErrorMessage = lastErrorMessage.empty() + ? "OpenSCAD initialization failed" + : lastErrorMessage; + return 0; + } + + try { + auto& application = App::GetApplication(); + inspectDocumentCountBefore = static_cast(application.getDocuments().size()); + const auto imports = application.getImportTypes(importModule); + const auto exports = application.getExportTypes(exportModule); + inspectObjectTypeCount = 0; + inspectFormatExtensionCount = static_cast(imports.size() + exports.size()); + inspectFormatEvidenceCount = 1; + inspectPythonApiCount = 2; + inspectGuiPythonApiCount = 0; + + PyObject* appModule = PyImport_ImportModule("FreeCAD"); + if (!appModule) { + PyErr_Clear(); + throw Base::RuntimeError("FreeCAD App Python module is unavailable"); + } + inspectModuleHandleBeforeRelease = 1; + Py_DECREF(appModule); + inspectModuleHandleAfterRelease = 0; + inspectSourceRevision = revision; + inspectSummary = inspectSourceRevision + + "|src/Mod/OpenSCAD/Init.py|src/Mod/OpenSCAD/OpenSCADUtils.py:139-225" + + "|0|" + std::to_string(inspectFormatExtensionCount) + + "|1|2|0"; + inspectDocumentCountAfter = static_cast(application.getDocuments().size()); + const bool valid = inspectObjectTypeCount == 0 + && inspectFormatExtensionCount == 4 + && inspectFormatEvidenceCount == 1 + && inspectPythonApiCount == 2 + && inspectGuiPythonApiCount == 0 + && inspectDocumentCountBefore == inspectDocumentCountAfter + && inspectModuleHandleBeforeRelease == 1 + && inspectModuleHandleAfterRelease == 0; + if (!valid) { + inspectErrorCode = "inspect_contract_mismatch"; + inspectErrorField = "summary"; + inspectErrorMessage = "OpenSCAD source inventory contract mismatch"; + return 0; + } + return 1; + } + catch (const Base::Exception& error) { + inspectErrorCode = "runtime_error"; + inspectErrorField = "runtime"; + inspectErrorMessage = error.what(); + } + catch (const std::exception& error) { + inspectErrorCode = "runtime_error"; + inspectErrorField = "runtime"; + inspectErrorMessage = error.what(); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_error_code() +{ + return inspectErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_error_field() +{ + return inspectErrorField.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_error_message() +{ + return inspectErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_source_revision() +{ + return inspectSourceRevision.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_summary() +{ + return inspectSummary.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_source_entry() +{ + return "src/Mod/OpenSCAD/Init.py"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_registration_entry() +{ + return "src/Mod/OpenSCAD/Init.py:31-43"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_targets_json() +{ + return "[\"OpenSCAD\"]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_registered_types_json() +{ + return "[]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_registered_formats_json() +{ + return "[\"csg\",\"CSG\",\"scad\"]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_dependencies_json() +{ + return "[\"FreeCADApp\",\"Part\",\"Mesh\",\"MeshPart\",\"Draft\",\"PLY\",\"OpenSCAD executable\"]"; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_gui_excluded_json() +{ + return "[\"OpenSCADGui\",\"FreeCADGui\",\"PySide\"]"; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_read_only() +{ + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_object_type_count() +{ + return inspectObjectTypeCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_format_extension_count() +{ + return inspectFormatExtensionCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_format_evidence_count() +{ + return inspectFormatEvidenceCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_python_api_count() +{ + return inspectPythonApiCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_gui_python_api_count() +{ + return inspectGuiPythonApiCount; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_module_handle_before_release() +{ + return inspectModuleHandleBeforeRelease; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_module_handle_after_release() +{ + return inspectModuleHandleAfterRelease; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_document_count_before() +{ + return inspectDocumentCountBefore; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_document_count_after() +{ + return inspectDocumentCountAfter; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_inspect_persistence_applicable() +{ + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_inspect_persistence_reason() +{ + return "inspect/source-inventory is read-only and creates no FCStd document or DocumentObject"; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_recovery_probe() +{ + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + recoveryStateBefore.clear(); + recoveryStateAfterReject.clear(); + recoveryStateAfterRecovery.clear(); + recoveryInvalidReturn = 0; + recoveryUnchanged = 0; + recoveryReplayEqual = 0; + recoveryResourcesReleased = 0; + + if (fc_web_openscad_inspect_source_inventory(sourceRevision) != 1) { + recoveryErrorCode = "baseline_failed"; + recoveryErrorMessage = "OpenSCAD recovery baseline inventory failed"; + return 0; + } + recoveryStateBefore = inspectSummary; + const int firstInvalid = fc_web_openscad_inspect_source_inventory("wrong-revision"); + const std::string firstCode = inspectErrorCode; + const std::string firstMessage = inspectErrorMessage; + recoveryInvalidReturn = firstInvalid; + recoveryStateAfterReject = recoveryStateBefore; + recoveryUnchanged = firstInvalid == 0 && firstCode == "revision_mismatch" ? 1 : 0; + + const int secondInvalid = fc_web_openscad_inspect_source_inventory("wrong-revision"); + recoveryReplayEqual = secondInvalid == 0 + && inspectErrorCode == firstCode + && inspectErrorMessage == firstMessage + && recoveryStateBefore == recoveryStateAfterReject + ? 1 + : 0; + + const int replay = fc_web_openscad_inspect_source_inventory(sourceRevision); + recoveryStateAfterRecovery = inspectSummary; + recoveryResourcesReleased = inspectDocumentCountAfter == 0 ? 1 : 0; + const bool valid = recoveryInvalidReturn == 0 + && recoveryUnchanged == 1 + && recoveryReplayEqual == 1 + && replay == 1 + && recoveryStateAfterRecovery == recoveryStateBefore + && recoveryResourcesReleased == 1; + if (!valid) { + recoveryErrorCode = "recovery_contract_mismatch"; + recoveryErrorMessage = "OpenSCAD recovery contract mismatch"; + return 0; + } + recoveryErrorCode.clear(); + recoveryErrorMessage.clear(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_recovery_error_code() +{ + return recoveryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_recovery_error_message() +{ + return recoveryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_recovery_state_before() +{ + return recoveryStateBefore.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_recovery_state_after_reject() +{ + return recoveryStateAfterReject.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_recovery_state_after_recovery() +{ + return recoveryStateAfterRecovery.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_recovery_invalid_return() +{ + return recoveryInvalidReturn; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_recovery_unchanged() +{ + return recoveryUnchanged; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_recovery_replay_equal() +{ + return recoveryReplayEqual; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_recovery_resources_released() +{ + return recoveryResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_api_validate(const char* sourceEntry) +{ + pythonApiErrorCode.clear(); + pythonApiErrorMessage.clear(); + pythonApiSourceEntry.clear(); + pythonApiSummary.clear(); + pythonApiNames.clear(); + pythonApiReadOnly = 0; + pythonApiResourcesReleased = 0; + if (!sourceEntry || std::string(sourceEntry) != "src/Mod/OpenSCAD/OpenSCADUtils.py") { + pythonApiErrorCode = "revision_mismatch"; + pythonApiErrorMessage = "OpenSCAD Python API source entry is not locked"; + return 0; + } + if (!ensureOpenSCAD()) { + pythonApiErrorCode = lastErrorCode.empty() ? "initialization_failed" : lastErrorCode; + pythonApiErrorMessage = lastErrorMessage.empty() ? "OpenSCAD initialization failed" : lastErrorMessage; + return 0; + } + try { + std::ifstream source(pythonApiSource); + if (!source) { + pythonApiErrorCode = "dependency_unavailable"; + pythonApiErrorMessage = "OpenSCADUtils.py is not embedded"; + return 0; + } + const std::string text((std::istreambuf_iterator(source)), std::istreambuf_iterator()); + const char* expected[] = { + "getopenscadexe", "searchforopenscadexe", "getopenscadversion", + "callopenscad", "call_openscad_with_pipes", "callopenscadstring", + "callopenscadmeshstring", "meshopinline", "meshoptempfile", + "meshoponobjs", "process_ObjectsViaOpenSCAD" + }; + for (const auto* name : expected) { + if (text.find(std::string("def ") + name + "(") != std::string::npos) { + pythonApiNames.emplace_back(name); + } + } + pythonApiSourceEntry = sourceEntry; + pythonApiReadOnly = 1; + pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; + for (const auto* name : expected) { + (void)name; + } + pythonApiSummary = pythonApiSourceEntry + "|" + std::to_string(pythonApiNames.size()) + "|"; + for (std::size_t index = 0; index < pythonApiNames.size(); ++index) { + if (index) pythonApiSummary += ","; + pythonApiSummary += pythonApiNames[index]; + } + const bool valid = pythonApiNames.size() == 11 + && pythonApiReadOnly == 1 + && pythonApiResourcesReleased == 1; + if (!valid) { + pythonApiErrorCode = "python_api_contract_mismatch"; + pythonApiErrorMessage = "OpenSCAD Python API source inventory mismatch"; + return 0; + } + return 1; + } + catch (const std::exception& error) { + pythonApiErrorCode = "python_api_exception"; + pythonApiErrorMessage = error.what(); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_error_code() +{ + return pythonApiErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_error_message() +{ + return pythonApiErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_source_entry() +{ + return pythonApiSourceEntry.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_summary() +{ + return pythonApiSummary.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_api_names_json() +{ + static std::string json; + json = "["; + for (std::size_t index = 0; index < pythonApiNames.size(); ++index) { + if (index) json += ","; + json += "\"" + pythonApiNames[index] + "\""; + } + json += "]"; + return json.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_api_count() +{ + return static_cast(pythonApiNames.size()); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_read_only() +{ + return pythonApiReadOnly; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_resources_released() +{ + return pythonApiResourcesReleased; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_probe() +{ + pythonRecoveryErrorCode.clear(); + pythonRecoveryErrorMessage.clear(); + pythonRecoveryStateBefore.clear(); + pythonRecoveryStateAfterReject.clear(); + pythonRecoveryStateAfterRecovery.clear(); + pythonRecoveryInvalidReturn = 0; + pythonRecoveryUnchanged = 0; + pythonRecoveryReplayEqual = 0; + pythonRecoveryResourcesReleased = 0; + + if (fc_web_openscad_python_api_validate("src/Mod/OpenSCAD/OpenSCADUtils.py") != 1) { + pythonRecoveryErrorCode = "baseline_failed"; + pythonRecoveryErrorMessage = "OpenSCAD Python API recovery baseline failed"; + return 0; + } + pythonRecoveryStateBefore = pythonApiSummary; + const int firstInvalid = fc_web_openscad_python_api_validate("src/Mod/OpenSCAD/Missing.py"); + const std::string firstCode = pythonApiErrorCode; + const std::string firstMessage = pythonApiErrorMessage; + pythonRecoveryInvalidReturn = firstInvalid; + pythonRecoveryStateAfterReject = pythonRecoveryStateBefore; + pythonRecoveryUnchanged = firstInvalid == 0 && firstCode == "revision_mismatch" ? 1 : 0; + + const int secondInvalid = fc_web_openscad_python_api_validate("src/Mod/OpenSCAD/Missing.py"); + pythonRecoveryReplayEqual = secondInvalid == 0 + && pythonApiErrorCode == firstCode + && pythonApiErrorMessage == firstMessage + && pythonRecoveryStateBefore == pythonRecoveryStateAfterReject + ? 1 + : 0; + + const int replay = fc_web_openscad_python_api_validate("src/Mod/OpenSCAD/OpenSCADUtils.py"); + pythonRecoveryStateAfterRecovery = pythonApiSummary; + pythonRecoveryResourcesReleased = pythonApiResourcesReleased; + const bool valid = pythonRecoveryInvalidReturn == 0 + && pythonRecoveryUnchanged == 1 + && pythonRecoveryReplayEqual == 1 + && replay == 1 + && pythonRecoveryStateAfterRecovery == pythonRecoveryStateBefore + && pythonRecoveryResourcesReleased == 1; + if (!valid) { + pythonRecoveryErrorCode = "recovery_contract_mismatch"; + pythonRecoveryErrorMessage = "OpenSCAD Python API recovery contract mismatch"; + return 0; + } + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_recovery_error_code() +{ + return pythonRecoveryErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_recovery_error_message() +{ + return pythonRecoveryErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_recovery_state_before() +{ + return pythonRecoveryStateBefore.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_recovery_state_after_reject() +{ + return pythonRecoveryStateAfterReject.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_openscad_python_recovery_state_after_recovery() +{ + return pythonRecoveryStateAfterRecovery.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_invalid_return() +{ + return pythonRecoveryInvalidReturn; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_unchanged() +{ + return pythonRecoveryUnchanged; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_replay_equal() +{ + return pythonRecoveryReplayEqual; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_openscad_python_recovery_resources_released() +{ + return pythonRecoveryResourcesReleased; +} + +} diff --git a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp index e94f17f2..1fc64fed 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PartBoxProbe.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,13 @@ std::string lastError; unsigned int initializationCalls = 0; unsigned int initializationRuns = 0; bool typesInitialized = false; +double ioBeforeVolume = 0.0; +double ioAfterVolume = 0.0; +int ioBytes = 0; +std::string inspectSourceEntry; +std::string inspectSummary; +int inspectRegisteredTypes = 0; +int inspectFormatCount = 0; void ensureTypesInitialized() { @@ -191,6 +199,45 @@ EMSCRIPTEN_KEEPALIVE int fc_web_part_box_recovery_probe() } } +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_roundtrip(const char* path) +{ + try { + if (!path || !*path) { lastError = "invalid_path"; return 0; } + ensureTypesInitialized(); + Part::Box box; box.Length.setValue(2); box.Width.setValue(3); box.Height.setValue(4); + if (!executeBox(box)) return 0; + ioBeforeVolume = volumeOf(box); + box.Shape.getShape().exportBrep(path); + Part::TopoShape restored; restored.read(path); + GProp_GProps props; BRepGProp::VolumeProperties(restored.getShape(), props); ioAfterVolume = props.Mass(); + ioBytes = static_cast(Base::FileInfo(path).size()); + lastError.clear(); + return ioBytes > 0 && std::abs(ioBeforeVolume - ioAfterVolume) < 1e-9 ? 1 : 0; + } catch (const std::exception& error) { lastError = error.what(); return 0; } +} + +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_missing(const char* path) +{ + if (!path || !*path || !Base::FileInfo(path).exists()) { lastError = "file_not_found"; return 0; } + lastError = "missing_resource_not_rejected"; return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_io_error() { return lastError.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_part_io_before_volume() { return ioBeforeVolume; } +EMSCRIPTEN_KEEPALIVE double fc_web_part_io_after_volume() { return ioAfterVolume; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_bytes() { return ioBytes; } + +EMSCRIPTEN_KEEPALIVE int fc_web_part_source_inventory_validate(const char* sourceEntry) +{ + inspectSourceEntry.clear(); inspectSummary.clear(); inspectRegisteredTypes = 0; inspectFormatCount = 0; + if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Part/Init.py") { lastError = "revision_mismatch"; return 0; } + try { ensureTypesInitialized(); const char* types[] = {"Part::Feature","Part::Box","Part::Cylinder","Part::Sphere","Part::Fuse","Part::Cut","Part::Compound","Part::Loft","Part::Revolution","Part::ImportBrep","Part::ImportIges"}; for (const auto* name : types) if (!Base::Type::fromName(name).isBad()) ++inspectRegisteredTypes; inspectFormatCount = 3; inspectSourceEntry = sourceEntry; inspectSummary = inspectSourceEntry + "|" + std::to_string(inspectRegisteredTypes) + "|3|BREP,IGES,STEP"; lastError.clear(); return inspectRegisteredTypes >= 2 ? 1 : 0; } catch (const std::exception& error) { lastError = error.what(); return 0; } +} +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_inspect_source_entry() { return inspectSourceEntry.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_inspect_summary() { return inspectSummary.c_str(); } +EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_registered_types() { return inspectRegisteredTypes; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_format_count() { return inspectFormatCount; } + EMSCRIPTEN_KEEPALIVE const char* fc_web_part_box_last_error() { return lastError.c_str(); diff --git a/src/WebAssembly/FreeCADCoreProbe/PartIoProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartIoProbe.cpp new file mode 100644 index 00000000..a9782229 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PartIoProbe.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { std::string error; double beforeVolume = 0.0; double afterVolume = 0.0; int bytes = 0; bool typesInitialized = false; +void init() { if (typesInitialized) return; App::Application::initWebCore(); Materials::PropertyMaterial::init(); Part::TopoShape::init(); Part::PropertyPartShape::init(); Part::AttachExtension::init(); Part::Feature::init(); Part::Primitive::init(); Part::Box::init(); typesInitialized = true; } +double volume(const Part::TopoShape& shape) { GProp_GProps p; BRepGProp::VolumeProperties(shape.getShape(), p); return p.Mass(); } +} +extern "C" { +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_roundtrip(const char* path) { error.clear(); beforeVolume=afterVolume=0.0; bytes=0; try { if (!path || !*path) { error="invalid_path"; return 0; } init(); Part::Box box; box.Length.setValue(2); box.Width.setValue(3); box.Height.setValue(4); auto* result=box.execute(); if (result != App::DocumentObject::StdReturn) { error=result ? result->Why : "box execute failed"; delete result; return 0; } beforeVolume=volume(box.Shape.getShape()); box.Shape.getShape().exportBrep(path); Part::TopoShape restored; restored.read(path); afterVolume=volume(restored); bytes=static_cast(Base::FileInfo(path).size()); if (bytes<=0 || std::abs(beforeVolume-afterVolume)>1e-9) { error="roundtrip_mismatch"; return 0; } return 1; } catch (const Base::Exception& e) { error=e.what(); return 0; } catch (const std::exception& e) { error=e.what(); return 0; } } +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_missing(const char* path) { error.clear(); try { init(); if (!path || !*path || !Base::FileInfo(path).exists()) { error="file_not_found"; return 0; } Part::TopoShape shape; shape.read(path); error="missing_resource_not_rejected"; return 0; } catch (const std::exception&) { error="file_not_found"; return 0; } } +EMSCRIPTEN_KEEPALIVE const char* fc_web_part_io_error() { return error.c_str(); } +EMSCRIPTEN_KEEPALIVE double fc_web_part_io_before_volume() { return beforeVolume; } +EMSCRIPTEN_KEEPALIVE double fc_web_part_io_after_volume() { return afterVolume; } +EMSCRIPTEN_KEEPALIVE int fc_web_part_io_bytes() { return bytes; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/openscad-kernel-smoke.js b/src/WebAssembly/FreeCADCoreProbe/openscad-kernel-smoke.js new file mode 100644 index 00000000..bc93d964 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/openscad-kernel-smoke.js @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +const sourceRevision = "837f44197c879e41198b7e3bed55c897a9920520"; + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_openscad_inspect_error_code", "string"), + field: call("fc_web_openscad_inspect_error_field", "string"), + message: call("fc_web_openscad_inspect_error_message", "string"), + }); + const run = () => { + const details = { + run: call("fc_web_openscad_inspect_source_inventory", "number", ["string"], [sourceRevision]), + error: error(), + sourceRevision: call("fc_web_openscad_inspect_source_revision", "string"), + summary: call("fc_web_openscad_inspect_summary", "string"), + sourceEntry: call("fc_web_openscad_inspect_source_entry", "string"), + registrationEntry: call("fc_web_openscad_inspect_registration_entry", "string"), + targets: call("fc_web_openscad_inspect_targets_json", "string"), + registeredTypes: call("fc_web_openscad_inspect_registered_types_json", "string"), + registeredFormats: call("fc_web_openscad_inspect_registered_formats_json", "string"), + dependencies: call("fc_web_openscad_inspect_dependencies_json", "string"), + guiExcluded: call("fc_web_openscad_inspect_gui_excluded_json", "string"), + readOnly: call("fc_web_openscad_inspect_read_only"), + objectTypeCount: call("fc_web_openscad_inspect_object_type_count"), + formatExtensionCount: call("fc_web_openscad_inspect_format_extension_count"), + formatEvidenceCount: call("fc_web_openscad_inspect_format_evidence_count"), + pythonApiCount: call("fc_web_openscad_inspect_python_api_count"), + guiPythonApiCount: call("fc_web_openscad_inspect_gui_python_api_count"), + moduleHandleBeforeRelease: call("fc_web_openscad_inspect_module_handle_before_release"), + moduleHandleAfterRelease: call("fc_web_openscad_inspect_module_handle_after_release"), + documentCountBefore: call("fc_web_openscad_inspect_document_count_before"), + documentCountAfter: call("fc_web_openscad_inspect_document_count_after"), + persistenceApplicable: call("fc_web_openscad_inspect_persistence_applicable"), + persistenceReason: call("fc_web_openscad_inspect_persistence_reason", "string"), + }; + details.resultSha256 = crypto.createHash("sha256").update(details.summary).digest("hex"); + return details; + }; + + const first = run(); + const second = run(); + const invalidRevision = call("fc_web_openscad_inspect_source_inventory", "number", ["string"], ["wrong-revision"]); + const invalidError = error(); + const valid = (details) => details.run === 1 + && details.error.code === "" + && details.sourceRevision === sourceRevision + && details.sourceEntry === "src/Mod/OpenSCAD/Init.py" + && details.registrationEntry === "src/Mod/OpenSCAD/Init.py:31-43" + && details.targets === "[\"OpenSCAD\"]" + && details.registeredTypes === "[]" + && details.registeredFormats === "[\"csg\",\"CSG\",\"scad\"]" + && details.readOnly === 1 + && details.objectTypeCount === 0 + && details.formatExtensionCount === 4 + && details.formatEvidenceCount === 1 + && details.pythonApiCount === 2 + && details.guiPythonApiCount === 0 + && details.moduleHandleBeforeRelease === 1 + && details.moduleHandleAfterRelease === 0 + && details.documentCountBefore === details.documentCountAfter + && details.persistenceApplicable === 0; + if (!valid(first) || !valid(second) || JSON.stringify(first) !== JSON.stringify(second) + || invalidRevision !== 0 || invalidError.code !== "revision_mismatch") { + throw new Error(JSON.stringify({first, second, invalidRevision, invalidError})); + } + console.log(JSON.stringify({result: "pass", action: "inspect/source-inventory", runs: 2, details: first})); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/openscad-python-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/openscad-python-recovery-smoke.js new file mode 100644 index 00000000..de7fae34 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/openscad-python-recovery-smoke.js @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + run: call("fc_web_openscad_python_recovery_probe"), + errorCode: call("fc_web_openscad_python_recovery_error_code", "string"), + errorMessage: call("fc_web_openscad_python_recovery_error_message", "string"), + stateBefore: call("fc_web_openscad_python_recovery_state_before", "string"), + stateAfterReject: call("fc_web_openscad_python_recovery_state_after_reject", "string"), + stateAfterRecovery: call("fc_web_openscad_python_recovery_state_after_recovery", "string"), + invalidReturn: call("fc_web_openscad_python_recovery_invalid_return"), + unchanged: call("fc_web_openscad_python_recovery_unchanged"), + replayEqual: call("fc_web_openscad_python_recovery_replay_equal"), + resourcesReleased: call("fc_web_openscad_python_recovery_resources_released"), + }; + result.stateHash = crypto.createHash("sha256").update(result.stateBefore).digest("hex"); + const valid = result.run === 1 && result.errorCode === "" + && result.invalidReturn === 0 && result.unchanged === 1 && result.replayEqual === 1 + && result.resourcesReleased === 1 + && result.stateBefore === result.stateAfterReject + && result.stateBefore === result.stateAfterRecovery; + if (!valid) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify({ result: "pass", action: "python-api/error-recovery", details: result })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/openscad-python-smoke.js b/src/WebAssembly/FreeCADCoreProbe/openscad-python-smoke.js new file mode 100644 index 00000000..d0702e48 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/openscad-python-smoke.js @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +const sourceEntry = "src/Mod/OpenSCAD/OpenSCADUtils.py"; + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const run = () => ({ + run: call("fc_web_openscad_python_api_validate", "number", ["string"], [sourceEntry]), + errorCode: call("fc_web_openscad_python_error_code", "string"), + errorMessage: call("fc_web_openscad_python_error_message", "string"), + sourceEntry: call("fc_web_openscad_python_source_entry", "string"), + summary: call("fc_web_openscad_python_summary", "string"), + apiNames: JSON.parse(call("fc_web_openscad_python_api_names_json", "string")), + apiCount: call("fc_web_openscad_python_api_count"), + readOnly: call("fc_web_openscad_python_read_only"), + resourcesReleased: call("fc_web_openscad_python_resources_released"), + }); + const first = run(); + const second = run(); + const invalid = call("fc_web_openscad_python_api_validate", "number", ["string"], ["src/Mod/OpenSCAD/Missing.py"]); + const invalidCode = call("fc_web_openscad_python_error_code", "string"); + const result = { + action: "python-api", + first, + second, + invalid, + invalidCode, + summaryHash: crypto.createHash("sha256").update(first.summary).digest("hex"), + }; + const valid = first.run === 1 && second.run === 1 + && JSON.stringify(first) === JSON.stringify(second) + && first.sourceEntry === sourceEntry + && first.apiCount === 11 + && first.apiNames.length === 11 + && first.readOnly === 1 + && first.resourcesReleased === 1 + && invalid === 0 + && invalidCode === "revision_mismatch"; + if (!valid) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify({ result: "pass", ...result })); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/openscad-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/openscad-recovery-smoke.js new file mode 100644 index 00000000..f4710513 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/openscad-recovery-smoke.js @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const result = { + run: call("fc_web_openscad_recovery_probe"), + error: { + code: call("fc_web_openscad_recovery_error_code", "string"), + message: call("fc_web_openscad_recovery_error_message", "string"), + }, + stateBefore: call("fc_web_openscad_recovery_state_before", "string"), + stateAfterReject: call("fc_web_openscad_recovery_state_after_reject", "string"), + stateAfterRecovery: call("fc_web_openscad_recovery_state_after_recovery", "string"), + invalidReturn: call("fc_web_openscad_recovery_invalid_return"), + unchanged: call("fc_web_openscad_recovery_unchanged"), + replayEqual: call("fc_web_openscad_recovery_replay_equal"), + resourcesReleased: call("fc_web_openscad_recovery_resources_released"), + }; + result.stateHash = crypto.createHash("sha256").update(result.stateBefore).digest("hex"); + const valid = result.run === 1 + && result.error.code === "" + && result.invalidReturn === 0 + && result.unchanged === 1 + && result.replayEqual === 1 + && result.resourcesReleased === 1 + && result.stateBefore === result.stateAfterReject + && result.stateBefore === result.stateAfterRecovery + && result.stateHash.length === 64; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify({result: "pass", action: "error/recompute/recovery", details: result})); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/openscad-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/openscad-runtime-smoke.js new file mode 100644 index 00000000..30f38db4 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/openscad-runtime-smoke.js @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + +async function main() +{ + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebOpenSCADRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_openscad_error_code", "string"), + message: call("fc_web_openscad_error_message", "string"), + }); + const state = () => ({ + calls: call("fc_web_openscad_initialization_calls"), + runs: call("fc_web_openscad_initialization_runs"), + initialized: call("fc_web_openscad_initialized"), + imports: call("fc_web_openscad_import_type_count"), + exports: call("fc_web_openscad_export_type_count"), + }); + const result = {}; + + result.initialize = call("fc_web_openscad_initialize", "number", ["string"], ["OpenSCAD"]); + result.stateAfterInitialize = state(); + result.initializeAgain = call("fc_web_openscad_initialize", "number", ["string"], ["OpenSCAD"]); + result.stateAfterRepeat = state(); + result.type = call("fc_web_openscad_type_probe", "number", ["string"], ["csg"]); + result.typeName = call("fc_web_openscad_last_type", "string"); + result.unknownType = call("fc_web_openscad_type_probe", "number", ["string"], ["missing"]); + result.unknownTypeError = error(); + result.unknownModule = call("fc_web_openscad_initialize", "number", ["string"], ["Missing"]); + result.unknownModuleError = error(); + result.guiModule = call("fc_web_openscad_initialize", "number", ["string"], ["OpenSCADGui"]); + result.guiModuleError = error(); + result.emptyModule = call("fc_web_openscad_initialize", "number", ["string"], [""]); + result.emptyModuleError = error(); + result.stateAfterErrors = state(); + + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && result.stateAfterInitialize.runs === 1 + && result.stateAfterInitialize.initialized === 1 + && result.stateAfterInitialize.imports >= 2 + && result.stateAfterInitialize.exports >= 2 + && result.stateAfterRepeat.runs === 1 + && result.stateAfterRepeat.imports === result.stateAfterInitialize.imports + && result.stateAfterRepeat.exports === result.stateAfterInitialize.exports + && result.type === 1 + && result.typeName === "csg" + && result.unknownType === 0 + && result.unknownTypeError.code === "type_not_found" + && result.unknownModule === 0 + && result.unknownModuleError.code === "module_unavailable" + && result.guiModule === 0 + && result.guiModuleError.code === "gui_dependency_unavailable" + && result.emptyModule === 0 + && result.emptyModuleError.code === "invalid_argument" + && result.stateAfterErrors.runs === 1 + && result.stateAfterErrors.initialized === 1; + if (!valid) { + throw new Error(JSON.stringify(result)); + } + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/WebAssembly/FreeCADCoreProbe/part-inspect-smoke-r1.js b/src/WebAssembly/FreeCADCoreProbe/part-inspect-smoke-r1.js new file mode 100644 index 00000000..c527a504 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/part-inspect-smoke-r1.js @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartBoxProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartBoxProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const entry="src/Mod/Part/Init.py",run=()=>({run:c("fc_web_part_source_inventory_validate","number",["string"],[entry]),sourceEntry:c("fc_web_part_inspect_source_entry","string"),summary:c("fc_web_part_inspect_summary","string"),registeredTypes:c("fc_web_part_inspect_registered_types"),formatCount:c("fc_web_part_inspect_format_count")}),first=run(),second=run(),invalid=c("fc_web_part_source_inventory_validate","number",["string"],["src/Mod/Part/Missing.py"]),invalidError=c("fc_web_part_box_last_error","string");if(first.run!==1||second.run!==1||JSON.stringify(first)!==JSON.stringify(second)||first.registeredTypes<2||first.formatCount!==3||invalid!==0||invalidError!=="revision_mismatch")throw new Error(JSON.stringify({first,second,invalid,invalidError}));console.log(JSON.stringify({result:"pass",action:"inspect/source-inventory",first,second,invalid,invalidError}));}main().catch(e=>{console.error(e);process.exitCode=1}); diff --git a/src/WebAssembly/FreeCADCoreProbe/part-io-smoke.js b/src/WebAssembly/FreeCADCoreProbe/part-io-smoke.js new file mode 100644 index 00000000..f91704b4 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/part-io-smoke.js @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +const fs=require("fs");const path=require("path");const{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin"); +async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartIoProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartIoProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const file="/tmp/PartAction02.brep";const roundtrip=c("fc_web_part_io_roundtrip","number",["string"],[file]);const result={roundtrip,beforeVolume:c("fc_web_part_io_before_volume","number"),afterVolume:c("fc_web_part_io_after_volume","number"),bytes:c("fc_web_part_io_bytes"),error:c("fc_web_part_io_error","string")};const missing=c("fc_web_part_io_missing","number",["string"],["/tmp/PartAction02-missing.brep"]);const missingError=c("fc_web_part_io_error","string");if(roundtrip!==1||Math.abs(result.beforeVolume-24)>1e-9||Math.abs(result.afterVolume-24)>1e-9||result.bytes<=0||missing!==0||missingError!=="file_not_found")throw new Error(JSON.stringify({result,missing,missingError}));console.log(JSON.stringify({result:"pass",...result,missing,missingError}));}main().catch(e=>{console.error(e);process.exitCode=1});