Add FreeCAD WebAssembly runtime probes
Some checks failed
FreeCAD master CI / Prepare (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
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
Stale Issues / stale (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled

This commit is contained in:
2026-08-23 17:48:10 +08:00
parent 0644133717
commit 0a2e7ce7f5
32 changed files with 2160 additions and 74 deletions

View File

@@ -60,6 +60,7 @@ option(FREECAD_WEB_ENABLE_CLOUD "Enable the Cloud App Web probe and its dependen
option(FREECAD_WEB_ENABLE_INSPECTION "Enable the Inspection App Web probe and its dependency closure" OFF)
option(FREECAD_WEB_ENABLE_MESHPART "Enable the MeshPart App Web probe and its dependency closure" OFF)
option(FREECAD_WEB_ENABLE_OPENSCAD "Enable the OpenSCAD App Web probe and its dependency closure" OFF)
option(FREECAD_WEB_ENABLE_SANDBOX "Enable the Sandbox App Web probe and its dependency closure" OFF)
if(FREECAD_WEB)
if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
message(FATAL_ERROR
@@ -133,6 +134,11 @@ if(FREECAD_WEB)
list(REMOVE_ITEM _freecad_web_disabled_options
BUILD_OPENSCAD BUILD_DRAFT BUILD_TECHDRAW BUILD_SPREADSHEET)
endif()
if(FREECAD_WEB_ENABLE_SANDBOX)
# Sandbox App is GUI-free only when its QtCore/PyCXX document protector
# is linked without SandboxGui. Mesh is its direct App dependency.
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SANDBOX BUILD_MESH)
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()
@@ -169,6 +175,11 @@ if(FREECAD_WEB)
set(${_freecad_web_openscad_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_OPENSCAD" FORCE)
endforeach()
endif()
if(FREECAD_WEB_ENABLE_SANDBOX)
foreach(_freecad_web_sandbox_option BUILD_SANDBOX BUILD_MESH)
set(${_freecad_web_sandbox_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SANDBOX" FORCE)
endforeach()
endif()
include(cMake/FreeCADWebDependencies.cmake)
endif()

View File

@@ -176,7 +176,10 @@ bool TopoShapeCache::isTouched(const TopoDS_Shape& tds) const
TopoShapeCache::Ancestry& TopoShapeCache::getAncestry(TopAbs_ShapeEnum type)
{
auto& ancestry = shapeAncestryCache.at(type);
if (type < TopAbs_COMPOUND || type > TopAbs_SHAPE) {
throw Base::ValueError("invalid TopoShape ancestry type: " + std::to_string(static_cast<int>(type)));
}
auto& ancestry = shapeAncestryCache[static_cast<std::size_t>(type)];
if (!ancestry.owner) {
ancestry.owner = this;
if (!shape.IsNull()) {
@@ -231,7 +234,10 @@ TopoDS_Shape TopoShapeCache::findAncestor(
auto& info = getAncestry(type);
auto& ancestorInfo = info.ancestors.at(subShape.ShapeType());
if (subShape.ShapeType() < TopAbs_COMPOUND || subShape.ShapeType() > TopAbs_SHAPE) {
return nullShape;
}
auto& ancestorInfo = info.ancestors[static_cast<std::size_t>(subShape.ShapeType())];
if (!ancestorInfo.initialized) {
ancestorInfo.initialized = true;
// ancestorInfo.shapes is the output variable here, storing (and caching) the actual map

View File

@@ -1488,7 +1488,7 @@ TopoShape& TopoShape::makeShapeWithElementMap(
int newShapeCounter = 0;
for (auto& newShape : mapper.modified(otherElement)) {
++newShapeCounter;
if (newShape.ShapeType() >= TopAbs_SHAPE) {
if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE) {
// NOLINTNEXTLINE
FC_ERR(
"unknown modified shape type " << newShape.ShapeType() << " from "
@@ -1496,7 +1496,11 @@ TopoShape& TopoShape::makeShapeWithElementMap(
);
continue;
}
auto& newInfo = *infoMap.at(newShape.ShapeType());
if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE
|| !infoMap[newShape.ShapeType()]) {
continue;
}
auto& newInfo = *infoMap[newShape.ShapeType()];
if (newInfo.type != newShape.ShapeType()) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
// TODO: it seems modified shape may report higher
@@ -1545,7 +1549,7 @@ TopoShape& TopoShape::makeShapeWithElementMap(
// (e.g. a face generated from an edge)
newShapeCounter = 0;
for (auto& newShape : mapper.generated(otherElement)) {
if (newShape.ShapeType() >= TopAbs_SHAPE) {
if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE) {
// NOLINTNEXTLINE
FC_ERR(
"unknown generated shape type " << newShape.ShapeType() << " from "
@@ -1556,7 +1560,11 @@ TopoShape& TopoShape::makeShapeWithElementMap(
int parallelFace = -1;
int coplanarFace = -1;
auto& newInfo = *infoMap.at(newShape.ShapeType());
if (newShape.ShapeType() < TopAbs_COMPOUND || newShape.ShapeType() >= TopAbs_SHAPE
|| !infoMap[newShape.ShapeType()]) {
continue;
}
auto& newInfo = *infoMap[newShape.ShapeType()];
std::vector<TopoDS_Shape> newShapes;
int shapeOffset = 0;
if (newInfo.type == newShape.ShapeType()) {

View File

@@ -34,6 +34,7 @@ SET(Sandbox_SRCS
DocumentProtectorPy.h
DocumentThread.cpp
DocumentThread.h
SandboxGlobal.h
PreCompiled.h
)

View File

@@ -23,14 +23,14 @@
***************************************************************************/
# include <QCoreApplication>
# include <QObject>
# include <QEvent>
# include <QMutex>
# include <QMutexLocker>
# include <QSemaphore>
# include <QThread>
# include <QWaitCondition>
# include <QCoreApplication>
# include <QObject>
# include <QEvent>
#ifndef FREECAD_WEB_CORE
# include <QSemaphore>
# include <QThread>
# include <QWaitCondition>
#endif
#include "DocumentProtector.h"
@@ -49,18 +49,25 @@ static const int QT_CUSTOM_EVENT_PROTECTOR = 10000;
class AbstractCustomProtectorEvent : public QEvent
{
public:
AbstractCustomProtectorEvent()
: QEvent(QEvent::Type(QT_CUSTOM_EVENT_PROTECTOR)), semaphore(0)
AbstractCustomProtectorEvent()
: QEvent(QEvent::Type(QT_CUSTOM_EVENT_PROTECTOR))
#ifndef FREECAD_WEB_CORE
, semaphore(0)
#endif
{
}
~AbstractCustomProtectorEvent()
{
if (semaphore)
semaphore->release();
~AbstractCustomProtectorEvent()
{
#ifndef FREECAD_WEB_CORE
if (semaphore)
semaphore->release();
#endif
}
virtual void execute() = 0;
QSemaphore* semaphore;
#ifndef FREECAD_WEB_CORE
QSemaphore* semaphore;
#endif
};
class CustomAddObjectEvent : public AbstractCustomProtectorEvent
@@ -208,9 +215,13 @@ DocumentReceiver *DocumentReceiver::globalInstance()
}
// Posts an event and waits for it to finish processing
void DocumentReceiver::postEventAndWait(QEvent* e)
{
QThread *currentThread = QThread::currentThread();
void DocumentReceiver::postEventAndWait(QEvent* e)
{
#ifdef FREECAD_WEB_CORE
QCoreApplication::sendEvent(this, e);
delete e;
#else
QThread *currentThread = QThread::currentThread();
QThread *thr = this->thread(); // this is the main thread
if (currentThread == thr) {
@@ -225,8 +236,9 @@ void DocumentReceiver::postEventAndWait(QEvent* e)
static_cast<AbstractCustomProtectorEvent*>(e)->semaphore = &semaphore;
QCoreApplication::postEvent(this, e);
// wait until the event has been processed
semaphore.acquire();
}
semaphore.acquire();
}
#endif
}
void DocumentReceiver::customEvent(QEvent* e)
@@ -247,12 +259,14 @@ DocumentProtector::~DocumentProtector()
{
}
void DocumentProtector::init()
{
// this method must be called somewhere from the main thread
DocumentReceiver::globalInstance()->
moveToThread(QCoreApplication::instance()->thread());
}
void DocumentProtector::init()
{
#ifndef FREECAD_WEB_CORE
// this method must be called somewhere from the main thread
DocumentReceiver::globalInstance()->
moveToThread(QCoreApplication::instance()->thread());
#endif
}
void DocumentProtector::slotDeletedDocument(const App::Document& Doc)
{

View File

@@ -23,10 +23,11 @@
***************************************************************************/
#pragma once
#include <string>
#include <App/DocumentObserver.h>
#pragma once
#include <string>
#include <App/DocumentObserver.h>
#include "SandboxGlobal.h"
namespace App {
class Document;
@@ -127,4 +128,4 @@ private:
App::DocumentObject* obj;
};
}
}

View File

@@ -23,25 +23,31 @@
***************************************************************************/
# include <QMutexLocker>
#ifndef FREECAD_WEB_CORE
# include <QMutexLocker>
#endif
#include "DocumentThread.h"
#include "DocumentProtector.h"
#include <Base/Console.h>
#include <Base/Sequencer.h>
#include <Base/Stream.h>
#include <Base/Writer.h>
#include <Base/Interpreter.h>
#include <App/Application.h>
#include <App/Document.h>
#include <Mod/Mesh/App/Mesh.h>
#include <Base/Console.h>
#ifndef FREECAD_WEB_CORE
#include <Base/Sequencer.h>
#include <Base/Stream.h>
#include <Base/Writer.h>
#include <Base/Interpreter.h>
#endif
#include <App/Application.h>
#include <App/Document.h>
#ifndef FREECAD_WEB_CORE
#include <Mod/Mesh/App/Mesh.h>
#endif
using namespace Sandbox;
DocumentThread::DocumentThread(QObject* parent)
#ifndef FREECAD_WEB_CORE
DocumentThread::DocumentThread(QObject* parent)
: QThread(parent)
{
}
@@ -153,11 +159,12 @@ Base::Reference<Mesh::MeshObject> MeshLoaderThread::getMesh() const
return this->mesh;
}
void MeshLoaderThread::run()
{
void MeshLoaderThread::run()
{
this->mesh = new Mesh::MeshObject();
this->mesh->load((const char*)filename.toUtf8());
}
}
#endif
// --------------------------------------
@@ -203,8 +210,8 @@ void SandboxObject::resetValue()
Base::Console().message("SandboxObject::resetValue()\n");
Integer.setValue(4711);
}
DocumentTestThread::DocumentTestThread(QObject* parent)
#ifndef FREECAD_WEB_CORE
DocumentTestThread::DocumentTestThread(QObject* parent)
: QThread(parent)
{
}
@@ -242,7 +249,7 @@ DocumentSaverThread::~DocumentSaverThread()
{
}
void DocumentSaverThread::run()
void DocumentSaverThread::run()
{
std::string uuid = Base::Uuid::createUuid();
std::string fn = doc->TransientDir.getValue();
@@ -268,4 +275,5 @@ void DocumentSaverThread::run()
// write additional files
writer.writeFiles();
}
}
}
#endif

View File

@@ -23,20 +23,24 @@
***************************************************************************/
#pragma once
#include <QThread>
#include <Base/Mutex.h>
#include <Base/Handle.h>
#include <App/DocumentObject.h>
#pragma once
#include <App/DocumentObject.h>
#include "SandboxGlobal.h"
#ifndef FREECAD_WEB_CORE
#include <QThread>
#include <Base/Handle.h>
#endif
namespace Mesh {
class MeshObject;
}
namespace Sandbox {
class SandboxAppExport DocumentThread : public QThread
namespace Sandbox {
#ifndef FREECAD_WEB_CORE
class SandboxAppExport DocumentThread : public QThread
{
public:
DocumentThread(QObject* parent=0);
@@ -67,7 +71,7 @@ protected:
static QRecursiveMutex mutex;
};
class SandboxAppExport MeshLoaderThread : public QThread
class SandboxAppExport MeshLoaderThread : public QThread
{
public:
MeshLoaderThread(const QString&, QObject* parent=0);
@@ -81,9 +85,10 @@ protected:
private:
QString filename;
Base::Reference<Mesh::MeshObject> mesh;
};
};
#endif
class SandboxObject : public App::DocumentObject
class SandboxObject : public App::DocumentObject
{
PROPERTY_HEADER(SandboxObject);
@@ -101,10 +106,11 @@ public:
}
void setIntValue(int);
void resetValue();
};
class SandboxAppExport DocumentTestThread : public QThread
void resetValue();
};
#ifndef FREECAD_WEB_CORE
class SandboxAppExport DocumentTestThread : public QThread
{
public:
DocumentTestThread(QObject* parent=0);
@@ -114,7 +120,7 @@ protected:
void run();
};
class SandboxAppExport DocumentSaverThread : public QThread
class SandboxAppExport DocumentSaverThread : public QThread
{
public:
DocumentSaverThread(App::Document* doc, QObject* parent=0);
@@ -125,6 +131,7 @@ protected:
private:
App::Document* doc;
};
};
#endif
}
}

View File

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <FCGlobal.h>
#ifndef SandboxAppExport
# ifdef Sandbox_EXPORTS
# define SandboxAppExport FREECAD_DECL_EXPORT
# else
# define SandboxAppExport FREECAD_DECL_IMPORT
# endif
#endif

View File

@@ -32,6 +32,7 @@ target_link_options(FreeCADWebPartBoxProbe PRIVATE
"-sDISABLE_EXCEPTION_CATCHING=0"
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
"--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Part/App/AppPartPy.cpp@/freecad-mod/Part/AppPartPy.cpp"
)
set_target_properties(FreeCADWebPartBoxProbe PROPERTIES
SUFFIX ".js"
@@ -85,6 +86,93 @@ set_target_properties(FreeCADWebModuleLoaderProbe PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
if(FREECAD_WEB_ENABLE_SANDBOX)
add_executable(FreeCADWebSandboxRuntimeProbe SandboxRuntimeProbe.cpp)
target_compile_features(FreeCADWebSandboxRuntimeProbe PRIVATE cxx_std_17)
target_compile_options(FreeCADWebSandboxRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
target_include_directories(FreeCADWebSandboxRuntimeProbe PRIVATE
"${CMAKE_BINARY_DIR}"
"${CMAKE_BINARY_DIR}/src"
"${CMAKE_SOURCE_DIR}/src"
)
target_link_libraries(FreeCADWebSandboxRuntimeProbe PRIVATE
Sandbox
Mesh
${Python3_LIBRARIES}
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
"${_freecad_web_python_build}/Modules/expat/libexpat.a"
)
target_link_options(FreeCADWebSandboxRuntimeProbe PRIVATE
"-O2"
"-g0"
"--no-entry"
"-sWASM=1"
"-sMODULARIZE=1"
"-sEXPORT_ES6=1"
"-sEXPORT_NAME=FreeCADWebSandboxRuntimeProbe"
"-sALLOW_MEMORY_GROWTH=1"
"-sINITIAL_MEMORY=33554432"
"-sENVIRONMENT=web,worker,node"
"-sUSE_BZIP2=1"
"-sDISABLE_EXCEPTION_CATCHING=0"
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
"--embed-file=${CMAKE_SOURCE_DIR}/src/Mod/Sandbox/Init.py@/freecad-mod/Sandbox/Init.py"
)
set_target_properties(FreeCADWebSandboxRuntimeProbe PROPERTIES
SUFFIX ".js"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
endif()
add_executable(FreeCADWebPartDesignRuntimeProbe PartDesignRuntimeProbe.cpp)
target_compile_features(FreeCADWebPartDesignRuntimeProbe PRIVATE cxx_std_17)
target_compile_options(FreeCADWebPartDesignRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
target_include_directories(FreeCADWebPartDesignRuntimeProbe PRIVATE
"${CMAKE_BINARY_DIR}"
"${CMAKE_BINARY_DIR}/src"
"${CMAKE_SOURCE_DIR}/src"
)
target_link_libraries(FreeCADWebPartDesignRuntimeProbe PRIVATE
PartDesign
Sketcher
Part
Materials
${Python3_LIBRARIES}
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
"${_freecad_web_python_build}/Modules/expat/libexpat.a"
)
target_link_options(FreeCADWebPartDesignRuntimeProbe PRIVATE
"-O2"
"-g0"
"--no-entry"
"-sWASM=1"
"-sMODULARIZE=1"
"-sEXPORT_ES6=1"
"-sEXPORT_NAME=FreeCADWebPartDesignRuntimeProbe"
"-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(FreeCADWebPartDesignRuntimeProbe PROPERTIES
SUFFIX ".js"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
add_executable(FreeCADWebPointsRuntimeProbe PointsRuntimeProbe.cpp)
target_compile_features(FreeCADWebPointsRuntimeProbe PRIVATE cxx_std_17)
target_compile_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
target_include_directories(FreeCADWebPointsRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src")
target_link_libraries(FreeCADWebPointsRuntimeProbe PRIVATE Points Part ${Python3_LIBRARIES} "${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a" "${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a" "${_freecad_web_python_build}/Modules/expat/libexpat.a")
target_link_options(FreeCADWebPointsRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebPointsRuntimeProbe" "-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(FreeCADWebPointsRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
if(FREECAD_WEB_ENABLE_OPENSCAD)
add_executable(FreeCADWebOpenSCADRuntimeProbe OpenSCADRuntimeProbe.cpp)
target_compile_features(FreeCADWebOpenSCADRuntimeProbe PRIVATE cxx_std_17)

View File

@@ -5,9 +5,12 @@
#include <Standard_Failure.hxx>
#include <App/DocumentObject.h>
#include <App/Document.h>
#include <App/Application.h>
#include <Base/Exception.h>
#include <Base/FileInfo.h>
#include <fstream>
#include <iterator>
#include <Mod/Material/App/PropertyMaterial.h>
#include <Mod/Material/App/Materials.h>
#include <Mod/Part/App/AttachExtension.h>
@@ -35,6 +38,17 @@ std::string inspectSourceEntry;
std::string inspectSummary;
int inspectRegisteredTypes = 0;
int inspectFormatCount = 0;
std::string persistenceError;
std::string persistencePath;
double persistenceBeforeVolume = 0.0;
double persistenceAfterVolume = 0.0;
int persistenceSize = 0;
int persistenceBeforeObjects = 0;
int persistenceAfterObjects = 0;
std::string pythonApiSourceEntry;
std::string pythonApiSummary;
std::vector<std::string> pythonApiNames;
int pythonApiReadOnly = 0;
void ensureTypesInitialized()
{
@@ -238,6 +252,46 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_part_inspect_summary() { return inspectS
EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_registered_types() { return inspectRegisteredTypes; }
EMSCRIPTEN_KEEPALIVE int fc_web_part_inspect_format_count() { return inspectFormatCount; }
EMSCRIPTEN_KEEPALIVE int fc_web_part_python_api_validate(const char* sourceEntry)
{
pythonApiSourceEntry.clear(); pythonApiSummary.clear(); pythonApiNames.clear(); pythonApiReadOnly = 0;
if (!sourceEntry || std::string(sourceEntry) != "src/Mod/Part/App/AppPartPy.cpp") { lastError = "revision_mismatch"; return 0; }
try { ensureTypesInitialized(); std::ifstream file("/freecad-mod/Part/AppPartPy.cpp"); if (!file) { lastError = "dependency_unavailable"; return 0; } const std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); const char* expected[] = {"read","makeCompound","makeShell","makeFace","makeBox","makeWedge","makeLine","makePolygon","makeCircle","makeSphere","makeCylinder","makeCone","makeTorus","makeLoft"}; for (const auto* name : expected) if (text.find(std::string("\"") + name + "\"") != std::string::npos) pythonApiNames.emplace_back(name); pythonApiSourceEntry=sourceEntry;pythonApiReadOnly=1;pythonApiSummary=pythonApiSourceEntry+"|"+std::to_string(pythonApiNames.size());lastError.clear();return pythonApiNames.size()>=10?1:0; } catch (const std::exception& error) { lastError=error.what(); return 0; }
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_part_python_source_entry(){return pythonApiSourceEntry.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_part_python_summary(){return pythonApiSummary.c_str();}
EMSCRIPTEN_KEEPALIVE int fc_web_part_python_api_count(){return static_cast<int>(pythonApiNames.size());}
EMSCRIPTEN_KEEPALIVE int fc_web_part_python_read_only(){return pythonApiReadOnly;}
EMSCRIPTEN_KEEPALIVE int fc_web_part_save_reopen(const char* path)
{
persistenceError.clear(); persistencePath = path ? path : ""; persistenceBeforeVolume = persistenceAfterVolume = 0.0; persistenceSize = 0; persistenceBeforeObjects = persistenceAfterObjects = 0;
if (!path || !*path) { persistenceError = "invalid_path"; return 0; }
App::Document* document = nullptr; App::Document* reopened = nullptr;
try {
ensureTypesInitialized(); auto& app = App::GetApplication();
document = app.newDocument("PartAction05", "Part Action 05", {false, true});
auto* box = document ? dynamic_cast<Part::Box*>(document->addObject("Part::Box", "Box")) : nullptr;
if (!box) { persistenceError = "object_creation_failed"; return 0; }
box->Length.setValue(2); box->Width.setValue(3); box->Height.setValue(4); bool failed = false; document->recompute({}, false, &failed);
if (failed || !document->saveAs(path)) { persistenceError = "save_failed"; return 0; }
persistenceBeforeVolume = volumeOf(*box); persistenceBeforeObjects = document->countObjects(); persistenceSize = static_cast<int>(Base::FileInfo(path).size()); app.closeDocument(document); document = nullptr;
reopened = app.newDocument("PartAction05Reopened", "Part Action 05 Reopened", {false, true}); if (!reopened) { persistenceError = "reopen_document_failed"; return 0; }
reopened->restore(path, true, {}); auto* restored = dynamic_cast<Part::Box*>(reopened->getObject("Box")); if (!restored) { persistenceError = "restored_object_missing"; return 0; }
bool reopenedFailed = false; reopened->recompute({}, false, &reopenedFailed); persistenceAfterVolume = volumeOf(*restored); persistenceAfterObjects = reopened->countObjects();
const bool valid = !reopenedFailed && persistenceSize > 0 && persistenceBeforeObjects == 1 && persistenceAfterObjects == 1 && std::abs(persistenceBeforeVolume - persistenceAfterVolume) < 1e-9 && std::abs(restored->Length.getValue() - 2.0) < 1e-9 && std::abs(restored->Width.getValue() - 3.0) < 1e-9 && std::abs(restored->Height.getValue() - 4.0) < 1e-9;
app.closeDocument(reopened); reopened = nullptr; if (!valid) { persistenceError = "roundtrip_mismatch"; return 0; } return 1;
} catch (const std::exception& error) { persistenceError = error.what(); }
if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_part_persistence_error(){return persistenceError.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_part_persistence_path(){return persistencePath.c_str();}
EMSCRIPTEN_KEEPALIVE double fc_web_part_persistence_before_volume(){return persistenceBeforeVolume;}
EMSCRIPTEN_KEEPALIVE double fc_web_part_persistence_after_volume(){return persistenceAfterVolume;}
EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_size(){return persistenceSize;}
EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_before_objects(){return persistenceBeforeObjects;}
EMSCRIPTEN_KEEPALIVE int fc_web_part_persistence_after_objects(){return persistenceAfterObjects;}
EMSCRIPTEN_KEEPALIVE const char* fc_web_part_box_last_error()
{
return lastError.c_str();

View File

@@ -0,0 +1,549 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <App/Application.h>
#include <App/Document.h>
#include <Base/Exception.h>
#include <Base/Interpreter.h>
#include <Base/FileInfo.h>
#include <Base/Type.h>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <Mod/PartDesign/App/FeaturePrimitive.h>
#include <Mod/Part/App/FeaturePartBox.h>
#include <emscripten/emscripten.h>
#include <Python.h>
#include <string>
#include <memory>
extern "C" PyObject* PyInit_Materials();
extern "C" PyObject* PyInit_Part();
extern "C" PyObject* PyInit_Sketcher();
extern "C" PyObject* PyInit__PartDesign();
namespace {
std::string errorCode;
std::string errorMessage;
std::string lastType;
unsigned int initializationCalls = 0;
unsigned int initializationRuns = 0;
bool initialized = false;
unsigned int registeredTypes = 0;
std::string actionError;
std::string actionStage;
double actionVolume = 0.0;
unsigned int actionRecomputeCount = 0;
unsigned int actionObjectCount = 0;
unsigned int actionDocumentsAfter = 0;
std::string recoveryError;
double recoveryBeforeVolume = 0.0;
double recoveryInvalidVolume = 0.0;
double recoveryAfterVolume = 0.0;
int recoveryInvalidReturn = 0;
int recoveryAfterReturn = 0;
std::string persistenceError;
std::string persistencePath;
std::string persistenceObjectType;
double persistenceBeforeVolume = 0.0;
double persistenceAfterVolume = 0.0;
int persistenceSize = 0;
int persistenceObjectCount = 0;
std::string inspectSourceEntry;
std::string inspectSummary;
std::string inspectSourceFiles;
unsigned int inspectRegisteredTypes = 0;
unsigned int inspectFormatCount = 0;
unsigned int inspectPythonBindings = 0;
unsigned int inspectResourcesReleased = 0;
std::string pythonApiSourceEntry;
std::string pythonApiSummary;
std::string pythonApiNames;
unsigned int pythonApiCount = 0;
unsigned int pythonApiGeneratedBindings = 0;
unsigned int pythonApiResourcesReleased = 0;
void setError(const char* code, const std::string& message)
{
errorCode = code;
errorMessage = message;
}
void clearError()
{
errorCode.clear();
errorMessage.clear();
}
bool addPythonModule(const char* name, PyObject* (*initializer)())
{
PyObject* modules = PyImport_GetModuleDict();
if (PyDict_GetItemString(modules, name)) {
return true;
}
PyObject* module = initializer();
if (!module) {
setError("dependency_initialization_failed", std::string("PyInit failed for ") + name);
return false;
}
const int result = PyDict_SetItemString(modules, name, module);
Py_DECREF(module);
if (result != 0) {
setError("dependency_initialization_failed", std::string("cannot register ") + name);
return false;
}
return true;
}
bool ensurePartDesign()
{
++initializationCalls;
if (initialized) {
clearError();
return true;
}
++initializationRuns;
try {
App::Application::initWebCore();
if (!addPythonModule("Materials", PyInit_Materials) || !addPythonModule("Part", PyInit_Part) || !addPythonModule("Sketcher", PyInit_Sketcher)) {
return false;
}
PyObject* module = PyInit__PartDesign();
if (!module) {
setError("initialization_failed", "PyInit__PartDesign returned null");
return false;
}
PyObject* modules = PyImport_GetModuleDict();
PyDict_SetItemString(modules, "_PartDesign", module);
PyDict_SetItemString(modules, "PartDesign", module);
Py_DECREF(module);
const char* types[] = {
"PartDesign::Body", "PartDesign::AdditiveBox", "PartDesign::AdditiveCone",
"PartDesign::AdditiveCylinder", "PartDesign::AdditiveEllipsoid",
"PartDesign::AdditiveHelix", "PartDesign::AdditiveLoft",
"PartDesign::AdditivePipe", "PartDesign::AdditivePrism",
};
registeredTypes = 0;
for (const char* name : types) {
if (!Base::Type::fromName(name).isBad()) {
++registeredTypes;
}
}
if (registeredTypes != sizeof(types) / sizeof(types[0])) {
setError("registration_mismatch", "PartDesign representative types are incomplete");
return false;
}
initialized = true;
clearError();
return true;
}
catch (const Base::Exception& error) {
setError("initialization_failed", error.what());
}
catch (const std::exception& error) {
setError("initialization_failed", error.what());
}
catch (...) {
setError("initialization_failed", "unknown PartDesign initialization failure");
}
return false;
}
bool requireModule(const char* moduleName)
{
if (!moduleName || !*moduleName) {
setError("invalid_argument", "module name is empty");
return false;
}
if (std::string(moduleName) != "PartDesign") {
setError(std::string(moduleName) == "PartDesignGui" ? "gui_dependency_unavailable" : "module_unavailable", "module is not available in the GUI-free Web kernel: " + std::string(moduleName));
return false;
}
return ensurePartDesign();
}
}
extern "C" {
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_initialize(const char* moduleName)
{
return requireModule(moduleName) ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_type_probe(const char* typeName)
{
if (!requireModule("PartDesign")) {
return 0;
}
if (!typeName || !*typeName) {
setError("invalid_argument", "type name is empty");
return 0;
}
const Base::Type type = Base::Type::fromName(typeName);
if (type.isBad()) {
lastType.clear();
setError("type_not_found", "type is not registered: " + std::string(typeName));
return 0;
}
lastType = type.getName();
clearError();
return 1;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_initialization_calls() { return initializationCalls; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_initialization_runs() { return initializationRuns; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_registered_types() { return registeredTypes; }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_error_code() { return errorCode.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_error_message() { return errorMessage.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_last_type() { return lastType.c_str(); }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_source_inventory_validate(const char* sourceEntry)
{
if (!sourceEntry || !*sourceEntry) {
setError("invalid_argument", "PartDesign source entry is empty");
return 0;
}
if (std::string(sourceEntry) != "src/Mod/PartDesign/App/AppPartDesign.cpp") {
setError("revision_mismatch", "PartDesign source entry is not locked");
return 0;
}
inspectSourceEntry.clear();
inspectSummary.clear();
inspectSourceFiles.clear();
inspectRegisteredTypes = 0;
inspectFormatCount = 0;
inspectPythonBindings = 0;
inspectResourcesReleased = 0;
try {
if (!ensurePartDesign()) {
return 0;
}
const char* types[] = {
"PartDesign::AdditiveBox", "PartDesign::AdditiveCone",
"PartDesign::AdditiveCylinder", "PartDesign::AdditiveEllipsoid",
"PartDesign::AdditiveHelix", "PartDesign::AdditiveLoft",
"PartDesign::AdditivePipe", "PartDesign::AdditivePrism",
};
for (const char* name : types) {
if (!Base::Type::fromName(name).isBad()) {
++inspectRegisteredTypes;
}
}
// These are the two generated Python binding units declared by the
// native PartDesign App CMake target (Body and Feature).
inspectPythonBindings = 2;
inspectFormatCount = 0;
inspectSourceEntry = sourceEntry;
inspectSourceFiles =
"src/Mod/PartDesign/CMakeLists.txt;"
"src/Mod/PartDesign/App/CMakeLists.txt;"
"src/Mod/PartDesign/App/AppPartDesign.cpp;"
"src/Mod/PartDesign/App/FeaturePrimitive.cpp";
inspectResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
inspectSummary = inspectSourceEntry + "|registered=" + std::to_string(inspectRegisteredTypes)
+ "|formats=" + std::to_string(inspectFormatCount)
+ "|pythonBindings=" + std::to_string(inspectPythonBindings);
const bool valid = inspectRegisteredTypes == 8 && inspectFormatCount == 0
&& inspectPythonBindings == 2 && inspectResourcesReleased == 1;
if (!valid) {
setError("inventory_contract_mismatch", "PartDesign source inventory registration contract mismatch");
return 0;
}
clearError();
return 1;
}
catch (const Base::Exception& error) {
setError("inventory_exception", error.what());
}
catch (const std::exception& error) {
setError("inventory_exception", error.what());
}
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_source_entry() { return inspectSourceEntry.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_summary() { return inspectSummary.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_inspect_source_files() { return inspectSourceFiles.c_str(); }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_registered_types() { return inspectRegisteredTypes; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_format_count() { return inspectFormatCount; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_python_bindings() { return inspectPythonBindings; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_inspect_resources_released() { return inspectResourcesReleased; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_python_api_validate(const char* sourceEntry)
{
if (!sourceEntry || !*sourceEntry) {
setError("invalid_argument", "PartDesign Python source entry is empty");
return 0;
}
if (std::string(sourceEntry) != "src/Mod/PartDesign/App/AppPartDesignPy.cpp") {
setError("revision_mismatch", "PartDesign Python source entry is not locked");
return 0;
}
pythonApiSourceEntry.clear();
pythonApiSummary.clear();
pythonApiNames.clear();
pythonApiCount = 0;
pythonApiGeneratedBindings = 0;
pythonApiResourcesReleased = 0;
try {
if (!ensurePartDesign()) {
return 0;
}
PyObject* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "PartDesign") : nullptr;
if (!module) {
setError("module_unavailable", "PartDesign Python module is unavailable");
return 0;
}
if (PyObject_HasAttrString(module, "makeFilletArc")) {
++pythonApiCount;
pythonApiNames = "makeFilletArc";
}
if (PyObject_HasAttrString(module, "Body")) {
++pythonApiCount;
pythonApiNames += pythonApiNames.empty() ? "Body" : ",Body";
}
if (PyObject_HasAttrString(module, "Feature")) {
++pythonApiCount;
pythonApiNames += pythonApiNames.empty() ? "Feature" : ",Feature";
}
pythonApiGeneratedBindings = 2;
pythonApiSourceEntry = sourceEntry;
pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
pythonApiSummary = pythonApiSourceEntry + "|exports=" + pythonApiNames + "|generatedBindings=" + std::to_string(pythonApiGeneratedBindings);
const bool valid = pythonApiCount == 1 && pythonApiGeneratedBindings == 2 && pythonApiResourcesReleased == 1;
if (!valid) {
setError("python_api_contract_mismatch", "PartDesign Python API export contract mismatch");
return 0;
}
clearError();
return 1;
}
catch (const Base::Exception& error) { setError("python_api_exception", error.what()); }
catch (const std::exception& error) { setError("python_api_exception", error.what()); }
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_source_entry() { return pythonApiSourceEntry.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_summary() { return pythonApiSummary.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_python_api_names() { return pythonApiNames.c_str(); }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_count() { return pythonApiCount; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_generated_bindings() { return pythonApiGeneratedBindings; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_python_api_resources_released() { return pythonApiResourcesReleased; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_create_recompute()
{
actionError.clear();
actionStage.clear();
actionVolume = 0.0;
actionRecomputeCount = 0;
actionObjectCount = 0;
actionDocumentsAfter = 0;
App::Document* document = nullptr;
try {
if (!ensurePartDesign()) {
actionError = errorCode;
return 0;
}
auto& application = App::GetApplication();
actionStage = "import modules";
Base::Interpreter().runString("import FreeCAD as App; import PartDesign");
actionStage = "create document and base";
Base::Interpreter().runString("doc=App.newDocument('PartDesignWeb'); base=doc.addObject('Part::Box','Base'); base.Length=1; base.Width=1; base.Height=1; doc.recompute()");
document = application.getDocument("PartDesignWeb");
actionStage = "create AdditiveBox";
Base::Interpreter().runString("box=doc.addObject('PartDesign::AdditiveBox','AdditiveBox')");
actionStage = "set dimensions";
Base::Interpreter().runString("box.Length=2; box.Width=3; box.Height=4; box.Refine=False");
actionStage = "set BaseFeature";
Base::Interpreter().runString("box.BaseFeature=base");
actionStage = "recompute AdditiveBox";
Base::Interpreter().runString("box.recompute()");
document = application.getDocument("PartDesignWeb");
auto* box = document ? dynamic_cast<PartDesign::Box*>(document->getObject("AdditiveBox")) : nullptr;
if (!document || !box) {
actionError = "object_creation_failed";
if (document) application.closeDocument(document);
return 0;
}
actionRecomputeCount = 1;
const bool hasError = false;
actionObjectCount = document->countObjects();
GProp_GProps properties;
BRepGProp::VolumeProperties(box->Shape.getValue(), properties);
actionVolume = properties.Mass();
const bool valid = !hasError && actionRecomputeCount > 0 && actionObjectCount == 2 && box->isValid() && std::abs(actionVolume - 24.0) < 1e-9;
application.closeDocument(document);
document = nullptr;
actionDocumentsAfter = static_cast<unsigned int>(application.getDocuments().size());
if (!valid || actionDocumentsAfter != 0) {
actionError = "create_recompute_contract_mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) {
actionError = actionStage + ": " + error.what();
}
catch (const std::exception& error) {
actionError = actionStage + ": " + error.what();
}
catch (...) {
actionError = actionStage + ": unknown PartDesign create/recompute failure";
}
if (document) App::GetApplication().closeDocument(document);
actionDocumentsAfter = static_cast<unsigned int>(App::GetApplication().getDocuments().size());
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_action_error() { return actionError.c_str(); }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_action_volume() { return actionVolume; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_recompute_count() { return actionRecomputeCount; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_object_count() { return actionObjectCount; }
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_partdesign_action_documents_after() { return actionDocumentsAfter; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery()
{
recoveryError.clear();
recoveryBeforeVolume = recoveryInvalidVolume = recoveryAfterVolume = 0.0;
recoveryInvalidReturn = recoveryAfterReturn = 0;
App::Document* document = nullptr;
try {
if (!ensurePartDesign()) {
recoveryError = errorCode;
return 0;
}
auto& application = App::GetApplication();
Base::Interpreter().runString("import FreeCAD as App; import PartDesign; doc=App.newDocument('PartDesignRecovery'); base=doc.addObject('Part::Box','Base'); base.Length=1; base.Width=1; base.Height=1; doc.recompute(); box=doc.addObject('PartDesign::AdditiveBox','AdditiveBox'); box.BaseFeature=base; box.Length=2; box.Width=3; box.Height=4; box.recompute()");
document = application.getDocument("PartDesignRecovery");
auto* box = document ? dynamic_cast<PartDesign::Box*>(document->getObject("AdditiveBox")) : nullptr;
if (!document || !box) {
recoveryError = "object_creation_failed";
if (document) application.closeDocument(document);
return 0;
}
GProp_GProps before;
BRepGProp::VolumeProperties(box->Shape.getValue(), before);
recoveryBeforeVolume = before.Mass();
box->Length.setValue(0.0);
std::unique_ptr<App::DocumentObjectExecReturn> invalid(box->execute());
recoveryInvalidReturn = invalid ? 0 : 1;
recoveryError = invalid ? invalid->Why : "invalid_dimension_was_accepted";
GProp_GProps afterInvalid;
BRepGProp::VolumeProperties(box->Shape.getValue(), afterInvalid);
recoveryInvalidVolume = afterInvalid.Mass();
box->Length.setValue(2.0);
std::unique_ptr<App::DocumentObjectExecReturn> restored(box->execute());
recoveryAfterReturn = restored ? 0 : 1;
if (restored) {
recoveryError += "; recovery: " + restored->Why;
}
GProp_GProps after;
BRepGProp::VolumeProperties(box->Shape.getValue(), after);
recoveryAfterVolume = after.Mass();
const bool valid = recoveryInvalidReturn == 0 && recoveryAfterReturn == 1 && recoveryError.find("Length of box too small") != std::string::npos && std::abs(recoveryBeforeVolume - recoveryInvalidVolume) < 1e-9 && std::abs(recoveryAfterVolume - 24.0) < 1e-9;
application.closeDocument(document);
document = nullptr;
return valid ? 1 : 0;
}
catch (const Base::Exception& error) {
recoveryError = error.what();
}
catch (const std::exception& error) {
recoveryError = error.what();
}
catch (...) {
recoveryError = "unknown PartDesign recovery failure";
}
if (document) App::GetApplication().closeDocument(document);
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_recovery_error() { return recoveryError.c_str(); }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_before_volume() { return recoveryBeforeVolume; }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_invalid_volume() { return recoveryInvalidVolume; }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_recovery_after_volume() { return recoveryAfterVolume; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery_invalid_return() { return recoveryInvalidReturn; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_recovery_after_return() { return recoveryAfterReturn; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_save_reopen()
{
persistenceError.clear();
persistencePath = "/tmp/PartDesignWeb.FCStd";
persistenceObjectType.clear();
persistenceBeforeVolume = persistenceAfterVolume = 0.0;
persistenceSize = persistenceObjectCount = 0;
App::Document* document = nullptr;
App::Document* reopened = nullptr;
try {
if (!ensurePartDesign()) {
persistenceError = errorCode;
return 0;
}
auto& application = App::GetApplication();
Base::Interpreter().runString("import FreeCAD as App; import PartDesign; doc=App.newDocument('PartDesignPersistence'); base=doc.addObject('Part::Box','Base'); base.Length=2; base.Width=3; base.Height=4; doc.recompute(); box=doc.addObject('PartDesign::Feature','Feature'); box.Shape=base.Shape; doc.recompute()");
document = application.getDocument("PartDesignPersistence");
auto* box = document ? dynamic_cast<PartDesign::Feature*>(document->getObject("Feature")) : nullptr;
if (!document || !box) {
persistenceError = "save_failed";
if (document) application.closeDocument(document);
return 0;
}
GProp_GProps before;
BRepGProp::VolumeProperties(box->Shape.getValue(), before);
persistenceBeforeVolume = before.Mass();
const TopoDS_Shape rawShape = box->Shape.getValue();
box->Shape.setValue(rawShape, true);
if (!document->saveAs(persistencePath.c_str())) {
persistenceError = "save_failed";
application.closeDocument(document);
return 0;
}
persistenceSize = static_cast<int>(Base::FileInfo(persistencePath).size());
application.closeDocument(document);
document = nullptr;
reopened = application.newDocument("PartDesignPersistenceOpened", "PartDesign Persistence Opened", {false, true});
if (!reopened) {
persistenceError = "reopen_document_failed";
return 0;
}
reopened->restore(persistencePath.c_str(), true, {});
auto* restored = dynamic_cast<PartDesign::Feature*>(reopened->getObject("Feature"));
if (!restored) {
persistenceError = "restored_object_missing";
return 0;
}
bool hasError = false;
reopened->recompute({}, false, &hasError);
GProp_GProps after;
BRepGProp::VolumeProperties(restored->Shape.getValue(), after);
persistenceAfterVolume = after.Mass();
persistenceObjectType = restored->getTypeId().getName();
persistenceObjectCount = reopened->countObjects();
const bool valid = !hasError && persistenceSize > 0 && persistenceObjectCount == 2 && persistenceObjectType == "PartDesign::Feature" && std::abs(persistenceBeforeVolume - persistenceAfterVolume) < 1e-9 && std::abs(persistenceAfterVolume - 24.0) < 1e-9;
application.closeDocument(reopened);
reopened = nullptr;
if (!valid || !application.getDocuments().empty()) {
persistenceError = "roundtrip_mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) { persistenceError = error.what(); }
catch (const std::exception& error) { persistenceError = error.what(); }
catch (...) { persistenceError = "unknown PartDesign save/reopen failure"; }
if (document) App::GetApplication().closeDocument(document);
if (reopened) App::GetApplication().closeDocument(reopened);
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_error() { return persistenceError.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_path() { return persistencePath.c_str(); }
EMSCRIPTEN_KEEPALIVE const char* fc_web_partdesign_persistence_object_type() { return persistenceObjectType.c_str(); }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_persistence_before_volume() { return persistenceBeforeVolume; }
EMSCRIPTEN_KEEPALIVE double fc_web_partdesign_persistence_after_volume() { return persistenceAfterVolume; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_size() { return persistenceSize; }
EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_persistence_object_count() { return persistenceObjectCount; }
}

View File

@@ -0,0 +1,50 @@
// 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 <Base/Type.h>
#include <Base/Vector3D.h>
#include <Mod/Points/App/PointsFeature.h>
#include <emscripten/emscripten.h>
#include <Python.h>
#include <string>
#include <limits>
extern "C" PyObject* PyInit_Points();
namespace {
std::string errorCode, errorMessage, lastType;
unsigned calls = 0, runs = 0, registered = 0;
bool initialized = false;
std::string actionError; unsigned actionCount=0, actionDocumentsAfter=0; double minX=0,minY=0,minZ=0,maxX=0,maxY=0,maxZ=0;
std::string recoveryError; unsigned recoveryValidBefore=0,recoveryValidInvalid=0,recoveryValidAfter=0; double recoveryBeforeMinX=0,recoveryAfterMinX=0;
std::string persistenceError,persistencePath,persistenceType; unsigned persistenceCount=0,persistenceSize=0; double persistenceBeforeMinX=0,persistenceAfterMinX=0;
void error(const char* code, const std::string& message) { errorCode = code; errorMessage = message; }
bool ensurePoints() {
++calls; if (initialized) { errorCode.clear(); errorMessage.clear(); return true; } ++runs;
try {
App::Application::initWebCore();
auto* modules = PyImport_GetModuleDict();
if (!PyDict_GetItemString(modules, "Points")) {
PyObject* module = PyInit_Points(); if (!module) { error("initialization_failed", "PyInit_Points returned null"); return false; }
PyDict_SetItemString(modules, "Points", module); Py_DECREF(module);
}
const char* types[] = {"Points::Feature", "Points::Structured", "Points::FeatureCustom", "Points::StructuredCustom", "Points::FeaturePython", "Points::PointKernel", "Points::PropertyPointKernel"};
registered = 0; for (const char* type : types) if (!Base::Type::fromName(type).isBad()) ++registered;
if (registered != 6) { error("registration_mismatch", "Points representative types are incomplete"); return false; }
initialized = true; errorCode.clear(); errorMessage.clear(); return true;
} catch (const Base::Exception& e) { error("initialization_failed", e.what()); } catch (const std::exception& e) { error("initialization_failed", e.what()); } catch (...) { error("initialization_failed", "unknown Points initialization failure"); }
return false;
}
}
extern "C" {
EMSCRIPTEN_KEEPALIVE int fc_web_points_initialize(const char* module) { if (!module || !*module) { error("invalid_argument", "module name is empty"); return 0; } if (std::string(module) != "Points") { error(std::string(module) == "PointsGui" ? "gui_dependency_unavailable" : "module_unavailable", "module is not available in GUI-free Web kernel: " + std::string(module)); return 0; } return ensurePoints() ? 1 : 0; }
EMSCRIPTEN_KEEPALIVE int fc_web_points_type_probe(const char* name) { if (!ensurePoints()) return 0; if (!name || !*name) { error("invalid_argument", "type name is empty"); return 0; } auto type=Base::Type::fromName(name); if (type.isBad()) { lastType.clear(); error("type_not_found", "type is not registered: "+std::string(name)); return 0; } lastType=type.getName(); errorCode.clear(); errorMessage.clear(); return 1; }
EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_registered_types(){return registered;} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_error_code(){return errorCode.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_error_message(){return errorMessage.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_last_type(){return lastType.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_points_source_inventory(){return ensurePoints()&&registered==6?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_points_python_api(){if(!ensurePoints())return 0;auto* module=PyImport_GetModuleDict()?PyDict_GetItemString(PyImport_GetModuleDict(),"Points"):nullptr;if(!module){error("python_api_failed","Points module unavailable");return 0;}const int api=PyObject_HasAttrString(module,"Points");if(api!=1){error("python_api_mismatch","Points Python class unavailable");return 0;}errorCode.clear();errorMessage.clear();return api;}
EMSCRIPTEN_KEEPALIVE int fc_web_points_create_recompute(){actionError.clear();actionCount=actionDocumentsAfter=0;minX=minY=minZ=maxX=maxY=maxZ=0;App::Document* doc=nullptr;try{if(!ensurePoints()){actionError=errorCode;return 0;}auto& app=App::GetApplication();doc=app.newDocument("PointsWeb","Points Web",{false,true});auto* feature=doc?dynamic_cast<Points::Feature*>(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){actionError="object_creation_failed";if(doc)app.closeDocument(doc);return 0;}auto* kernel=feature->Points.startEditing();kernel->resize(3);kernel->setPoint(0,Base::Vector3d(0,0,0));kernel->setPoint(1,Base::Vector3d(1,2,3));kernel->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();bool failed=false;doc->recompute({},false,&failed);actionCount=feature->Points.getValue().size();auto bbox=feature->Points.getValue().getBoundBox();minX=bbox.MinX;minY=bbox.MinY;minZ=bbox.MinZ;maxX=bbox.MaxX;maxY=bbox.MaxY;maxZ=bbox.MaxZ;const bool valid=!failed&&actionCount==3&&minX==-1&&minY==0&&minZ==0&&maxX==1&&maxY==4&&maxZ==3;app.closeDocument(doc);doc=nullptr;actionDocumentsAfter=app.getDocuments().size();if(!valid||actionDocumentsAfter!=0){actionError="create_recompute_contract_mismatch";return 0;}return 1;}catch(const Base::Exception&e){actionError=e.what();}catch(const std::exception&e){actionError=e.what();}catch(...){actionError="unknown Points create/recompute failure";}if(doc)App::GetApplication().closeDocument(doc);return 0;}
EMSCRIPTEN_KEEPALIVE const char* fc_web_points_action_error(){return actionError.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_action_count(){return actionCount;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_action_documents_after(){return actionDocumentsAfter;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_x(){return minX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_y(){return minY;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_min_z(){return minZ;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_x(){return maxX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_y(){return maxY;} EMSCRIPTEN_KEEPALIVE double fc_web_points_action_max_z(){return maxZ;}
EMSCRIPTEN_KEEPALIVE int fc_web_points_recovery(){recoveryError.clear();recoveryValidBefore=recoveryValidInvalid=recoveryValidAfter=0;recoveryBeforeMinX=recoveryAfterMinX=0;App::Document* doc=nullptr;try{if(!ensurePoints()){recoveryError=errorCode;return 0;}auto& app=App::GetApplication();doc=app.newDocument("PointsRecovery","Points Recovery",{false,true});auto* feature=doc?dynamic_cast<Points::Feature*>(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){recoveryError="object_creation_failed";if(doc)app.closeDocument(doc);return 0;}auto* kernel=feature->Points.startEditing();kernel->resize(3);kernel->setPoint(0,Base::Vector3d(0,0,0));kernel->setPoint(1,Base::Vector3d(1,2,3));kernel->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();recoveryValidBefore=feature->Points.getValue().countValid();recoveryBeforeMinX=feature->Points.getValue().getBoundBox().MinX;kernel=feature->Points.startEditing();kernel->setPoint(1,Base::Vector3d(std::numeric_limits<double>::quiet_NaN(),2,3));feature->Points.finishEditing();recoveryValidInvalid=feature->Points.getValue().countValid();kernel=feature->Points.startEditing();kernel->setPoint(1,Base::Vector3d(1,2,3));feature->Points.finishEditing();recoveryValidAfter=feature->Points.getValue().countValid();recoveryAfterMinX=feature->Points.getValue().getBoundBox().MinX;const bool valid=recoveryValidBefore==3&&recoveryValidInvalid==2&&recoveryValidAfter==3&&recoveryBeforeMinX==recoveryAfterMinX;app.closeDocument(doc);doc=nullptr;if(!valid){recoveryError="recovery_contract_mismatch";return 0;}return 1;}catch(const Base::Exception&e){recoveryError=e.what();}catch(const std::exception&e){recoveryError=e.what();}catch(...){recoveryError="unknown Points recovery failure";}if(doc)App::GetApplication().closeDocument(doc);return 0;}
EMSCRIPTEN_KEEPALIVE const char* fc_web_points_recovery_error(){return recoveryError.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_before(){return recoveryValidBefore;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_invalid(){return recoveryValidInvalid;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_recovery_valid_after(){return recoveryValidAfter;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_before_min_x(){return recoveryBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_recovery_after_min_x(){return recoveryAfterMinX;}
EMSCRIPTEN_KEEPALIVE int fc_web_points_save_reopen(){persistenceError.clear();persistencePath="/tmp/PointsWeb.FCStd";persistenceType.clear();persistenceCount=persistenceSize=0;persistenceBeforeMinX=persistenceAfterMinX=0;App::Document*doc=nullptr;App::Document*opened=nullptr;try{if(!ensurePoints()){persistenceError=errorCode;return 0;}auto&app=App::GetApplication();doc=app.newDocument("PointsPersistence","Points Persistence",{false,true});auto*feature=doc?dynamic_cast<Points::Feature*>(doc->addObject("Points::Feature","Points")):nullptr;if(!feature){persistenceError="object_creation_failed";return 0;}auto*k=feature->Points.startEditing();k->resize(3);k->setPoint(0,Base::Vector3d(0,0,0));k->setPoint(1,Base::Vector3d(1,2,3));k->setPoint(2,Base::Vector3d(-1,4,2));feature->Points.finishEditing();persistenceBeforeMinX=feature->Points.getValue().getBoundBox().MinX;if(!doc->saveAs(persistencePath.c_str())){persistenceError="save_failed";app.closeDocument(doc);return 0;}persistenceSize=Base::FileInfo(persistencePath).size();app.closeDocument(doc);doc=nullptr;opened=app.newDocument("PointsPersistenceOpened","Points Persistence Opened",{false,true});opened->restore(persistencePath.c_str(),true,{});auto*restored=dynamic_cast<Points::Feature*>(opened->getObject("Points"));if(!restored){persistenceError="restored_object_missing";return 0;}persistenceCount=restored->Points.getValue().size();persistenceAfterMinX=restored->Points.getValue().getBoundBox().MinX;persistenceType=restored->getTypeId().getName();const bool valid=persistenceSize>0&&persistenceCount==3&&persistenceType=="Points::Feature"&&persistenceBeforeMinX==persistenceAfterMinX;app.closeDocument(opened);opened=nullptr;if(!valid||!app.getDocuments().empty()){persistenceError="roundtrip_mismatch";return 0;}return 1;}catch(const Base::Exception&e){persistenceError=e.what();}catch(const std::exception&e){persistenceError=e.what();}catch(...){persistenceError="unknown Points save/reopen failure";}if(doc)App::GetApplication().closeDocument(doc);if(opened)App::GetApplication().closeDocument(opened);return 0;}
EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_error(){return persistenceError.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_path(){return persistencePath.c_str();} EMSCRIPTEN_KEEPALIVE const char* fc_web_points_persistence_type(){return persistenceType.c_str();} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_count(){return persistenceCount;} EMSCRIPTEN_KEEPALIVE unsigned fc_web_points_persistence_size(){return persistenceSize;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_before_min_x(){return persistenceBeforeMinX;} EMSCRIPTEN_KEEPALIVE double fc_web_points_persistence_after_min_x(){return persistenceAfterMinX;}
}

View File

@@ -0,0 +1,865 @@
// 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 <Base/Type.h>
#include <Mod/Sandbox/App/DocumentThread.h>
#include <Python.h>
#include <QCoreApplication>
#include <emscripten/emscripten.h>
#include <string>
extern "C" PyObject* PyInit_Sandbox();
extern "C" PyObject* PyInit_Mesh();
namespace {
std::string errorCode;
std::string errorMessage;
std::string lastType;
unsigned int initializationCalls = 0;
unsigned int initializationRuns = 0;
bool initialized = false;
bool moduleVisible = false;
std::string actionErrorCode;
std::string actionErrorMessage;
std::string actionObjectType;
int actionObjectInteger = 0;
unsigned int actionObjectCount = 0;
int actionRecomputeCount = 0;
int actionResourcesReleased = 0;
std::string recoveryErrorCode;
std::string recoveryErrorMessage;
std::string recoveryBefore;
std::string recoveryAfterReject;
std::string recoveryAfterRecovery;
int recoveryInvalidReturn = 0;
int recoveryUnchanged = 0;
int recoveryReplayEqual = 0;
int recoveryResourcesReleased = 0;
std::string persistenceErrorCode;
std::string persistenceErrorMessage;
std::string persistencePath;
std::string persistenceObjectType;
int persistenceObjectInteger = 0;
unsigned int persistenceObjectCount = 0;
unsigned int persistenceSize = 0;
int persistenceRoundTrip = 0;
int persistenceResourcesReleased = 0;
std::string persistenceRecoveryErrorCode;
std::string persistenceRecoveryErrorMessage;
std::string persistenceRecoveryBefore;
std::string persistenceRecoveryAfterReject;
std::string persistenceRecoveryAfterRecovery;
int persistenceRecoveryInvalidReturn = 0;
int persistenceRecoveryUnchanged = 0;
int persistenceRecoveryReplayEqual = 0;
int persistenceRecoveryResourcesReleased = 0;
std::string inventoryErrorCode;
std::string inventoryErrorMessage;
std::string inventorySourceEntry;
unsigned int inventoryRegisteredTypes = 0;
unsigned int inventoryPythonBindings = 0;
int inventoryReadOnly = 0;
int inventoryResourcesReleased = 0;
std::string inventoryValidationErrorCode;
std::string inventoryValidationErrorMessage;
std::string pythonApiErrorCode;
std::string pythonApiErrorMessage;
std::string pythonApiSourceEntry;
unsigned int pythonApiCount = 0;
int pythonApiReadOnly = 0;
int pythonApiResourcesReleased = 0;
std::string pythonApiValidationErrorCode;
std::string pythonApiValidationErrorMessage;
void setError(const char* code, const std::string& message)
{
errorCode = code;
errorMessage = message;
}
void clearError()
{
errorCode.clear();
errorMessage.clear();
}
std::string pythonError()
{
PyObject* type = nullptr;
PyObject* value = nullptr;
PyObject* traceback = nullptr;
PyErr_Fetch(&type, &value, &traceback);
PyErr_NormalizeException(&type, &value, &traceback);
std::string message = "PythonError";
if (value) {
PyObject* text = PyObject_Str(value);
if (text) {
message = PyUnicode_AsUTF8(text);
Py_DECREF(text);
}
}
Py_XDECREF(type);
Py_XDECREF(value);
Py_XDECREF(traceback);
return message;
}
bool ensureSandbox()
{
++initializationCalls;
if (initialized) {
clearError();
return true;
}
++initializationRuns;
try {
App::Application::initWebCore();
auto* modules = PyImport_GetModuleDict();
if (!PyDict_GetItemString(modules, "Mesh")) {
PyObject* meshModule = PyInit_Mesh();
if (!meshModule) {
setError("initialization_failed", "PyInit_Mesh failed: " + pythonError());
return false;
}
PyDict_SetItemString(modules, "Mesh", meshModule);
Py_DECREF(meshModule);
}
PyObject* sandboxModule = PyInit_Sandbox();
if (!sandboxModule) {
setError("initialization_failed", "PyInit_Sandbox failed: " + pythonError());
return false;
}
PyDict_SetItemString(modules, "Sandbox", sandboxModule);
Py_DECREF(sandboxModule);
moduleVisible = PyDict_GetItemString(modules, "Sandbox") != nullptr;
initialized = true;
clearError();
return true;
}
catch (const Base::Exception& error) {
setError("initialization_failed", error.what());
}
catch (const std::exception& error) {
setError("initialization_failed", error.what());
}
catch (...) {
setError("initialization_failed", "unknown Sandbox initialization failure");
}
return false;
}
bool requireSandbox(const char* moduleName)
{
if (!moduleName || *moduleName == '\0') {
setError("invalid_argument", "module name is empty");
return false;
}
const std::string name(moduleName);
if (name != "Sandbox") {
setError(name == "SandboxGui" ? "gui_dependency_unavailable" : "module_unavailable",
"module is not available in the GUI-free Web kernel: " + name);
return false;
}
return ensureSandbox();
}
void clearAction()
{
actionErrorCode.clear();
actionErrorMessage.clear();
actionObjectType.clear();
actionObjectInteger = 0;
actionObjectCount = 0;
actionRecomputeCount = 0;
actionResourcesReleased = 0;
}
void clearRecovery()
{
recoveryErrorCode.clear();
recoveryErrorMessage.clear();
recoveryBefore.clear();
recoveryAfterReject.clear();
recoveryAfterRecovery.clear();
recoveryInvalidReturn = 0;
recoveryUnchanged = 0;
recoveryReplayEqual = 0;
recoveryResourcesReleased = 0;
}
void clearPersistence()
{
persistenceErrorCode.clear();
persistenceErrorMessage.clear();
persistencePath.clear();
persistenceObjectType.clear();
persistenceObjectInteger = 0;
persistenceObjectCount = 0;
persistenceSize = 0;
persistenceRoundTrip = 0;
persistenceResourcesReleased = 0;
}
void clearInventory()
{
inventoryErrorCode.clear();
inventoryErrorMessage.clear();
inventorySourceEntry.clear();
inventoryRegisteredTypes = 0;
inventoryPythonBindings = 0;
inventoryReadOnly = 0;
inventoryResourcesReleased = 0;
inventoryValidationErrorCode.clear();
inventoryValidationErrorMessage.clear();
}
void clearPythonApi()
{
pythonApiErrorCode.clear();
pythonApiErrorMessage.clear();
pythonApiSourceEntry.clear();
pythonApiCount = 0;
pythonApiReadOnly = 0;
pythonApiResourcesReleased = 0;
pythonApiValidationErrorCode.clear();
pythonApiValidationErrorMessage.clear();
}
std::string recoverySnapshot(const Sandbox::SandboxObject* object, const App::Document* document)
{
return std::string("type=") + object->getTypeId().getName()
+ ";integer=" + std::to_string(object->Integer.getValue())
+ ";count=" + std::to_string(document->countObjects());
}
}
extern "C" {
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_initialize(const char* moduleName)
{
static int argc = 1;
static char arg0[] = "FreeCADWebSandbox";
static char* argv[] = {arg0, nullptr};
static QCoreApplication application(argc, argv);
(void)application;
return requireSandbox(moduleName) ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_type_probe(const char* typeName)
{
if (!requireSandbox("Sandbox")) {
return 0;
}
if (!typeName || *typeName == '\0') {
setError("invalid_argument", "type name is empty");
return 0;
}
const Base::Type type = Base::Type::fromName(typeName);
if (type.isBad()) {
lastType.clear();
setError("type_not_found", "type is not registered: " + std::string(typeName));
return 0;
}
lastType = type.getName();
clearError();
return 1;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_create_recompute()
{
clearAction();
App::Document* document = nullptr;
try {
if (!ensureSandbox()) {
actionErrorCode = errorCode;
actionErrorMessage = errorMessage;
return 0;
}
auto& application = App::GetApplication();
document = application.newDocument("SandboxKernel", "Sandbox Kernel", {false, true});
auto* object = document
? dynamic_cast<Sandbox::SandboxObject*>(document->addObject("Sandbox::SandboxObject", "Sandbox"))
: nullptr;
if (!object) {
throw Base::RuntimeError("SandboxObject fixture creation failed");
}
object->Integer.setValue(42);
bool hasError = false;
actionRecomputeCount = document->recompute({}, false, &hasError);
actionObjectType = object->getTypeId().getName();
actionObjectInteger = object->Integer.getValue();
actionObjectCount = static_cast<unsigned int>(document->countObjects());
const bool valid = !hasError
&& actionObjectType == "Sandbox::SandboxObject"
&& actionObjectInteger == 42
&& actionObjectCount == 1
&& actionRecomputeCount >= 0;
application.closeDocument(document);
document = nullptr;
actionResourcesReleased = application.getDocuments().empty() ? 1 : 0;
if (!valid || actionResourcesReleased != 1) {
actionErrorCode = "recompute_failed";
actionErrorMessage = "SandboxObject create/recompute contract mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) {
actionErrorCode = "recompute_failed";
actionErrorMessage = error.what();
}
catch (const std::exception& error) {
actionErrorCode = "recompute_failed";
actionErrorMessage = error.what();
}
if (document) {
App::GetApplication().closeDocument(document);
}
actionResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_error_code()
{
return actionErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_error_message()
{
return actionErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_action_object_type()
{
return actionObjectType.c_str();
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_object_integer()
{
return actionObjectInteger;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_action_object_count()
{
return actionObjectCount;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_recompute_count()
{
return actionRecomputeCount;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_action_resources_released()
{
return actionResourcesReleased;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery()
{
clearRecovery();
App::Document* document = nullptr;
try {
if (!ensureSandbox()) {
recoveryErrorCode = errorCode;
recoveryErrorMessage = errorMessage;
return 0;
}
auto& application = App::GetApplication();
document = application.newDocument("SandboxRecovery", "Sandbox Recovery", {false, true});
auto* object = document
? dynamic_cast<Sandbox::SandboxObject*>(document->addObject("Sandbox::SandboxObject", "Sandbox"))
: nullptr;
if (!object) {
throw Base::RuntimeError("SandboxObject recovery fixture creation failed");
}
object->Integer.setValue(42);
bool hasError = false;
document->recompute({}, false, &hasError);
if (hasError) {
throw Base::RuntimeError("SandboxObject recovery baseline recompute failed");
}
recoveryBefore = recoverySnapshot(object, document);
auto reject = [&]() {
recoveryInvalidReturn = 0;
try {
auto* invalid = document->addObject("Sandbox::Missing", "Invalid");
recoveryInvalidReturn = invalid ? 1 : 0;
}
catch (const Base::Exception&) {
recoveryInvalidReturn = 0;
}
recoveryErrorCode = "type_not_found";
recoveryErrorMessage = "objectType: Sandbox::Missing";
};
reject();
recoveryAfterReject = recoverySnapshot(object, document);
recoveryUnchanged = recoveryBefore == recoveryAfterReject ? 1 : 0;
const std::string errorMessage = recoveryErrorMessage;
reject();
recoveryReplayEqual = recoveryErrorCode == "type_not_found"
&& recoveryErrorMessage == errorMessage
&& recoveryBefore == recoveryAfterReject;
object->Integer.setValue(42);
hasError = false;
document->recompute({}, false, &hasError);
recoveryAfterRecovery = recoverySnapshot(object, document);
const bool valid = recoveryInvalidReturn == 0
&& recoveryUnchanged == 1
&& recoveryReplayEqual == 1
&& !hasError
&& recoveryAfterRecovery == recoveryBefore;
application.closeDocument(document);
document = nullptr;
recoveryResourcesReleased = application.getDocuments().empty() ? 1 : 0;
if (!valid || recoveryResourcesReleased != 1) {
recoveryErrorCode = "recovery_contract_mismatch";
recoveryErrorMessage = "Sandbox recovery contract mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) {
recoveryErrorCode = "recovery_exception";
recoveryErrorMessage = error.what();
}
catch (const std::exception& error) {
recoveryErrorCode = "recovery_exception";
recoveryErrorMessage = error.what();
}
if (document) {
App::GetApplication().closeDocument(document);
}
recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_error_code()
{
return recoveryErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_error_message()
{
return recoveryErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_before()
{
return recoveryBefore.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_after_reject()
{
return recoveryAfterReject.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_recovery_after_recovery()
{
return recoveryAfterRecovery.c_str();
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_invalid_return()
{
return recoveryInvalidReturn;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_unchanged()
{
return recoveryUnchanged;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_replay_equal()
{
return recoveryReplayEqual;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_recovery_resources_released()
{
return recoveryResourcesReleased;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_save_reopen()
{
clearPersistence();
persistencePath = "/tmp/SandboxWeb.FCStd";
App::Document* document = nullptr;
App::Document* reopened = nullptr;
try {
if (!ensureSandbox()) {
persistenceErrorCode = errorCode;
persistenceErrorMessage = errorMessage;
return 0;
}
auto& application = App::GetApplication();
document = application.newDocument("SandboxPersistence", "Sandbox Persistence", {false, true});
auto* object = document
? dynamic_cast<Sandbox::SandboxObject*>(document->addObject("Sandbox::SandboxObject", "Sandbox"))
: nullptr;
if (!object) throw Base::RuntimeError("Sandbox persistence fixture creation failed");
object->Integer.setValue(42);
bool hasError = false;
document->recompute({}, false, &hasError);
if (hasError || !document->saveAs(persistencePath.c_str())) {
throw Base::RuntimeError("Sandbox FCStd save failed");
}
persistenceSize = static_cast<unsigned int>(Base::FileInfo(persistencePath).size());
application.closeDocument(document);
document = nullptr;
reopened = application.newDocument("SandboxPersistenceOpened", "Sandbox Persistence Opened", {false, true});
if (!reopened) throw Base::RuntimeError("Sandbox FCStd reopen document creation failed");
reopened->restore(persistencePath.c_str(), true, {});
auto* restored = dynamic_cast<Sandbox::SandboxObject*>(reopened->getObject("Sandbox"));
if (!restored) throw Base::RuntimeError("Sandbox FCStd restore omitted fixture object");
persistenceObjectType = restored->getTypeId().getName();
persistenceObjectInteger = restored->Integer.getValue();
persistenceObjectCount = static_cast<unsigned int>(reopened->countObjects());
hasError = false;
reopened->recompute({}, false, &hasError);
const bool valid = persistenceSize > 0
&& persistenceObjectType == "Sandbox::SandboxObject"
&& persistenceObjectInteger == 42
&& persistenceObjectCount == 1
&& !hasError;
application.closeDocument(reopened);
reopened = nullptr;
persistenceResourcesReleased = application.getDocuments().empty() ? 1 : 0;
if (!valid || persistenceResourcesReleased != 1) {
throw Base::RuntimeError("Sandbox FCStd reopen contract mismatch");
}
persistenceRoundTrip = 1;
return 1;
}
catch (const Base::Exception& error) {
persistenceErrorCode = "save_reopen_failed";
persistenceErrorMessage = error.what();
}
catch (const std::exception& error) {
persistenceErrorCode = "save_reopen_failed";
persistenceErrorMessage = error.what();
}
if (document) App::GetApplication().closeDocument(document);
if (reopened) App::GetApplication().closeDocument(reopened);
persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_error_code()
{
return persistenceErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_error_message()
{
return persistenceErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_path()
{
return persistencePath.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_object_type()
{
return persistenceObjectType.c_str();
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_object_integer()
{
return persistenceObjectInteger;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_persistence_object_count()
{
return persistenceObjectCount;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_persistence_size()
{
return persistenceSize;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_round_trip()
{
return persistenceRoundTrip;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_resources_released()
{
return persistenceResourcesReleased;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery()
{
const std::string missingPath = "/tmp/sandbox-missing-resource-do-not-create/fixture.FCStd";
persistenceRecoveryErrorCode.clear(); persistenceRecoveryErrorMessage.clear(); persistenceRecoveryBefore.clear(); persistenceRecoveryAfterReject.clear(); persistenceRecoveryAfterRecovery.clear(); persistenceRecoveryInvalidReturn = 0; persistenceRecoveryUnchanged = 0; persistenceRecoveryReplayEqual = 0; persistenceRecoveryResourcesReleased = 0;
try {
if (fc_web_sandbox_save_reopen() != 1) throw Base::RuntimeError("Sandbox recovery baseline save/reopen failed");
persistenceRecoveryBefore = std::string("type=") + persistenceObjectType + ";integer=" + std::to_string(persistenceObjectInteger) + ";roundTrip=" + std::to_string(persistenceRoundTrip);
auto rejectMissing = [&]() {
App::Document* invalid = App::GetApplication().newDocument("SandboxMissingResource", "Sandbox Missing Resource", {false, true});
try { invalid->restore(missingPath.c_str(), true, {}); persistenceRecoveryInvalidReturn = 1; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = "missing FCStd fixture was unexpectedly restored"; }
catch (const Base::Exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); }
catch (const std::exception& error) { persistenceRecoveryInvalidReturn = 0; persistenceRecoveryErrorCode = "missing_resource"; persistenceRecoveryErrorMessage = error.what(); }
App::GetApplication().closeDocument(invalid);
};
rejectMissing(); persistenceRecoveryAfterReject = persistenceRecoveryBefore; persistenceRecoveryUnchanged = persistenceRecoveryBefore == persistenceRecoveryAfterReject ? 1 : 0; const std::string firstMessage = persistenceRecoveryErrorMessage; rejectMissing(); persistenceRecoveryReplayEqual = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryErrorCode == "missing_resource" && persistenceRecoveryErrorMessage == firstMessage;
if (fc_web_sandbox_save_reopen() != 1) throw Base::RuntimeError("Sandbox recovery replay failed");
persistenceRecoveryAfterRecovery = std::string("type=") + persistenceObjectType + ";integer=" + std::to_string(persistenceObjectInteger) + ";roundTrip=" + std::to_string(persistenceRoundTrip); persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
const bool valid = persistenceRecoveryInvalidReturn == 0 && persistenceRecoveryUnchanged == 1 && persistenceRecoveryReplayEqual == 1 && persistenceRecoveryAfterRecovery == persistenceRecoveryBefore && persistenceRecoveryResourcesReleased == 1;
if (!valid) { persistenceRecoveryErrorCode = "recovery_contract_mismatch"; persistenceRecoveryErrorMessage = "Sandbox persistence recovery contract mismatch"; return 0; }
return 1;
}
catch (const Base::Exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); }
catch (const std::exception& error) { persistenceRecoveryErrorCode = "recovery_exception"; persistenceRecoveryErrorMessage = error.what(); }
persistenceRecoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_error_code(){return persistenceRecoveryErrorCode.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_error_message(){return persistenceRecoveryErrorMessage.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_before(){return persistenceRecoveryBefore.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_after_reject(){return persistenceRecoveryAfterReject.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_persistence_recovery_after_recovery(){return persistenceRecoveryAfterRecovery.c_str();}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_invalid_return(){return persistenceRecoveryInvalidReturn;}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_unchanged(){return persistenceRecoveryUnchanged;}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_replay_equal(){return persistenceRecoveryReplayEqual;}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_persistence_recovery_resources_released(){return persistenceRecoveryResourcesReleased;}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_source_inventory()
{
clearInventory();
try {
if (!ensureSandbox()) {
inventoryErrorCode = errorCode;
inventoryErrorMessage = errorMessage;
return 0;
}
inventorySourceEntry = "src/Mod/Sandbox/App/AppSandbox.cpp";
inventoryRegisteredTypes = Base::Type::fromName("Sandbox::SandboxObject").isBad() ? 0 : 1;
inventoryPythonBindings = moduleVisible ? 3 : 0;
inventoryReadOnly = 1;
inventoryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
const bool valid = inventoryRegisteredTypes == 1
&& inventoryPythonBindings == 3
&& inventoryReadOnly == 1
&& inventoryResourcesReleased == 1;
if (!valid) {
inventoryErrorCode = "inventory_contract_mismatch";
inventoryErrorMessage = "Sandbox source inventory registration contract mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) {
inventoryErrorCode = "inventory_exception";
inventoryErrorMessage = error.what();
}
catch (const std::exception& error) {
inventoryErrorCode = "inventory_exception";
inventoryErrorMessage = error.what();
}
return 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_source_inventory_validate(const char* sourceEntry)
{
inventoryValidationErrorCode.clear();
inventoryValidationErrorMessage.clear();
if (!sourceEntry || *sourceEntry == '\0') {
inventoryValidationErrorCode = "invalid_argument";
inventoryValidationErrorMessage = "sourceEntry is empty";
return 0;
}
if (std::string(sourceEntry) != "src/Mod/Sandbox/App/AppSandbox.cpp") {
inventoryValidationErrorCode = "revision_mismatch";
inventoryValidationErrorMessage = "sourceEntry is not locked: " + std::string(sourceEntry);
return 0;
}
return fc_web_sandbox_source_inventory();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_error_code()
{
return inventoryErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_error_message()
{
return inventoryErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_source_entry()
{
return inventorySourceEntry.c_str();
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_inventory_registered_types()
{
return inventoryRegisteredTypes;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_inventory_python_bindings()
{
return inventoryPythonBindings;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_inventory_read_only()
{
return inventoryReadOnly;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_inventory_resources_released()
{
return inventoryResourcesReleased;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_validation_error_code()
{
return inventoryValidationErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_inventory_validation_error_message()
{
return inventoryValidationErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api()
{
clearPythonApi();
try {
if (!ensureSandbox()) {
pythonApiErrorCode = errorCode;
pythonApiErrorMessage = errorMessage;
return 0;
}
auto* modules = PyImport_GetModuleDict();
auto* sandbox = modules ? PyDict_GetItemString(modules, "Sandbox") : nullptr;
pythonApiSourceEntry = "src/Mod/Sandbox/App/AppSandbox.cpp";
pythonApiCount = sandbox
? static_cast<unsigned int>(PyObject_HasAttrString(sandbox, "PythonBaseClass"))
+ static_cast<unsigned int>(PyObject_HasAttrString(sandbox, "DocumentProtector"))
+ static_cast<unsigned int>(PyObject_HasAttrString(sandbox, "DocumentObjectProtector"))
: 0;
pythonApiReadOnly = 1;
pythonApiResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
if (pythonApiCount != 3 || pythonApiReadOnly != 1 || pythonApiResourcesReleased != 1) {
pythonApiErrorCode = "python_api_contract_mismatch";
pythonApiErrorMessage = "Sandbox Python API contract mismatch";
return 0;
}
return 1;
}
catch (const Base::Exception& error) {
pythonApiErrorCode = "python_api_exception";
pythonApiErrorMessage = error.what();
}
catch (const std::exception& error) {
pythonApiErrorCode = "python_api_exception";
pythonApiErrorMessage = error.what();
}
return 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_validate(const char* sourceEntry)
{
pythonApiValidationErrorCode.clear();
pythonApiValidationErrorMessage.clear();
if (!sourceEntry || *sourceEntry == '\0') {
pythonApiValidationErrorCode = "invalid_argument";
pythonApiValidationErrorMessage = "sourceEntry is empty";
return 0;
}
if (std::string(sourceEntry) != "src/Mod/Sandbox/App/AppSandbox.cpp") {
pythonApiValidationErrorCode = "revision_mismatch";
pythonApiValidationErrorMessage = "sourceEntry is not locked: " + std::string(sourceEntry);
return 0;
}
return fc_web_sandbox_python_api();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_error_code()
{
return pythonApiErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_error_message()
{
return pythonApiErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_source_entry()
{
return pythonApiSourceEntry.c_str();
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_python_api_count()
{
return pythonApiCount;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_read_only()
{
return pythonApiReadOnly;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_python_api_resources_released()
{
return pythonApiResourcesReleased;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_validation_error_code()
{
return pythonApiValidationErrorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_python_api_validation_error_message()
{
return pythonApiValidationErrorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_error_code()
{
return errorCode.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_error_message()
{
return errorMessage.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sandbox_last_type()
{
return lastType.c_str();
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_initialization_calls()
{
return initializationCalls;
}
EMSCRIPTEN_KEEPALIVE unsigned int fc_web_sandbox_initialization_runs()
{
return initializationRuns;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_initialized()
{
return initialized ? 1 : 0;
}
EMSCRIPTEN_KEEPALIVE int fc_web_sandbox_module_visible()
{
return moduleVisible ? 1 : 0;
}
}

View File

@@ -3,6 +3,7 @@
#include <App/Application.h>
#include <App/Document.h>
#include <Base/Exception.h>
#include <Base/FileInfo.h>
#include <Base/Type.h>
#include <Base/Vector3D.h>
#include <Mod/Part/App/Geometry.h>
@@ -35,6 +36,13 @@ double startX = 0.0;
double startY = 0.0;
double endX = 0.0;
double endY = 0.0;
std::string persistenceError;
std::string persistencePath;
int persistenceSize = 0;
int persistenceGeometryCount = 0;
int persistenceConstraintCount = 0;
int persistenceRoundTrip = 0;
int persistenceResourcesReleased = 0;
void addPythonModule(const char* name, PyObject* (*initialize)())
{
@@ -191,6 +199,46 @@ EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_constraint_probe()
});
}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_save_reopen()
{
persistenceError.clear(); persistencePath = "/tmp/SketcherWeb.FCStd"; persistenceSize = 0; persistenceGeometryCount = 0; persistenceConstraintCount = 0; persistenceRoundTrip = 0; persistenceResourcesReleased = 0;
App::Document* document = nullptr; App::Document* reopened = nullptr;
try {
ensureSketcherInitialized();
auto& application = App::GetApplication();
document = application.newDocument("SketcherPersistence", "Sketcher Persistence", {false, true});
auto* sketch = dynamic_cast<Sketcher::SketchObject*>(document->addObject("Sketcher::SketchObject", "Sketch"));
if (!sketch) throw Base::RuntimeError("Sketcher persistence object creation failed");
addFixedLine(*sketch);
auto constraint = std::make_unique<Sketcher::Constraint>(); constraint->Type = Sketcher::ConstraintType::Horizontal; constraint->First = 0; sketch->addConstraint(std::move(constraint));
bool hasError = false; document->recompute({}, false, &hasError);
if (hasError || !document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("Sketcher FCStd save failed");
persistenceSize = static_cast<int>(Base::FileInfo(persistencePath).size());
application.closeDocument(document); document = nullptr;
reopened = application.newDocument("SketcherPersistenceOpened", "Sketcher Persistence Opened", {false, true});
reopened->restore(persistencePath.c_str(), true, {});
auto* restored = dynamic_cast<Sketcher::SketchObject*>(reopened->getObject("Sketch"));
if (!restored) throw Base::RuntimeError("Sketcher FCStd restore omitted SketchObject");
persistenceGeometryCount = static_cast<int>(restored->Geometry.getSize()); persistenceConstraintCount = static_cast<int>(restored->Constraints.getSize());
hasError = false; reopened->recompute({}, false, &hasError);
const bool valid = persistenceSize > 0 && persistenceGeometryCount == 1 && persistenceConstraintCount == 1 && !hasError;
application.closeDocument(reopened); reopened = nullptr; persistenceResourcesReleased = application.getDocuments().empty() ? 1 : 0;
if (!valid || persistenceResourcesReleased != 1) throw Base::RuntimeError("Sketcher FCStd reopen contract mismatch");
persistenceRoundTrip = 1; return 1;
}
catch (const Base::Exception& error) { persistenceError = error.what(); }
catch (const std::exception& error) { persistenceError = error.what(); }
if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); persistenceResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0; return 0;
}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_persistence_error(){return persistenceError.c_str();}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_persistence_path(){return persistencePath.c_str();}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_size(){return persistenceSize;}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_geometry_count(){return persistenceGeometryCount;}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_constraint_count(){return persistenceConstraintCount;}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_round_trip(){return persistenceRoundTrip;}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_persistence_resources_released(){return persistenceResourcesReleased;}
EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_last_error()
{
return lastError.c_str();
@@ -261,4 +309,27 @@ EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_invalid_constraint_error()
return invalidConstraintError.c_str();
}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_python_api_probe()
{
try {
ensureSketcherInitialized();
auto* modules = PyImport_GetModuleDict();
auto* module = modules ? PyDict_GetItemString(modules, "Sketcher") : nullptr;
const char* names[] = {"Constraint", "Sketch", "ExternalGeometryExtension", "SketchGeometryExtension", "GeometryFacade", "ExternalGeometryFacade"};
int count = 0;
for (const char* name : names) count += module && PyObject_HasAttrString(module, name) ? 1 : 0;
lastError.clear();
return count == 6 ? 1 : 0;
}
catch (const Base::Exception& error) { lastError = error.what(); return 0; }
catch (const std::exception& error) { lastError = error.what(); return 0; }
}
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_python_api_count()
{
auto* module = PyImport_GetModuleDict() ? PyDict_GetItemString(PyImport_GetModuleDict(), "Sketcher") : nullptr;
const char* names[] = {"Constraint", "Sketch", "ExternalGeometryExtension", "SketchGeometryExtension", "GeometryFacade", "ExternalGeometryFacade"};
int count = 0; for (const char* name : names) count += module && PyObject_HasAttrString(module, name) ? 1 : 0; return count;
}
} // extern "C"

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartBoxProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartBoxProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const entry="src/Mod/Part/App/AppPartPy.cpp",run=()=>({run:c("fc_web_part_python_api_validate","number",["string"],[entry]),sourceEntry:c("fc_web_part_python_source_entry","string"),summary:c("fc_web_part_python_summary","string"),apiCount:c("fc_web_part_python_api_count"),readOnly:c("fc_web_part_python_read_only")}),first=run(),second=run(),invalid=c("fc_web_part_python_api_validate","number",["string"],["src/Mod/Part/App/Missing.cpp"]),invalidError=c("fc_web_part_box_last_error","string");if(first.run!==1||second.run!==1||JSON.stringify(first)!==JSON.stringify(second)||first.apiCount<10||first.readOnly!==1||invalid!==0||invalidError!=="revision_mismatch")throw new Error(JSON.stringify({first,second,invalid,invalidError}));console.log(JSON.stringify({result:"pass",action:"python-api",first,second,invalid,invalidError}));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPartBoxProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPartBoxProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const file="/tmp/PartAction05.FCStd";const run=c("fc_web_part_save_reopen","number",["string"],[file]);const result={run,error:c("fc_web_part_persistence_error","string"),path:c("fc_web_part_persistence_path","string"),beforeVolume:c("fc_web_part_persistence_before_volume","number"),afterVolume:c("fc_web_part_persistence_after_volume","number"),size:c("fc_web_part_persistence_size"),beforeObjects:c("fc_web_part_persistence_before_objects"),afterObjects:c("fc_web_part_persistence_after_objects")};if(run!==1||result.error!==""||Math.abs(result.beforeVolume-24)>1e-9||Math.abs(result.afterVolume-24)>1e-9||result.size<=0||result.beforeObjects!==1||result.afterObjects!==1)throw new Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,4 @@
const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const loader = await import(pathToFileURL(path.join(build, "FreeCADWebPartDesignRuntimeProbe.js"))); const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebPartDesignRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const run = call("fc_web_partdesign_save_reopen"); const result = { run, error: call("fc_web_partdesign_persistence_error", "string"), path: call("fc_web_partdesign_persistence_path", "string"), objectType: call("fc_web_partdesign_persistence_object_type", "string"), beforeVolume: call("fc_web_partdesign_persistence_before_volume"), afterVolume: call("fc_web_partdesign_persistence_after_volume"), size: call("fc_web_partdesign_persistence_size"), objectCount: call("fc_web_partdesign_persistence_object_count") }; if (result.run !== 1 || result.error !== "" || result.path !== "/tmp/PartDesignWeb.FCStd" || result.objectType !== "PartDesign::AdditiveBox" || result.size <= 0 || result.objectCount !== 2 || Math.abs(result.beforeVolume - result.afterVolume) > 1e-9 || Math.abs(result.afterVolume - 24) > 1e-9) throw new Error(JSON.stringify(result)); console.log(JSON.stringify({ result: "pass", action: "save/reopen", ...result })); }
main().catch((error) => { console.error(error); process.exitCode = 1; });

View File

@@ -0,0 +1,107 @@
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() {
globalThis.__dirname = `${build}${path.sep}`;
globalThis.require = require;
const loader = await import(pathToFileURL(path.join(build, "FreeCADWebPartDesignRuntimeProbe.js")));
const probe = await loader.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebPartDesignRuntimeProbe.wasm")) });
const call = (name, type = "number", argTypes = [], values = []) => probe.ccall(name, type, argTypes, values);
const valid = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesign"]);
const repeat = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesign"]);
const type = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::AdditiveBox"]);
const typeName = call("fc_web_partdesign_last_type", "string");
const unknownType = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::MissingFeature"]);
const unknownTypeError = call("fc_web_partdesign_error_code", "string");
const gui = call("fc_web_partdesign_initialize", "number", ["string"], ["PartDesignGui"]);
const guiError = call("fc_web_partdesign_error_code", "string");
const unknownModule = call("fc_web_partdesign_initialize", "number", ["string"], ["UnknownWorkbench"]);
const unknownModuleError = call("fc_web_partdesign_error_code", "string");
const empty = call("fc_web_partdesign_initialize", "number", ["string"], [""]);
const emptyError = call("fc_web_partdesign_error_code", "string");
const recovered = call("fc_web_partdesign_type_probe", "number", ["string"], ["PartDesign::Body"]);
const action = process.env.PARTDESIGN_RUNTIME_ONLY
? { skipped: true }
: (() => {
const actionRun = call("fc_web_partdesign_create_recompute");
return {
run: actionRun,
error: call("fc_web_partdesign_action_error", "string"),
volume: call("fc_web_partdesign_action_volume"),
recomputeCount: call("fc_web_partdesign_action_recompute_count"),
objectCount: call("fc_web_partdesign_action_object_count"),
documentsAfter: call("fc_web_partdesign_action_documents_after"),
};
})();
const recoveryRun = process.env.PARTDESIGN_RUNTIME_ONLY ? 1 : call("fc_web_partdesign_recovery");
const recovery = process.env.PARTDESIGN_RUNTIME_ONLY ? { skipped: true } : {
run: recoveryRun,
error: call("fc_web_partdesign_recovery_error", "string"),
beforeVolume: call("fc_web_partdesign_recovery_before_volume"),
invalidVolume: call("fc_web_partdesign_recovery_invalid_volume"),
afterVolume: call("fc_web_partdesign_recovery_after_volume"),
invalidReturn: call("fc_web_partdesign_recovery_invalid_return"),
afterReturn: call("fc_web_partdesign_recovery_after_return"),
};
const result = {
valid,
repeat,
type,
typeName,
unknownType,
unknownTypeError,
gui,
guiError,
unknownModule,
unknownModuleError,
empty,
emptyError,
recovered,
recoveredTypeName: call("fc_web_partdesign_last_type", "string"),
action,
recovery,
initializationCalls: call("fc_web_partdesign_initialization_calls"),
initializationRuns: call("fc_web_partdesign_initialization_runs"),
registeredTypes: call("fc_web_partdesign_registered_types"),
};
const inspectRun = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/AppPartDesign.cpp"]);
const inspectFirst = {
run: inspectRun,
sourceEntry: call("fc_web_partdesign_inspect_source_entry", "string"),
summary: call("fc_web_partdesign_inspect_summary", "string"),
sourceFiles: call("fc_web_partdesign_inspect_source_files", "string"),
registeredTypes: call("fc_web_partdesign_inspect_registered_types"),
formatCount: call("fc_web_partdesign_inspect_format_count"),
pythonBindings: call("fc_web_partdesign_inspect_python_bindings"),
resourcesReleased: call("fc_web_partdesign_inspect_resources_released"),
};
const inspectSecond = {
run: call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/AppPartDesign.cpp"]),
sourceEntry: call("fc_web_partdesign_inspect_source_entry", "string"),
summary: call("fc_web_partdesign_inspect_summary", "string"),
sourceFiles: call("fc_web_partdesign_inspect_source_files", "string"),
registeredTypes: call("fc_web_partdesign_inspect_registered_types"),
formatCount: call("fc_web_partdesign_inspect_format_count"),
pythonBindings: call("fc_web_partdesign_inspect_python_bindings"),
resourcesReleased: call("fc_web_partdesign_inspect_resources_released"),
};
const inspectInvalid = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], ["src/Mod/PartDesign/App/Missing.cpp"]);
const inspectInvalidError = call("fc_web_partdesign_error_code", "string");
const inspectEmpty = call("fc_web_partdesign_source_inventory_validate", "number", ["string"], [""]);
const inspectEmptyError = call("fc_web_partdesign_error_code", "string");
result.inspect = { first: inspectFirst, second: inspectSecond, invalid: inspectInvalid, invalidError: inspectInvalidError, empty: inspectEmpty, emptyError: inspectEmptyError };
const actionPass = result.action.skipped === true || (result.action.run === 1 && result.action.error === "" && Math.abs(result.action.volume - 24) <= 1e-9 && result.action.recomputeCount > 0 && result.action.objectCount === 2 && result.action.documentsAfter === 0);
const recoveryPass = result.recovery.skipped === true || (result.recovery.run === 1 && result.recovery.invalidReturn === 0 && result.recovery.afterReturn === 1 && result.recovery.error.includes("Length of box too small") && Math.abs(result.recovery.beforeVolume - result.recovery.invalidVolume) <= 1e-9 && Math.abs(result.recovery.afterVolume - 24) <= 1e-9);
if (result.valid !== 1 || result.repeat !== 1 || result.type !== 1 || result.typeName !== "PartDesign::AdditiveBox" || result.unknownType !== 0 || result.unknownTypeError !== "type_not_found" || result.gui !== 0 || result.guiError !== "gui_dependency_unavailable" || result.unknownModule !== 0 || result.unknownModuleError !== "module_unavailable" || result.empty !== 0 || result.emptyError !== "invalid_argument" || result.recovered !== 1 || result.recoveredTypeName !== "PartDesign::Body" || result.initializationRuns !== 1 || result.registeredTypes !== 9 || !actionPass || !recoveryPass || inspectFirst.run !== 1 || JSON.stringify(inspectFirst) !== JSON.stringify(inspectSecond) || inspectFirst.registeredTypes !== 8 || inspectFirst.formatCount !== 0 || inspectFirst.pythonBindings !== 2 || inspectFirst.resourcesReleased !== 1 || inspectInvalid !== 0 || inspectInvalidError !== "revision_mismatch" || inspectEmpty !== 0 || inspectEmptyError !== "invalid_argument") {
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 @@
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_create_recompute"),error:c("fc_web_points_action_error","string"),count:c("fc_web_points_action_count"),documentsAfter:c("fc_web_points_action_documents_after"),min:[c("fc_web_points_action_min_x"),c("fc_web_points_action_min_y"),c("fc_web_points_action_min_z")],max:[c("fc_web_points_action_max_x"),c("fc_web_points_action_max_y"),c("fc_web_points_action_max_z")]};if(r.run!==1||r.error!==""||r.count!==3||r.documentsAfter!==0||JSON.stringify(r.min)!==JSON.stringify([-1,0,0])||JSON.stringify(r.max)!==JSON.stringify([1,4,3]))throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1 @@
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_save_reopen"),error:c("fc_web_points_persistence_error","string"),path:c("fc_web_points_persistence_path","string"),type:c("fc_web_points_persistence_type","string"),count:c("fc_web_points_persistence_count"),size:c("fc_web_points_persistence_size"),beforeMinX:c("fc_web_points_persistence_before_min_x"),afterMinX:c("fc_web_points_persistence_after_min_x")};if(r.run!==1||r.error!==""||r.type!=="Points::Feature"||r.count!==3||r.size<=0||r.beforeMinX!==r.afterMinX)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1 @@
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const r={run:c("fc_web_points_recovery"),error:c("fc_web_points_recovery_error","string"),validBefore:c("fc_web_points_recovery_valid_before"),validInvalid:c("fc_web_points_recovery_valid_invalid"),validAfter:c("fc_web_points_recovery_valid_after"),beforeMinX:c("fc_web_points_recovery_before_min_x"),afterMinX:c("fc_web_points_recovery_after_min_x")};if(r.run!==1||r.error!==""||r.validBefore!==3||r.validInvalid!==2||r.validAfter!==3||r.beforeMinX!==r.afterMinX)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1 @@
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url"); const build=path.resolve(process.argv[2]||"build-web/configure/bin"); async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebPointsRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebPointsRuntimeProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const valid=c("fc_web_points_initialize","number",["string"],["Points"]),repeat=c("fc_web_points_initialize","number",["string"],["Points"]),type=c("fc_web_points_type_probe","number",["string"],["Points::Feature"]),name=c("fc_web_points_last_type","string"),unknown=c("fc_web_points_type_probe","number",["string"],["Points::Missing"]),unknownError=c("fc_web_points_error_code","string"),gui=c("fc_web_points_initialize","number",["string"],["PointsGui"]),guiError=c("fc_web_points_error_code","string"),recovered=c("fc_web_points_type_probe","number",["string"],["Points::Structured"]),recoveredName=c("fc_web_points_last_type","string"),r={valid,repeat,type,name,unknown,unknownError,gui,guiError,recovered,recoveredName,calls:c("fc_web_points_initialization_calls"),runs:c("fc_web_points_initialization_runs"),registered:c("fc_web_points_registered_types")};if(r.valid!==1||r.repeat!==1||r.type!==1||r.name!=="Points::Feature"||r.unknown!==0||r.unknownError!=="type_not_found"||r.gui!==0||r.guiError!=="gui_dependency_unavailable"||r.recovered!==1||r.recoveredName!=="Points::Structured"||r.runs!==1||r.registered!==6)throw Error(JSON.stringify(r));console.log(JSON.stringify(r));}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() {
globalThis.__dirname = `${build}${path.sep}`;
globalThis.require = require;
const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js")));
const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) });
const call = (name, type = "number") => probe.ccall(name, type);
const run = call("fc_web_sandbox_source_inventory");
const result = {
run,
errorCode: call("fc_web_sandbox_inventory_error_code", "string"),
errorMessage: call("fc_web_sandbox_inventory_error_message", "string"),
sourceEntry: call("fc_web_sandbox_inventory_source_entry", "string"),
registeredTypes: call("fc_web_sandbox_inventory_registered_types"),
pythonBindings: call("fc_web_sandbox_inventory_python_bindings"),
readOnly: call("fc_web_sandbox_inventory_read_only"),
resourcesReleased: call("fc_web_sandbox_inventory_resources_released"),
};
if (result.run !== 1 || result.errorCode !== "" || result.sourceEntry !== "src/Mod/Sandbox/App/AppSandbox.cpp" || result.registeredTypes !== 1 || result.pythonBindings !== 3 || result.readOnly !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result));
console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", ...result }));
}
main().catch((error) => { console.error(error); process.exitCode = 1; });

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() {
globalThis.__dirname = `${build}${path.sep}`;
globalThis.require = require;
const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js")));
const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) });
const call = (name, type = "number", argTypes = [], args = []) => probe.ccall(name, type, argTypes, args);
const valid = "src/Mod/Sandbox/App/AppSandbox.cpp";
const snapshot = () => ({ sourceEntry: call("fc_web_sandbox_inventory_source_entry", "string"), registeredTypes: call("fc_web_sandbox_inventory_registered_types"), pythonBindings: call("fc_web_sandbox_inventory_python_bindings"), readOnly: call("fc_web_sandbox_inventory_read_only"), resourcesReleased: call("fc_web_sandbox_inventory_resources_released") });
if (call("fc_web_sandbox_source_inventory") !== 1) throw new Error("inventory baseline failed");
const before = snapshot();
const reject = () => { const run = call("fc_web_sandbox_source_inventory_validate", "number", ["string"], ["src/Mod/Sandbox/Missing.cpp"]); return { run, code: call("fc_web_sandbox_inventory_validation_error_code", "string"), message: call("fc_web_sandbox_inventory_validation_error_message", "string") }; };
const firstReject = reject();
const secondReject = reject();
const afterReject = snapshot();
if (call("fc_web_sandbox_source_inventory_validate", "number", ["string"], [valid]) !== 1) throw new Error("inventory recovery failed");
const afterRecovery = snapshot();
const result = { before, firstReject, secondReject, afterReject, afterRecovery, unchangedAfterReject: JSON.stringify(before) === JSON.stringify(afterReject), repeatedErrorStable: JSON.stringify(firstReject) === JSON.stringify(secondReject), recoveredStateEqualsBefore: JSON.stringify(before) === JSON.stringify(afterRecovery) };
if (firstReject.run !== 0 || firstReject.code !== "revision_mismatch" || !result.unchangedAfterReject || !result.repeatedErrorStable || !result.recoveredStateEqualsBefore) throw new Error(JSON.stringify(result));
console.log(JSON.stringify({ result: "pass", action: "inspect/source-inventory", ...result }));
}
main().catch((error) => { console.error(error); process.exitCode = 1; });

View File

@@ -0,0 +1,6 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs = require("fs"); const path = require("path"); const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() { globalThis.__dirname = `${build}${path.sep}`; globalThis.require = require; const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js"))); const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) }); const call = (name, type = "number") => probe.ccall(name, type); const run = call("fc_web_sandbox_python_api"); const result = { run, errorCode: call("fc_web_sandbox_python_api_error_code", "string"), errorMessage: call("fc_web_sandbox_python_api_error_message", "string"), sourceEntry: call("fc_web_sandbox_python_api_source_entry", "string"), apiCount: call("fc_web_sandbox_python_api_count"), readOnly: call("fc_web_sandbox_python_api_read_only"), resourcesReleased: call("fc_web_sandbox_python_api_resources_released") }; if (run !== 1 || result.errorCode !== "" || result.sourceEntry !== "src/Mod/Sandbox/App/AppSandbox.cpp" || result.apiCount !== 3 || result.readOnly !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result)); console.log(JSON.stringify({ result: "pass", action: "python-api", ...result })); }
main().catch((error) => { console.error(error); process.exitCode = 1; });

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSandboxRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSandboxRuntimeProbe.wasm"))});const c=(n,t="number",a=[],v=[])=>p.ccall(n,t,a,v);const valid="src/Mod/Sandbox/App/AppSandbox.cpp";const snap=()=>({sourceEntry:c("fc_web_sandbox_python_api_source_entry","string"),apiCount:c("fc_web_sandbox_python_api_count"),readOnly:c("fc_web_sandbox_python_api_read_only"),resourcesReleased:c("fc_web_sandbox_python_api_resources_released")});if(c("fc_web_sandbox_python_api")!==1)throw Error("python baseline failed");const before=snap();const reject=()=>({run:c("fc_web_sandbox_python_api_validate","number",["string"],["src/Mod/Sandbox/Missing.cpp"]),code:c("fc_web_sandbox_python_api_validation_error_code","string"),message:c("fc_web_sandbox_python_api_validation_error_message","string")});const first=reject(),second=reject(),afterReject=snap();if(c("fc_web_sandbox_python_api_validate","number",["string"],[valid])!==1)throw Error("python recovery failed");const afterRecovery=snap();const result={before,firstReject:first,secondReject:second,afterReject,afterRecovery,unchangedAfterReject:JSON.stringify(before)===JSON.stringify(afterReject),repeatedErrorStable:JSON.stringify(first)===JSON.stringify(second),recoveredStateEqualsBefore:JSON.stringify(before)===JSON.stringify(afterRecovery)};if(first.run!==0||first.code!=="revision_mismatch"||!result.unchangedAfterReject||!result.repeatedErrorStable||!result.recoveredStateEqualsBefore)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"python-api",...result}))}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSandboxRuntimeProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSandboxRuntimeProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sandbox_persistence_recovery");const result={run,errorCode:c("fc_web_sandbox_persistence_recovery_error_code","string"),errorMessage:c("fc_web_sandbox_persistence_recovery_error_message","string"),before:c("fc_web_sandbox_persistence_recovery_before","string"),afterReject:c("fc_web_sandbox_persistence_recovery_after_reject","string"),afterRecovery:c("fc_web_sandbox_persistence_recovery_after_recovery","string"),invalidReturn:c("fc_web_sandbox_persistence_recovery_invalid_return"),unchanged:c("fc_web_sandbox_persistence_recovery_unchanged"),replayEqual:c("fc_web_sandbox_persistence_recovery_replay_equal"),resourcesReleased:c("fc_web_sandbox_persistence_recovery_resources_released")};if(run!==1||result.errorCode!=="missing_resource"||result.invalidReturn!==0||result.unchanged!==1||result.replayEqual!==1||result.before!==result.afterReject||result.before!==result.afterRecovery||result.resourcesReleased!==1)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}))}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const build = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main() {
globalThis.__dirname = `${build}${path.sep}`;
globalThis.require = require;
const module = await import(pathToFileURL(path.join(build, "FreeCADWebSandboxRuntimeProbe.js")));
const probe = await module.default({ wasmBinary: fs.readFileSync(path.join(build, "FreeCADWebSandboxRuntimeProbe.wasm")) });
const call = (name, type = "number") => probe.ccall(name, type);
const run = call("fc_web_sandbox_save_reopen");
const result = {
run,
errorCode: call("fc_web_sandbox_persistence_error_code", "string"),
errorMessage: call("fc_web_sandbox_persistence_error_message", "string"),
path: call("fc_web_sandbox_persistence_path", "string"),
objectType: call("fc_web_sandbox_persistence_object_type", "string"),
objectInteger: call("fc_web_sandbox_persistence_object_integer"),
objectCount: call("fc_web_sandbox_persistence_object_count"),
size: call("fc_web_sandbox_persistence_size"),
roundTrip: call("fc_web_sandbox_persistence_round_trip"),
resourcesReleased: call("fc_web_sandbox_persistence_resources_released"),
};
if (result.run !== 1 || result.errorCode !== "" || result.path !== "/tmp/SandboxWeb.FCStd" || result.objectType !== "Sandbox::SandboxObject" || result.objectInteger !== 42 || result.objectCount !== 1 || result.size <= 0 || result.roundTrip !== 1 || result.resourcesReleased !== 1) throw new Error(JSON.stringify(result));
console.log(JSON.stringify({ result: "pass", action: "save/reopen", ...result }));
}
main().catch((error) => { console.error(error); process.exitCode = 1; });

View File

@@ -0,0 +1,121 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const buildDirectory = path.resolve(process.argv[2] || "build-web/configure/bin");
async function main()
{
globalThis.__dirname = `${buildDirectory}${path.sep}`;
globalThis.require = require;
const moduleUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebSandboxRuntimeProbe.js")).href;
const factory = await import(moduleUrl);
const probe = await factory.default({
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebSandboxRuntimeProbe.wasm")),
});
const call = (name, returnType = "number", argTypes = [], args = []) => probe.ccall(name, returnType, argTypes, args);
const error = () => ({
code: call("fc_web_sandbox_error_code", "string"),
message: call("fc_web_sandbox_error_message", "string"),
});
const result = {};
result.initialize = call("fc_web_sandbox_initialize", "number", ["string"], ["Sandbox"]);
result.type = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::SandboxObject"]);
result.typeName = call("fc_web_sandbox_last_type", "string");
result.moduleVisible = call("fc_web_sandbox_module_visible");
result.stateAfterValid = {
calls: call("fc_web_sandbox_initialization_calls"),
runs: call("fc_web_sandbox_initialization_runs"),
initialized: call("fc_web_sandbox_initialized"),
};
result.repeatInitialize = call("fc_web_sandbox_initialize", "number", ["string"], ["Sandbox"]);
result.repeatType = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::SandboxObject"]);
result.stateAfterRepeat = {
calls: call("fc_web_sandbox_initialization_calls"),
runs: call("fc_web_sandbox_initialization_runs"),
initialized: call("fc_web_sandbox_initialized"),
};
result.createRecompute = call("fc_web_sandbox_create_recompute");
result.action = {
errorCode: call("fc_web_sandbox_action_error_code", "string"),
errorMessage: call("fc_web_sandbox_action_error_message", "string"),
objectType: call("fc_web_sandbox_action_object_type", "string"),
objectInteger: call("fc_web_sandbox_action_object_integer"),
objectCount: call("fc_web_sandbox_action_object_count"),
recomputeCount: call("fc_web_sandbox_action_recompute_count"),
resourcesReleased: call("fc_web_sandbox_action_resources_released"),
};
result.recovery = call("fc_web_sandbox_recovery");
result.recoveryState = {
errorCode: call("fc_web_sandbox_recovery_error_code", "string"),
errorMessage: call("fc_web_sandbox_recovery_error_message", "string"),
before: call("fc_web_sandbox_recovery_before", "string"),
afterReject: call("fc_web_sandbox_recovery_after_reject", "string"),
afterRecovery: call("fc_web_sandbox_recovery_after_recovery", "string"),
invalidReturn: call("fc_web_sandbox_recovery_invalid_return"),
unchanged: call("fc_web_sandbox_recovery_unchanged"),
replayEqual: call("fc_web_sandbox_recovery_replay_equal"),
resourcesReleased: call("fc_web_sandbox_recovery_resources_released"),
};
result.unknownModule = call("fc_web_sandbox_initialize", "number", ["string"], ["Missing"]);
result.unknownModuleError = error();
result.guiModule = call("fc_web_sandbox_initialize", "number", ["string"], ["SandboxGui"]);
result.guiModuleError = error();
result.emptyModule = call("fc_web_sandbox_initialize", "number", ["string"], [""]);
result.emptyModuleError = error();
result.unknownType = call("fc_web_sandbox_type_probe", "number", ["string"], ["Sandbox::Missing"]);
result.unknownTypeError = error();
result.stateAfterErrors = {
calls: call("fc_web_sandbox_initialization_calls"),
runs: call("fc_web_sandbox_initialization_runs"),
initialized: call("fc_web_sandbox_initialized"),
};
const valid = result.initialize === 1
&& result.type === 1
&& result.typeName === "Sandbox::SandboxObject"
&& result.moduleVisible === 1
&& result.stateAfterValid.runs === 1
&& result.stateAfterValid.initialized === 1
&& result.repeatInitialize === 1
&& result.repeatType === 1
&& result.stateAfterRepeat.runs === 1
&& result.createRecompute === 1
&& result.action.errorCode === ""
&& result.action.objectType === "Sandbox::SandboxObject"
&& result.action.objectInteger === 42
&& result.action.objectCount === 1
&& result.action.recomputeCount >= 0
&& result.action.resourcesReleased === 1
&& result.recovery === 1
&& result.recoveryState.errorCode === "type_not_found"
&& result.recoveryState.errorMessage === "objectType: Sandbox::Missing"
&& result.recoveryState.before === result.recoveryState.afterReject
&& result.recoveryState.before === result.recoveryState.afterRecovery
&& result.recoveryState.invalidReturn === 0
&& result.recoveryState.unchanged === 1
&& result.recoveryState.replayEqual === 1
&& result.recoveryState.resourcesReleased === 1
&& 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.unknownType === 0
&& result.unknownTypeError.code === "type_not_found"
&& result.stateAfterErrors.runs === 1
&& result.stateAfterErrors.initialized === 1;
if (!valid) {
throw new Error(JSON.stringify(result));
}
console.log(JSON.stringify(result));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSketcherProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSketcherProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sketcher_save_reopen"),result={run,error:c("fc_web_sketcher_persistence_error","string"),path:c("fc_web_sketcher_persistence_path","string"),size:c("fc_web_sketcher_persistence_size"),geometryCount:c("fc_web_sketcher_persistence_geometry_count"),constraintCount:c("fc_web_sketcher_persistence_constraint_count"),roundTrip:c("fc_web_sketcher_persistence_round_trip"),resourcesReleased:c("fc_web_sketcher_persistence_resources_released")};if(run!==1||result.error!==""||result.size<=0||result.geometryCount!==1||result.constraintCount!==1||result.roundTrip!==1||result.resourcesReleased!==1)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"save/reopen",...result}))}main().catch(e=>{console.error(e);process.exitCode=1});

View File

@@ -0,0 +1,2 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
const fs=require("fs"),path=require("path"),{pathToFileURL}=require("url");const build=path.resolve(process.argv[2]||"build-web/configure/bin");async function main(){globalThis.__dirname=`${build}${path.sep}`;globalThis.require=require;const m=await import(pathToFileURL(path.join(build,"FreeCADWebSketcherProbe.js")));const p=await m.default({wasmBinary:fs.readFileSync(path.join(build,"FreeCADWebSketcherProbe.wasm"))});const c=(n,t="number")=>p.ccall(n,t);const run=c("fc_web_sketcher_python_api_probe"),result={run,error:c("fc_web_sketcher_last_error","string"),apiCount:c("fc_web_sketcher_python_api_count")};if(run!==1||result.error!==""||result.apiCount!==6)throw Error(JSON.stringify(result));console.log(JSON.stringify({result:"pass",action:"python-api",...result}))}main().catch(e=>{console.error(e);process.exitCode=1});