Add Inspection and Import WebAssembly probes
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled

This commit is contained in:
2026-08-21 10:57:23 +08:00
parent c87b2871be
commit a6f251f657
15 changed files with 1264 additions and 59 deletions

View File

@@ -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()

View File

@@ -1598,9 +1598,9 @@ std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader)
long lastId = 0;
for (int i = 0; i < Cnt; i++) {
reader.readElement("Object");
std::string type = reader.getAttribute<const char*>("type");
std::string name = reader.getAttribute<const char*>("name");
reader.readElement("Object");
std::string type = reader.getAttribute<const char*>("type");
std::string name = reader.getAttribute<const char*>("name");
std::string viewType =
reader.hasAttribute("ViewType") ? reader.getAttribute<const char*>("ViewType") : "";
@@ -2082,38 +2082,38 @@ bool Document::isAnyRestoring()
// Open the document
void Document::restore(const char* filename,
bool delaySignal,
const std::vector<std::string>& objNames)
bool delaySignal,
const std::vector<std::string>& 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();

View File

@@ -34,10 +34,12 @@
#include <TopoDS.hxx>
#include <gp_Pnt.hxx>
#include <QEventLoop>
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrentMap>
#include <QEventLoop>
#ifndef FREECAD_WEB_CORE
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrentMap>
#endif
#include <Base/Console.h>
#include <Base/FutureWatcherProgress.h>
@@ -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<unsigned long> 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() << "…";

View File

@@ -42,7 +42,9 @@
#include <boost/core/ignore_unused.hpp>
// Qt
#include <QEventLoop>
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrentMap>
#include <QEventLoop>
#ifndef FREECAD_WEB_CORE
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrentMap>
#endif

View File

@@ -26,9 +26,11 @@
#include <functional>
#include <limits>
#ifndef FREECAD_WEB_CORE
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrentMap>
#endif
#include <Base/Sequencer.h>
#include <Base/Tools.h>
@@ -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<CurvatureInfo> future
@@ -92,6 +99,7 @@ void MeshCurvature::ComputePerFace(bool parallel)
myCurvature.push_back(it);
}
}
#endif
}
#ifdef OPTIMIZE_CURVATURE

View File

@@ -22,7 +22,9 @@
* *
***************************************************************************/
#include <QtConcurrentMap>
#ifndef FREECAD_WEB_CORE
#include <QtConcurrentMap>
#endif
#include <boost/math/special_functions/fpclassify.hpp>
#include <cmath>
#include <iostream>
@@ -36,9 +38,9 @@
#include "PointsAlgos.h"
#ifdef _MSC_VER
# include <ppl.h>
#endif
#ifdef _MSC_VER
# include <ppl.h>
#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<value_type>& 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<float>(vec);
std::vector<value_type>& 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;
});

View File

@@ -22,7 +22,9 @@
* *
***************************************************************************/
#include <QtConcurrentMap>
#ifndef FREECAD_WEB_CORE
#include <QtConcurrentMap>
#endif
#include <algorithm>
#include <cmath>
#include <iostream>
@@ -38,9 +40,9 @@
#include "Points.h"
#include "Properties.h"
#ifdef _MSC_VER
# include <ppl.h>
#endif
#ifdef _MSC_VER
# include <ppl.h>
#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);
});

View File

@@ -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")

View File

@@ -0,0 +1,452 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <App/Application.h>
#include <App/Document.h>
#include <Base/Exception.h>
#include <Base/FileInfo.h>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <Mod/Import/App/ImportOCAF2.h>
#include <Mod/Import/App/ReaderStep.h>
#include <Mod/Material/App/MaterialManager.h>
#include <Mod/Part/App/PartFeature.h>
#include <Standard_Failure.hxx>
#include <TDocStd_Document.hxx>
#include <TopExp_Explorer.hxx>
#include <XCAFApp_Application.hxx>
#include <Python.h>
#include <emscripten/emscripten.h>
#include <string>
#include <cmath>
#include <vector>
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<App::Document*> 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<Part::Feature*>(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<int>(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<std::size_t>(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<Part::Feature*>(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<Part::Feature*>(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"

View File

@@ -0,0 +1,231 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <App/Application.h>
#include <App/Document.h>
#include <Base/Exception.h>
#include <Base/Type.h>
#include <Mod/Inspection/App/InspectionFeature.h>
#include <Mod/Part/App/FeaturePartBox.h>
#include <Python.h>
#include <emscripten/emscripten.h>
#include <string>
#include <cmath>
#include <vector>
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<Part::Box*>(actual)->Length.setValue(2.0);
static_cast<Part::Box*>(actual)->Width.setValue(3.0);
static_cast<Part::Box*>(actual)->Height.setValue(4.0);
static_cast<Part::Box*>(nominal)->Length.setValue(2.0);
static_cast<Part::Box*>(nominal)->Width.setValue(3.0);
static_cast<Part::Box*>(nominal)->Height.setValue(4.0);
auto* inspection = static_cast<Inspection::Feature*>(feature);
inspection->Actual.setValue(actual);
inspection->Nominals.setValues(std::vector<App::DocumentObject*>{nominal});
inspection->SearchRadius.setValue(10.0);
bool hasError = false;
document->recompute({}, false, &hasError);
lastDistanceCount = static_cast<unsigned int>(inspection->Distances.getValues().size());
lastDistanceRms = 0.0;
for (float distance : inspection->Distances.getValues()) {
lastDistanceRms += static_cast<double>(distance) * static_cast<double>(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"

View File

@@ -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;
});

View File

@@ -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));

View File

@@ -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));

View File

@@ -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));

View File

@@ -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;
});