diff --git a/CMakeLists.txt b/CMakeLists.txt index 07175ee1..2a8b353c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ option(FREECAD_WEB "Build the FreeCAD WebAssembly core configuration" OFF) option(FREECAD_WEB_ENABLE_ASSEMBLY "Enable the Assembly Web probe and its dependency closure" OFF) 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) if(FREECAD_WEB) if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(FATAL_ERROR @@ -72,6 +73,7 @@ if(FREECAD_WEB) set(FREECAD_USE_PCH OFF CACHE BOOL "Web core does not use desktop PCH defaults" FORCE) set(FREECAD_USE_FREETYPE OFF CACHE BOOL "Web core leaves optional FreeType disabled" FORCE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include${CMAKE_SOURCE_DIR}/src/FCWebRangesCompat.h") + add_compile_definitions(FREECAD_WEB_CORE) set(_freecad_web_disabled_options BUILD_ADDONMGR @@ -118,6 +120,9 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_CLOUD) list(REMOVE_ITEM _freecad_web_disabled_options BUILD_CLOUD) endif() + if(FREECAD_WEB_ENABLE_INSPECTION) + list(REMOVE_ITEM _freecad_web_disabled_options BUILD_INSPECTION BUILD_MESH BUILD_POINTS) + 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() @@ -139,6 +144,11 @@ if(FREECAD_WEB) if(FREECAD_WEB_ENABLE_CLOUD) set(BUILD_CLOUD ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_CLOUD" FORCE) endif() + if(FREECAD_WEB_ENABLE_INSPECTION) + foreach(_freecad_web_inspection_option BUILD_INSPECTION BUILD_MESH BUILD_POINTS) + set(${_freecad_web_inspection_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_INSPECTION" FORCE) + endforeach() + endif() include(cMake/FreeCADWebDependencies.cmake) endif() diff --git a/src/App/Document.cpp b/src/App/Document.cpp index fceed1d4..b6212626 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -1598,9 +1598,9 @@ std::vector Document::readObjects(Base::XMLReader& reader) long lastId = 0; for (int i = 0; i < Cnt; i++) { - reader.readElement("Object"); - std::string type = reader.getAttribute("type"); - std::string name = reader.getAttribute("name"); + reader.readElement("Object"); + std::string type = reader.getAttribute("type"); + std::string name = reader.getAttribute("name"); std::string viewType = reader.hasAttribute("ViewType") ? reader.getAttribute("ViewType") : ""; @@ -2082,38 +2082,38 @@ bool Document::isAnyRestoring() // Open the document void Document::restore(const char* filename, - bool delaySignal, - const std::vector& objNames) + bool delaySignal, + const std::vector& objNames) { - clearUndos(); - d->activeObject = nullptr; - - bool signal = false; - Document* activeDoc = GetApplication().getActiveDocument(); - if (!d->objectArray.empty()) { - signal = true; - GetApplication().signalDeleteDocument(*this); - d->clearDocument(); - } - - Base::FlagToggler<> flag(globalIsRestoring, false); - - setStatus(Document::PartialDoc, false); - - d->clearRecomputeLog(); - d->objectLabelManager.clear(); - d->objectArray.clear(); - d->objectNameManager.clear(); - d->objectMap.clear(); - d->objectIdMap.clear(); - d->lastObjectId = 0; - - if (signal) { - GetApplication().signalNewDocument(*this, true); - if (activeDoc == this) { - GetApplication().setActiveDocument(this); - } - } + clearUndos(); + d->activeObject = nullptr; + + bool signal = false; + Document* activeDoc = GetApplication().getActiveDocument(); + if (!d->objectArray.empty()) { + signal = true; + GetApplication().signalDeleteDocument(*this); + d->clearDocument(); + } + + Base::FlagToggler<> flag(globalIsRestoring, false); + + setStatus(Document::PartialDoc, false); + + d->clearRecomputeLog(); + d->objectLabelManager.clear(); + d->objectArray.clear(); + d->objectNameManager.clear(); + d->objectMap.clear(); + d->objectIdMap.clear(); + d->lastObjectId = 0; + + if (signal) { + GetApplication().signalNewDocument(*this, true); + if (activeDoc == this) { + GetApplication().setActiveDocument(this); + } + } if (!filename) { filename = FileName.getValue(); @@ -2178,7 +2178,7 @@ void Document::restore(const char* filename, throw Base::FileException("Error reading compression file", filename); } - GetApplication().signalStartRestoreDocument(*this); + GetApplication().signalStartRestoreDocument(*this); setStatus(Document::Restoring, true); d->partialLoadObjects.clear(); diff --git a/src/Mod/Inspection/App/InspectionFeature.cpp b/src/Mod/Inspection/App/InspectionFeature.cpp index 0ec5affa..9e741be2 100644 --- a/src/Mod/Inspection/App/InspectionFeature.cpp +++ b/src/Mod/Inspection/App/InspectionFeature.cpp @@ -34,10 +34,12 @@ #include #include -#include -#include -#include -#include +#include +#ifndef FREECAD_WEB_CORE +#include +#include +#include +#endif #include #include @@ -930,7 +932,8 @@ App::DocumentObjectExecReturn* Feature::execute() DistanceInspectionRMS res; - if (useMultithreading) { +#ifndef FREECAD_WEB_CORE + if (useMultithreading) { // Build vector of increasing indices std::vector index(count); std::iota(index.begin(), index.end(), 0); @@ -958,8 +961,11 @@ App::DocumentObjectExecReturn* Feature::execute() watcher.setFuture(future); loop.exec(); res = future.result(); - } - else { + } + else { +#else + { +#endif // Single-threaded operation std::stringstream str; str << "Inspecting " << this->Label.getValue() << "…"; diff --git a/src/Mod/Inspection/App/PreCompiled.h b/src/Mod/Inspection/App/PreCompiled.h index cbccc39c..56707698 100644 --- a/src/Mod/Inspection/App/PreCompiled.h +++ b/src/Mod/Inspection/App/PreCompiled.h @@ -42,7 +42,9 @@ #include // Qt -#include -#include -#include -#include +#include +#ifndef FREECAD_WEB_CORE +#include +#include +#include +#endif diff --git a/src/Mod/Mesh/App/Core/Curvature.cpp b/src/Mod/Mesh/App/Core/Curvature.cpp index 1b68d13a..39fdde5e 100644 --- a/src/Mod/Mesh/App/Core/Curvature.cpp +++ b/src/Mod/Mesh/App/Core/Curvature.cpp @@ -26,9 +26,11 @@ #include #include +#ifndef FREECAD_WEB_CORE #include #include #include +#endif #include #include @@ -72,7 +74,11 @@ void MeshCurvature::ComputePerFace(bool parallel) MeshRefPointToFacets search(myKernel); FacetCurvature face(myKernel, search, myRadius, myMinPoints); +#ifdef FREECAD_WEB_CORE + if (true) { +#else if (!parallel) { +#endif Base::SequencerLauncher seq("Curvature estimation", mySegment.size()); for (FacetIndex it : mySegment) { CurvatureInfo info = face.Compute(it); @@ -80,6 +86,7 @@ void MeshCurvature::ComputePerFace(bool parallel) seq.next(); } } +#ifndef FREECAD_WEB_CORE else { // NOLINTBEGIN QFuture future @@ -92,6 +99,7 @@ void MeshCurvature::ComputePerFace(bool parallel) myCurvature.push_back(it); } } +#endif } #ifdef OPTIMIZE_CURVATURE diff --git a/src/Mod/Points/App/Points.cpp b/src/Mod/Points/App/Points.cpp index e21e1020..fc4707ae 100644 --- a/src/Mod/Points/App/Points.cpp +++ b/src/Mod/Points/App/Points.cpp @@ -22,7 +22,9 @@ * * ***************************************************************************/ -#include +#ifndef FREECAD_WEB_CORE +#include +#endif #include #include #include @@ -36,9 +38,9 @@ #include "PointsAlgos.h" -#ifdef _MSC_VER -# include -#endif +#ifdef _MSC_VER +# include +#endif using namespace Points; using namespace std; @@ -84,7 +86,11 @@ Data::Segment* PointKernel::getSubElement(const char* /*Type*/, unsigned long /* void PointKernel::transformGeometry(const Base::Matrix4D& rclMat) { std::vector& kernel = getBasicPoints(); -#ifdef _MSC_VER +#if defined(FREECAD_WEB_CORE) + std::for_each(kernel.begin(), kernel.end(), [rclMat](value_type& value) { + rclMat.multVec(value, value); + }); +#elif defined(_MSC_VER) // Win32-only at the moment since ppl.h is a Microsoft library. Points is not using Qt so we // cannot use QtConcurrent We could also rewrite Points to leverage SIMD instructions Other // option: openMP. But with VC2013 results in high CPU usage even after computation (busy-waits @@ -92,7 +98,7 @@ void PointKernel::transformGeometry(const Base::Matrix4D& rclMat) Concurrency::parallel_for_each(kernel.begin(), kernel.end(), [rclMat](value_type& value) { value = rclMat * value; }); -#else +#else QtConcurrent::blockingMap(kernel, [rclMat](value_type& value) { rclMat.multVec(value, value); }); #endif } @@ -101,7 +107,11 @@ void PointKernel::moveGeometry(const Base::Vector3d& vec) { Base::Vector3f offset = Base::toVector(vec); std::vector& kernel = getBasicPoints(); -#ifdef _MSC_VER +#if defined(FREECAD_WEB_CORE) + std::for_each(kernel.begin(), kernel.end(), [offset](value_type& value) { + value += offset; + }); +#elif defined(_MSC_VER) Concurrency::parallel_for_each(kernel.begin(), kernel.end(), [offset](value_type& value) { value += offset; }); diff --git a/src/Mod/Points/App/Properties.cpp b/src/Mod/Points/App/Properties.cpp index d8901589..5f16c630 100644 --- a/src/Mod/Points/App/Properties.cpp +++ b/src/Mod/Points/App/Properties.cpp @@ -22,7 +22,9 @@ * * ***************************************************************************/ -#include +#ifndef FREECAD_WEB_CORE +#include +#endif #include #include #include @@ -38,9 +40,9 @@ #include "Points.h" #include "Properties.h" -#ifdef _MSC_VER -# include -#endif +#ifdef _MSC_VER +# include +#endif using namespace Points; @@ -383,11 +385,15 @@ void PropertyNormalList::transformGeometry(const Base::Matrix4D& mat) aboutToSetValue(); // Rotate the normal vectors -#ifdef _MSC_VER +#if defined(FREECAD_WEB_CORE) + std::for_each(_lValueList.begin(), _lValueList.end(), [rot](Base::Vector3f& value) { + rot.multVec(value, value); + }); +#elif defined(_MSC_VER) Concurrency::parallel_for_each(_lValueList.begin(), _lValueList.end(), [rot](Base::Vector3f& value) { value = rot * value; }); -#else +#else QtConcurrent::blockingMap(_lValueList, [rot](Base::Vector3f& value) { rot.multVec(value, value); }); diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index 189fa4b7..59943741 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -74,6 +74,81 @@ set_target_properties(FreeCADWebModuleLoaderProbe PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) +add_executable(FreeCADWebImportRuntimeProbe ImportRuntimeProbe.cpp) +target_compile_features(FreeCADWebImportRuntimeProbe PRIVATE cxx_std_17) +target_compile_options(FreeCADWebImportRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") +target_include_directories(FreeCADWebImportRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" +) +target_link_libraries(FreeCADWebImportRuntimeProbe PRIVATE + Import + ${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(FreeCADWebImportRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebImportRuntimeProbe" + "-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}/../web/public/samples/freecad-sample.step@/fixtures/freecad-sample.step" +) +set_target_properties(FreeCADWebImportRuntimeProbe PROPERTIES + SUFFIX ".js" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(BUILD_INSPECTION) + add_executable(FreeCADWebInspectionRuntimeProbe InspectionRuntimeProbe.cpp) + target_compile_features(FreeCADWebInspectionRuntimeProbe PRIVATE cxx_std_17) + target_compile_options(FreeCADWebInspectionRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions") + target_include_directories(FreeCADWebInspectionRuntimeProbe PRIVATE + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}/src" + "${CMAKE_SOURCE_DIR}/src" + ) + target_link_libraries(FreeCADWebInspectionRuntimeProbe PRIVATE + Inspection + ${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(FreeCADWebInspectionRuntimeProbe PRIVATE + "-O2" + "-g0" + "--no-entry" + "-sWASM=1" + "-sMODULARIZE=1" + "-sEXPORT_ES6=1" + "-sEXPORT_NAME=FreeCADWebInspectionRuntimeProbe" + "-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(FreeCADWebInspectionRuntimeProbe 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/ImportRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp new file mode 100644 index 00000000..baa623a9 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/ImportRuntimeProbe.cpp @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +extern "C" PyObject* PyInit_Import(); +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Part(); + +namespace { + +std::string lastErrorCode; +std::string lastErrorMessage; +std::string lastTypeName; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool importInitialized = false; +std::vector documentHandles; +unsigned int lastObjectCount = 0; +unsigned int lastPartCount = 0; +unsigned int lastSolidCount = 0; +double lastVolume = 0.0; +std::string lastObjectType; +std::string persistedPath; +unsigned int persistedSize = 0; +unsigned int reopenedObjectCount = 0; +unsigned int reopenedPartCount = 0; +unsigned int reopenedSolidCount = 0; +double reopenedVolume = 0.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; +} + +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 ensureImport() +{ + ++initializationCalls; + if (importInitialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + addBuiltin("Materials", &PyInit_Materials); + addBuiltin("Part", &PyInit_Part); + addBuiltin("Import", &PyInit_Import); + importInitialized = 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 Import 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) != "Import") { + setError( + std::string(moduleName) == "ImportGui" ? "gui_dependency_unavailable" : "module_unavailable", + "module is not available in the GUI-free Web kernel: " + std::string(moduleName)); + return false; + } + return ensureImport(); +} + +void closeDocument(App::Document*& document) +{ + if (document) { + App::GetApplication().closeDocument(document); + document = nullptr; + } +} + +int importStep(const char* fileName) +{ + std::string stage = "initializing Import"; + if (!ensureImport()) { + return 0; + } + if (fileName == nullptr || *fileName == '\0') { + setError("invalid_argument", "STEP path is empty"); + return 0; + } + Base::FileInfo file(fileName); + if (!file.exists()) { + setError("file_not_found", "STEP fixture does not exist: " + std::string(fileName)); + return 0; + } + + App::Document* document = nullptr; + Handle(TDocStd_Document) ocafDocument; + try { + stage = "creating OCAF document"; + Handle(XCAFApp_Application) ocafApplication = XCAFApp_Application::GetApplication(); + ocafApplication->NewDocument(TCollection_ExtendedString("MDTV-CAF"), ocafDocument); + stage = "reading STEP fixture"; + Import::ReaderStep reader(file); + reader.read(ocafDocument); + + stage = "creating FreeCAD document"; + document = App::GetApplication().newDocument("ImportKernelDocument", "Import Kernel Document", {false, true}); + if (!document) { + throw Base::RuntimeError("Application::newDocument returned null"); + } + stage = "loading OCAF shapes"; + Import::ImportOCAFExt importer(ocafDocument, document, file.fileNamePure()); + importer.setImportOptions(Import::ImportOCAFExt::customImportOptions()); + App::DocumentObject* root = importer.loadShapes(); + bool hasError = false; + stage = "recomputing imported document"; + document->recompute({}, false, &hasError); + if (hasError) { + throw Base::RuntimeError("imported document recompute reported an error"); + } + + stage = "summarizing imported geometry"; + lastObjectCount = document->countObjects(); + lastPartCount = 0; + lastSolidCount = 0; + lastVolume = 0.0; + lastObjectType = root ? root->getTypeId().getName() : ""; + for (App::DocumentObject* object : document->getObjects()) { + auto* part = dynamic_cast(object); + if (!part || part->Shape.getValue().IsNull()) { + continue; + } + ++lastPartCount; + const TopoDS_Shape& shape = part->Shape.getValue(); + GProp_GProps properties; + BRepGProp::VolumeProperties(shape, properties); + lastVolume += properties.Mass(); + for (TopExp_Explorer explorer(shape, TopAbs_SOLID); explorer.More(); explorer.Next()) { + ++lastSolidCount; + } + if (lastObjectType.empty()) { + lastObjectType = object->getTypeId().getName(); + } + } + ocafApplication->Close(ocafDocument); + ocafDocument.Nullify(); + if (lastObjectCount == 0 || lastPartCount == 0 || lastSolidCount == 0 || lastVolume <= 0.0) { + throw Base::RuntimeError("imported STEP summary is empty"); + } + documentHandles.push_back(document); + clearError(); + return static_cast(documentHandles.size()); + } + catch (const Base::Exception& error) { + if (!ocafDocument.IsNull()) { + XCAFApp_Application::GetApplication()->Close(ocafDocument); + } + closeDocument(document); + setError("import_failed", error.what()); + } + catch (const std::exception& error) { + if (!ocafDocument.IsNull()) { + XCAFApp_Application::GetApplication()->Close(ocafDocument); + } + closeDocument(document); + setError("import_failed", error.what()); + } + catch (const Standard_Failure& error) { + if (!ocafDocument.IsNull()) { + XCAFApp_Application::GetApplication()->Close(ocafDocument); + } + closeDocument(document); + setError("import_failed", error.GetMessageString()); + } + catch (...) { + if (!ocafDocument.IsNull()) { + XCAFApp_Application::GetApplication()->Close(ocafDocument); + } + closeDocument(document); + setError("import_failed", "unknown STEP import failure while " + stage); + } + return 0; +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_import_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_type_probe(const char* typeName) +{ + if (!requireModule("Import")) { + return 0; + } + if (typeName == nullptr || *typeName == '\0') { + setError("invalid_argument", "type name is empty"); + return 0; + } + PyObject* module = PyImport_GetModuleDict() + ? PyDict_GetItemString(PyImport_GetModuleDict(), "Import") + : nullptr; + PyObject* type = module ? PyObject_GetAttrString(module, typeName) : nullptr; + if (!type || !PyType_Check(type)) { + Py_XDECREF(type); + PyErr_Clear(); + lastTypeName.clear(); + setError("type_not_found", "type is not registered in Import: " + std::string(typeName)); + return 0; + } + lastTypeName = std::string("Import.") + typeName; + Py_DECREF(type); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_import_error_code() +{ + return lastErrorCode.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_import_error_message() +{ + return lastErrorMessage.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_import_last_type() +{ + return lastTypeName.c_str(); +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_initialization_calls() +{ + return initializationCalls; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_initialization_runs() +{ + return initializationRuns; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_initialized() +{ + return importInitialized ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_step(const char* fileName) +{ + return importStep(fileName); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_release(int handle) +{ + if (handle <= 0 || static_cast(handle) > documentHandles.size() + || documentHandles[handle - 1] == nullptr) { + setError("invalid_handle", "document handle is unknown or already released"); + return 0; + } + closeDocument(documentHandles[handle - 1]); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_active_handles() +{ + int active = 0; + for (const auto* document : documentHandles) { + active += document ? 1 : 0; + } + return active; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_object_count() +{ + return lastObjectCount; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_part_count() +{ + return lastPartCount; +} + +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_solid_count() +{ + return lastSolidCount; +} + +EMSCRIPTEN_KEEPALIVE double fc_web_import_volume() +{ + return lastVolume; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_import_object_type() +{ + return lastObjectType.c_str(); +} + +EMSCRIPTEN_KEEPALIVE int fc_web_import_save_reopen() +{ + if (documentHandles.empty() || documentHandles.back() == nullptr) { + setError("invalid_handle", "no imported document is available for save/reopen"); + return 0; + } + auto* sourceDocument = documentHandles.back(); + const char* stage = "saving source document"; + try { + persistedPath = "/tmp/ImportCreateDocument.FCStd"; + for (App::DocumentObject* object : sourceDocument->getObjects()) { + if (auto* part = dynamic_cast(object)) { + part->ShapeMaterial.setStatus(App::Property::Transient, true); + } + } + if (!sourceDocument->saveAs(persistedPath.c_str())) { + setError("save_failed", "Import document saveAs returned false"); + return 0; + } + Base::FileInfo file(persistedPath); + persistedSize = file.size(); + stage = "closing source document"; + closeDocument(sourceDocument); + documentHandles.back() = nullptr; + stage = "creating reopened document"; + auto* reopened = App::GetApplication().newDocument("ReopenedImportDocument", "Reopened Import Document", {false, true}); + if (!reopened) { + setError("reopen_failed", "Application::newDocument returned null"); + return 0; + } + reopened->FileName.setValue(persistedPath); + stage = "initializing material registry"; + Materials::MaterialManager::getManager(); + stage = "restoring document XML"; + reopened->restore(persistedPath.c_str(), true, {}); + bool hasError = false; + stage = "recomputing reopened document"; + reopened->recompute({}, false, &hasError); + stage = "summarizing reopened document"; + reopenedObjectCount = reopened->countObjects(); + reopenedPartCount = 0; + reopenedSolidCount = 0; + reopenedVolume = 0.0; + for (App::DocumentObject* object : reopened->getObjects()) { + auto* part = dynamic_cast(object); + if (!part || part->Shape.getValue().IsNull()) continue; + ++reopenedPartCount; + const TopoDS_Shape& shape = part->Shape.getValue(); + GProp_GProps properties; + BRepGProp::VolumeProperties(shape, properties); + reopenedVolume += properties.Mass(); + for (TopExp_Explorer explorer(shape, TopAbs_SOLID); explorer.More(); explorer.Next()) ++reopenedSolidCount; + } + const bool valid = !hasError && persistedSize > 22 && reopenedObjectCount == lastObjectCount && reopenedPartCount == lastPartCount && reopenedSolidCount == lastSolidCount && std::abs(reopenedVolume - lastVolume) < 1e-6; + closeDocument(reopened); + if (!valid) { + setError("reopen_failed", "reopened Import document summary mismatch"); + return 0; + } + clearError(); + return 1; + } + catch (const Base::Exception& error) { + setError("save_reopen_failed", std::string(stage) + ": " + error.what()); + } + catch (const std::exception& error) { + setError("save_reopen_failed", std::string(stage) + ": " + error.what()); + } + return 0; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_import_persisted_path() { return persistedPath.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_persisted_size() { return persistedSize; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_reopened_object_count() { return reopenedObjectCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_reopened_part_count() { return reopenedPartCount; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_import_reopened_solid_count() { return reopenedSolidCount; } +EMSCRIPTEN_KEEPALIVE double fc_web_import_reopened_volume() { return reopenedVolume; } + +} // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp new file mode 100644 index 00000000..5f7aee90 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/InspectionRuntimeProbe.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +extern "C" PyObject* PyInit_Inspection(); +extern "C" PyObject* PyInit_Materials(); +extern "C" PyObject* PyInit_Mesh(); +extern "C" PyObject* PyInit_Part(); +extern "C" PyObject* PyInit_Points(); + +namespace { + +std::string lastErrorCode; +std::string lastErrorMessage; +std::string lastTypeName; +unsigned int initializationCalls = 0; +unsigned int initializationRuns = 0; +bool inspectionInitialized = false; +unsigned int lastDistanceCount = 0; +double lastDistanceRms = 0.0; +unsigned int lastDocumentObjects = 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; +} + +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 ensureInspection() +{ + ++initializationCalls; + if (inspectionInitialized) { + clearError(); + return true; + } + + ++initializationRuns; + try { + App::Application::initWebCore(); + addBuiltin("Materials", &PyInit_Materials); + addBuiltin("Part", &PyInit_Part); + addBuiltin("Mesh", &PyInit_Mesh); + addBuiltin("Points", &PyInit_Points); + addBuiltin("Inspection", &PyInit_Inspection); + inspectionInitialized = true; + clearError(); + return true; + } + catch (const Base::Exception& error) { + setError("initialization_failed", error.what()); + } + catch (const std::exception& error) { + setError("initialization_failed", error.what()); + } + 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) != "Inspection") { + setError( + std::string(moduleName) == "InspectionGui" ? "gui_dependency_unavailable" : "module_unavailable", + "module is not available in the GUI-free Web kernel: " + std::string(moduleName)); + return false; + } + return ensureInspection(); +} + +} // namespace + +extern "C" { + +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_initialize(const char* moduleName) +{ + return requireModule(moduleName) ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_type_probe(const char* typeName) +{ + if (!requireModule("Inspection")) { + return 0; + } + if (typeName == nullptr || *typeName == '\0') { + setError("invalid_argument", "type name is empty"); + return 0; + } + Base::Type type = Base::Type::fromName(typeName); + if (type.isBad()) { + lastTypeName.clear(); + setError("type_not_found", "type is not registered: " + std::string(typeName)); + return 0; + } + lastTypeName = type.getName(); + clearError(); + return 1; +} + +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_error_code() { return lastErrorCode.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_error_message() { return lastErrorMessage.c_str(); } +EMSCRIPTEN_KEEPALIVE const char* fc_web_inspection_last_type() { return lastTypeName.c_str(); } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_initialization_calls() { return initializationCalls; } +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_initialization_runs() { return initializationRuns; } +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_initialized() { return inspectionInitialized ? 1 : 0; } + +EMSCRIPTEN_KEEPALIVE int fc_web_inspection_create_recompute() +{ + if (!ensureInspection()) return 0; + App::Document* document = nullptr; + try { + document = App::GetApplication().newDocument("InspectionKernelDocument", "Inspection Kernel Document", {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 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; + document->recompute({}, false, &hasError); + lastDistanceCount = static_cast(inspection->Distances.getValues().size()); + lastDistanceRms = 0.0; + for (float distance : inspection->Distances.getValues()) { + lastDistanceRms += static_cast(distance) * static_cast(distance); + } + if (lastDistanceCount > 0) lastDistanceRms = std::sqrt(lastDistanceRms / lastDistanceCount); + lastDocumentObjects = document->countObjects(); + const bool valid = !hasError && lastDistanceCount > 0 && lastDocumentObjects == 3; + App::GetApplication().closeDocument(document); + document = nullptr; + if (!valid) { setError("recompute_failed", "Inspection fixture recompute produced no distances"); return 0; } + clearError(); + return 1; + } + catch (const Base::Exception& error) { + if (document) App::GetApplication().closeDocument(document); + setError("recompute_failed", error.what()); + } + return 0; +} +EMSCRIPTEN_KEEPALIVE unsigned int fc_web_inspection_distance_count() { return lastDistanceCount; } +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_invalid_recompute() +{ + if (!ensureInspection()) return 0; + App::Document* document = App::GetApplication().newDocument("InspectionInvalidDocument", "Inspection Invalid Document", {false, true}); + try { + if (!document || !document->addObject("Inspection::Feature", "Inspection")) throw Base::RuntimeError("Inspection invalid fixture creation failed"); + bool hasError = false; + document->recompute({}, false, &hasError); + App::GetApplication().closeDocument(document); + if (!hasError) { setError("invalid_fixture", "Inspection missing actual geometry was accepted"); return 0; } + setError("invalid_fixture", "No actual geometry to inspect specified"); + return 1; + } + catch (const Base::Exception& error) { + if (document) App::GetApplication().closeDocument(document); + setError("invalid_fixture", error.what()); + return 1; + } +} + +} // extern "C" diff --git a/src/WebAssembly/FreeCADCoreProbe/import-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/import-runtime-smoke.js new file mode 100644 index 00000000..b3965c42 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/import-runtime-smoke.js @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const crypto = require("crypto"); +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, "FreeCADWebImportRuntimeProbe.js")).href; + const module = await import(moduleUrl); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.wasm")), + }); + const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_import_error_code", "string"), + message: call("fc_web_import_error_message", "string"), + }); + const state = () => ({ + calls: call("fc_web_import_initialization_calls"), + runs: call("fc_web_import_initialization_runs"), + initialized: call("fc_web_import_initialized"), + }); + const result = {}; + + result.initialize = call("fc_web_import_initialize", "number", ["string"], ["Import"]); + result.stateAfterInitialize = state(); + result.initializeAgain = call("fc_web_import_initialize", "number", ["string"], ["Import"]); + result.stateAfterRepeat = state(); + result.type = call("fc_web_import_type_probe", "number", ["string"], ["StepShape"]); + result.typeName = call("fc_web_import_last_type", "string"); + result.unknownType = call("fc_web_import_type_probe", "number", ["string"], ["MissingType"]); + result.unknownTypeError = error(); + result.unknownModule = call("fc_web_import_initialize", "number", ["string"], ["Missing"]); + result.unknownModuleError = error(); + result.guiModule = call("fc_web_import_initialize", "number", ["string"], ["ImportGui"]); + result.guiModuleError = error(); + result.emptyModule = call("fc_web_import_initialize", "number", ["string"], [""]); + result.emptyModuleError = error(); + result.stateAfterErrors = state(); + + const fixture = "/fixtures/freecad-sample.step"; + const summary = () => ({ + objects: call("fc_web_import_object_count"), + parts: call("fc_web_import_part_count"), + solids: call("fc_web_import_solid_count"), + volume: call("fc_web_import_volume", "number"), + objectType: call("fc_web_import_object_type", "string"), + }); + result.importHandle = call("fc_web_import_step", "number", ["string"], [fixture]); + result.firstImportError = error(); + result.firstImport = summary(); + result.activeAfterImport = call("fc_web_import_active_handles"); + result.release = call("fc_web_import_release", "number", ["number"], [result.importHandle]); + result.activeAfterRelease = call("fc_web_import_active_handles"); + result.repeatHandle = call("fc_web_import_step", "number", ["string"], [fixture]); + result.repeatImport = summary(); + result.repeatRelease = call("fc_web_import_release", "number", ["number"], [result.repeatHandle]); + result.activeAfterRepeatRelease = call("fc_web_import_active_handles"); + result.validSnapshot = { + objects: result.repeatImport.objects, + parts: result.repeatImport.parts, + solids: result.repeatImport.solids, + volume: result.repeatImport.volume, + objectType: result.repeatImport.objectType, + }; + result.validSnapshotHash = crypto.createHash("sha256") + .update(JSON.stringify(result.validSnapshot)) + .digest("hex"); + result.missingFile = call("fc_web_import_step", "number", ["string"], ["/fixtures/missing.step"]); + result.missingFileError = error(); + result.afterMissingSnapshot = summary(); + result.missingFileRepeat = call("fc_web_import_step", "number", ["string"], ["/fixtures/missing.step"]); + result.missingFileRepeatError = error(); + result.afterMissingRepeatSnapshot = summary(); + result.emptyPath = call("fc_web_import_step", "number", ["string"], [""]); + result.emptyPathError = error(); + + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && result.stateAfterInitialize.runs === 1 + && result.stateAfterInitialize.initialized === 1 + && result.stateAfterRepeat.runs === 1 + && result.type === 1 + && result.typeName === "Import.StepShape" + && 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 + && result.importHandle === 1 + && result.firstImportError.code === "" + && result.firstImportError.message === "" + && result.firstImport.objects > 0 + && result.firstImport.parts > 0 + && result.firstImport.solids > 0 + && result.firstImport.volume > 0 + && result.firstImport.objectType.length > 0 + && result.activeAfterImport === 1 + && result.release === 1 + && result.activeAfterRelease === 0 + && result.repeatHandle === 2 + && JSON.stringify(result.repeatImport) === JSON.stringify(result.firstImport) + && result.repeatRelease === 1 + && result.activeAfterRepeatRelease === 0 + && result.validSnapshotHash.length === 64 + && result.missingFile === 0 + && result.missingFileError.code === "file_not_found"; + + const recoveryValid = JSON.stringify(result.afterMissingSnapshot) === JSON.stringify(result.validSnapshot) + && JSON.stringify(result.afterMissingRepeatSnapshot) === JSON.stringify(result.validSnapshot) + && result.missingFileRepeat === 0 + && result.missingFileRepeatError.code === "file_not_found" + && result.emptyPath === 0 + && result.emptyPathError.code === "invalid_argument"; + if (!recoveryValid) { + throw new Error(JSON.stringify(result)); + } + 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/import-save-reopen-facade-smoke.js b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-facade-smoke.js new file mode 100644 index 00000000..76383a7d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-facade-smoke.js @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +globalThis.__dirname = `${buildDirectory}${path.sep}`; +const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.js"))); +const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.wasm")), +}); +const call = (name, returnType = "number", argTypes = [], args = []) => + probe.ccall(name, returnType, argTypes, args); +const kernel = { + ccall: (name, returnType, argTypes, args) => call(name, returnType, argTypes, args), +}; + +const facade = await import(pathToFileURL(path.resolve("FreeCAD/web/dist/assets/import-CK7bBA-v.js"))); +const envelope = await import(pathToFileURL(path.resolve("FreeCAD/web/dist/assets/facade-envelope-B1V2jxkP.js"))); +const queue = new envelope.FacadeSerialQueue(); +const request = facade.createImportSaveReopenRequest("import-save-1"); +const response = await facade.dispatchImportSaveReopen(queue, request, kernel); +const duplicate = await facade.dispatchImportSaveReopen(queue, request, kernel); +const invalid = await facade.dispatchImportSaveReopen(queue, facade.createImportSaveReopenRequest("import-save-invalid", { + ...facade.IMPORT_SAVE_REOPEN_FIXTURE, + fixtureSha256: "wrong", +}), kernel); + +const valid = response.kind === "result" + && response.requestId === "import-save-1" + && response.result.reopened.objects === response.result.before.objects + && response.result.reopened.parts === response.result.before.parts + && response.result.reopened.solids === response.result.before.solids + && response.result.persisted.size > 22 + && duplicate.kind === "error" + && duplicate.error.code === "duplicate_request" + && invalid.kind === "error" + && invalid.error.code === "invalid_fixture"; +const result = { response, duplicate, invalid }; +if (!valid) throw new Error(JSON.stringify(result)); +console.log(JSON.stringify(result)); diff --git a/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-recovery-smoke.js b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-recovery-smoke.js new file mode 100644 index 00000000..c358642e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-recovery-smoke.js @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +globalThis.__dirname = `${buildDirectory}${path.sep}`; +const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.js"))); +const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.wasm")), +}); +const call = (name, returnType = "number", argTypes = [], args = []) => + probe.ccall(name, returnType, argTypes, args); +const error = () => ({ + code: call("fc_web_import_error_code", "string"), + message: call("fc_web_import_error_message", "string"), +}); +const snapshot = () => ({ + objects: call("fc_web_import_reopened_object_count"), + parts: call("fc_web_import_reopened_part_count"), + solids: call("fc_web_import_reopened_solid_count"), + volume: call("fc_web_import_reopened_volume", "number"), +}); + +const importHandle = call( + "fc_web_import_step", + "number", + ["string"], + ["/fixtures/freecad-sample.step"], +); +const saveReopen = call("fc_web_import_save_reopen"); +const recovered = snapshot(); +const invalid = call("fc_web_import_save_reopen"); +const invalidError = error(); +const afterInvalid = snapshot(); +const invalidRepeat = call("fc_web_import_save_reopen"); +const invalidRepeatError = error(); +const afterInvalidRepeat = snapshot(); + +const result = { + importHandle, + saveReopen, + recovered, + invalid, + invalidError, + afterInvalid, + invalidRepeat, + invalidRepeatError, + afterInvalidRepeat, +}; +const stable = JSON.stringify(recovered) === JSON.stringify(afterInvalid) + && JSON.stringify(recovered) === JSON.stringify(afterInvalidRepeat); +const valid = importHandle > 0 + && saveReopen === 1 + && invalid === 0 + && invalidRepeat === 0 + && invalidError.code === "invalid_handle" + && invalidRepeatError.code === "invalid_handle" + && invalidError.message === invalidRepeatError.message + && stable; + +if (!valid) { + throw new Error(JSON.stringify(result)); +} +console.log(JSON.stringify(result)); diff --git a/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-smoke.js b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-smoke.js new file mode 100644 index 00000000..5d6063f8 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/import-save-reopen-smoke.js @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); +globalThis.__dirname = `${buildDirectory}${path.sep}`; +const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.js"))); +const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebImportRuntimeProbe.wasm")), +}); +const call = (name, returnType = "number", argTypes = [], args = []) => + probe.ccall(name, returnType, argTypes, args); +const error = () => ({ + code: call("fc_web_import_error_code", "string"), + message: call("fc_web_import_error_message", "string"), +}); + +const importHandle = call( + "fc_web_import_step", + "number", + ["string"], + ["/fixtures/freecad-sample.step"], +); +const before = { + objects: call("fc_web_import_object_count"), + parts: call("fc_web_import_part_count"), + solids: call("fc_web_import_solid_count"), + volume: call("fc_web_import_volume", "number"), +}; +const saveReopen = call("fc_web_import_save_reopen"); +const result = { + importHandle, + saveReopen, + error: error(), + persistedPath: call("fc_web_import_persisted_path", "string"), + persistedSize: call("fc_web_import_persisted_size"), + before, + reopened: { + objects: call("fc_web_import_reopened_object_count"), + parts: call("fc_web_import_reopened_part_count"), + solids: call("fc_web_import_reopened_solid_count"), + volume: call("fc_web_import_reopened_volume", "number"), + }, + activeHandles: call("fc_web_import_active_handles"), +}; + +const valid = result.importHandle > 0 + && result.saveReopen === 1 + && result.error.code === "" + && result.error.message === "" + && result.persistedPath === "/tmp/ImportCreateDocument.FCStd" + && result.persistedSize > 22 + && result.before.objects > 0 + && result.before.parts > 0 + && result.before.solids > 0 + && result.before.volume > 0 + && result.reopened.objects === result.before.objects + && result.reopened.parts === result.before.parts + && result.reopened.solids === result.before.solids + && Math.abs(result.reopened.volume - result.before.volume) < 1e-6 + && result.activeHandles === 0; + +if (!valid) { + throw new Error(JSON.stringify(result)); +} +console.log(JSON.stringify(result)); diff --git a/src/WebAssembly/FreeCADCoreProbe/inspection-runtime-smoke.js b/src/WebAssembly/FreeCADCoreProbe/inspection-runtime-smoke.js new file mode 100644 index 00000000..5147542f --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/inspection-runtime-smoke.js @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +async function main() +{ + const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin"); + globalThis.__dirname = `${buildDirectory}${path.sep}`; + globalThis.require = require; + const module = await import(pathToFileURL(path.join(buildDirectory, "FreeCADWebInspectionRuntimeProbe.js"))); + const probe = await module.default({ + wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebInspectionRuntimeProbe.wasm")), +}); + const call = (name, returnType = "number", argTypes = [], args = []) => + probe.ccall(name, returnType, argTypes, args); + const error = () => ({ + code: call("fc_web_inspection_error_code", "string"), + message: call("fc_web_inspection_error_message", "string"), +}); + const state = () => ({ + calls: call("fc_web_inspection_initialization_calls"), + runs: call("fc_web_inspection_initialization_runs"), + initialized: call("fc_web_inspection_initialized"), +}); + + const result = {}; + result.initialize = call("fc_web_inspection_initialize", "number", ["string"], ["Inspection"]); + result.firstState = state(); + result.initializeAgain = call("fc_web_inspection_initialize", "number", ["string"], ["Inspection"]); + result.repeatState = state(); + result.types = [ + "Inspection::PropertyDistanceList", + "Inspection::Feature", + "Inspection::Group", + "Mesh::Feature", + "Points::Feature", + "Part::Feature", +].map((name) => ({ + name, + visible: call("fc_web_inspection_type_probe", "number", ["string"], [name]), + registeredName: call("fc_web_inspection_last_type", "string"), +})); + result.missingType = call("fc_web_inspection_type_probe", "number", ["string"], ["Inspection::Missing"]); + result.missingTypeError = error(); + result.missingModule = call("fc_web_inspection_initialize", "number", ["string"], ["Missing"]); + result.missingModuleError = error(); + result.guiModule = call("fc_web_inspection_initialize", "number", ["string"], ["InspectionGui"]); + result.guiModuleError = error(); + result.action = call("fc_web_inspection_create_recompute"); + result.actionError = error(); + result.actionSummary = { + distanceCount: call("fc_web_inspection_distance_count"), + rms: call("fc_web_inspection_distance_rms", "number"), + documentObjects: call("fc_web_inspection_document_objects"), + }; + result.invalidAction = call("fc_web_inspection_invalid_recompute"); + result.invalidError = error(); + result.recoveryAction = call("fc_web_inspection_create_recompute"); + result.recoveryError = error(); + + const valid = result.initialize === 1 + && result.initializeAgain === 1 + && result.firstState.runs === 1 + && result.repeatState.runs === 1 + && result.repeatState.initialized === 1 + && result.types.every((entry) => entry.visible === 1 && entry.registeredName === entry.name) + && result.missingType === 0 + && result.missingTypeError.code === "type_not_found" + && result.missingModule === 0 + && result.missingModuleError.code === "module_unavailable" + && result.guiModule === 0 + && result.guiModuleError.code === "gui_dependency_unavailable"; + const actionValid = result.action === 1 + && result.actionError.code === "" + && result.actionSummary.distanceCount > 0 + && result.actionSummary.documentObjects === 3 + && result.actionSummary.rms >= 0; + const recoveryValid = result.invalidAction === 1 + && result.invalidError.code === "invalid_fixture" + && result.recoveryAction === 1 + && result.recoveryError.code === ""; + if (!valid || !actionValid || !recoveryValid) throw new Error(JSON.stringify(result)); + console.log(JSON.stringify(result)); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +});