Restore GUI-free Web module runtimes
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
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
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
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (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:
@@ -61,6 +61,8 @@ option(FREECAD_WEB_ENABLE_INSPECTION "Enable the Inspection App Web probe and it
|
||||
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)
|
||||
option(FREECAD_WEB_ENABLE_REVERSEENGINEERING "Enable the GUI-free ReverseEngineering App Web probe" OFF)
|
||||
option(FREECAD_WEB_ENABLE_ROBOT "Enable the GUI-free Robot App Web probe" OFF)
|
||||
if(FREECAD_WEB)
|
||||
if(NOT EMSCRIPTEN OR NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||
message(FATAL_ERROR
|
||||
@@ -139,6 +141,12 @@ if(FREECAD_WEB)
|
||||
# is linked without SandboxGui. Mesh is its direct App dependency.
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_SANDBOX BUILD_MESH)
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_REVERSEENGINEERING)
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_REVERSEENGINEERING)
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_ROBOT)
|
||||
list(REMOVE_ITEM _freecad_web_disabled_options BUILD_ROBOT)
|
||||
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()
|
||||
@@ -180,6 +188,12 @@ if(FREECAD_WEB)
|
||||
set(${_freecad_web_sandbox_option} ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_SANDBOX" FORCE)
|
||||
endforeach()
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_REVERSEENGINEERING)
|
||||
set(BUILD_REVERSEENGINEERING ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_REVERSEENGINEERING" FORCE)
|
||||
endif()
|
||||
if(FREECAD_WEB_ENABLE_ROBOT)
|
||||
set(BUILD_ROBOT ON CACHE BOOL "Enabled by FREECAD_WEB_ENABLE_ROBOT" FORCE)
|
||||
endif()
|
||||
|
||||
include(cMake/FreeCADWebDependencies.cmake)
|
||||
endif()
|
||||
|
||||
@@ -104,8 +104,20 @@ using namespace zipios;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace App
|
||||
{
|
||||
namespace App
|
||||
{
|
||||
|
||||
static std::string webRestoreStage;
|
||||
|
||||
extern "C" const char* fc_web_document_restore_stage()
|
||||
{
|
||||
return webRestoreStage.c_str();
|
||||
}
|
||||
|
||||
extern "C" void fc_web_document_restore_property(const char* name)
|
||||
{
|
||||
webRestoreStage = std::string("property:") + (name ? name : "<null>");
|
||||
}
|
||||
|
||||
static bool globalIsRestoring;
|
||||
static bool globalIsRelabeling;
|
||||
@@ -1118,14 +1130,16 @@ void Document::Save(Base::Writer& writer) const
|
||||
writer.Stream() << "</Document>" << '\n';
|
||||
}
|
||||
|
||||
void Document::Restore(Base::XMLReader& reader)
|
||||
{
|
||||
void Document::Restore(Base::XMLReader& reader)
|
||||
{
|
||||
webRestoreStage = "document-start";
|
||||
d->hashers.clear();
|
||||
d->touchedObjs.clear();
|
||||
addStringHasher(d->Hasher);
|
||||
setStatus(Document::PartialDoc, false);
|
||||
|
||||
reader.readElement("Document");
|
||||
webRestoreStage = "document-header";
|
||||
reader.readElement("Document");
|
||||
const long scheme = reader.getAttribute<long>("SchemaVersion");
|
||||
reader.DocumentSchema = static_cast<int>(scheme);
|
||||
if (reader.hasAttribute("ProgramVersion")) {
|
||||
@@ -1161,6 +1175,7 @@ void Document::Restore(Base::XMLReader& reader)
|
||||
// read the Document Properties, when reading in Uid the transient directory gets renamed
|
||||
// automatically
|
||||
try {
|
||||
webRestoreStage = "document-properties";
|
||||
PropertyContainer::Restore(reader);
|
||||
}
|
||||
catch (const std::out_of_range& error) {
|
||||
@@ -1207,6 +1222,7 @@ void Document::Restore(Base::XMLReader& reader)
|
||||
else if (scheme >= 3) {
|
||||
// read the feature types
|
||||
try {
|
||||
webRestoreStage = "objects";
|
||||
readObjects(reader);
|
||||
}
|
||||
catch (const std::out_of_range& error) {
|
||||
@@ -1215,11 +1231,14 @@ void Document::Restore(Base::XMLReader& reader)
|
||||
|
||||
// tip object handling. First the whole document has to be read, then we
|
||||
// can restore the Tip link out of the TipName Property:
|
||||
Tip.setValue(getObject(TipName.getValue()));
|
||||
webRestoreStage = "tip";
|
||||
Tip.setValue(getObject(TipName.getValue()));
|
||||
}
|
||||
|
||||
reader.readEndElement("Document");
|
||||
}
|
||||
webRestoreStage = "document-end";
|
||||
reader.readEndElement("Document");
|
||||
webRestoreStage = "document-complete";
|
||||
}
|
||||
|
||||
void DocumentP::checkStringHasher(const Base::XMLReader& reader)
|
||||
{
|
||||
@@ -1544,7 +1563,8 @@ std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader)
|
||||
|
||||
|
||||
// read the object types
|
||||
reader.readElement("Objects");
|
||||
webRestoreStage = "objects-header";
|
||||
reader.readElement("Objects");
|
||||
int Cnt = static_cast<int>(reader.getAttribute<long>("Count"));
|
||||
|
||||
if (!reader.hasAttribute(fcAttrDependencies)) {
|
||||
@@ -1598,6 +1618,7 @@ std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader)
|
||||
|
||||
long lastId = 0;
|
||||
for (int i = 0; i < Cnt; i++) {
|
||||
webRestoreStage = "object-declaration";
|
||||
reader.readElement("Object");
|
||||
std::string type = reader.getAttribute<const char*>("type");
|
||||
std::string name = reader.getAttribute<const char*>("name");
|
||||
@@ -1686,20 +1707,23 @@ std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader)
|
||||
|
||||
// read the features itself
|
||||
reader.clearPartialRestoreDocumentObject();
|
||||
reader.readElement("ObjectData");
|
||||
webRestoreStage = "object-data-header";
|
||||
reader.readElement("ObjectData");
|
||||
Cnt = static_cast<int>(reader.getAttribute<long>("Count"));
|
||||
for (int i = 0; i < Cnt; i++) {
|
||||
reader.readElement("Object");
|
||||
webRestoreStage = "object-data-entry";
|
||||
reader.readElement("Object");
|
||||
std::string name = reader.getName(reader.getAttribute<const char*>("name"));
|
||||
if (DocumentObject* pObj = getObject(name.c_str()); pObj
|
||||
&& !pObj->testStatus(
|
||||
PartialObject)) { // check if this feature has been registered
|
||||
pObj->setStatus(ObjectStatus::Restore, true);
|
||||
try {
|
||||
FC_TRACE("restoring " << pObj->getFullName());
|
||||
pObj->Restore(reader);
|
||||
}
|
||||
// Try to continue only for certain exception types if not handled
|
||||
try {
|
||||
FC_TRACE("restoring " << pObj->getFullName());
|
||||
webRestoreStage = std::string("object-properties:") + name;
|
||||
pObj->Restore(reader);
|
||||
}
|
||||
// Try to continue only for certain exception types if not handled
|
||||
// by the feature type. For all other exception types abort the process.
|
||||
catch (const Base::UnicodeError& e) {
|
||||
e.reportException();
|
||||
@@ -1726,9 +1750,11 @@ std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader)
|
||||
reader.clearPartialRestoreDocumentObject();
|
||||
}
|
||||
}
|
||||
reader.readEndElement("Object");
|
||||
webRestoreStage = "object-data-end";
|
||||
reader.readEndElement("Object");
|
||||
}
|
||||
reader.readEndElement("ObjectData");
|
||||
webRestoreStage = "object-data-complete";
|
||||
reader.readEndElement("ObjectData");
|
||||
|
||||
return objs;
|
||||
}
|
||||
@@ -2146,7 +2172,7 @@ void Document::restore(const char* filename,
|
||||
if (file.gcount() != static_cast<std::streamsize>(archive.size())) {
|
||||
throw Base::FileException("Unable to read project file", filename);
|
||||
}
|
||||
std::istringstream archiveStream(std::move(archive), std::ios::in | std::ios::binary);
|
||||
std::istringstream archiveStream(archive, std::ios::in | std::ios::binary);
|
||||
std::unique_ptr<zipios::ZipInputStream> zipstreamPtr;
|
||||
try {
|
||||
zipstreamPtr = std::make_unique<zipios::ZipInputStream>(archiveStream);
|
||||
@@ -2191,10 +2217,10 @@ void Document::restore(const char* filename,
|
||||
catch (const std::exception& e) {
|
||||
throw Base::RuntimeError(std::string("Document::Restore: ") + e.what());
|
||||
}
|
||||
catch (const Base::Exception& e) {
|
||||
Base::Console().error("Invalid Document.xml: %s\n", e.what());
|
||||
setStatus(Document::RestoreError, true);
|
||||
}
|
||||
catch (const Base::Exception& e) {
|
||||
Base::Console().error("Invalid Document.xml: %s\n", e.what());
|
||||
setStatus(Document::RestoreError, true);
|
||||
}
|
||||
|
||||
d->partialLoadObjects.clear();
|
||||
d->programVersion = reader.ProgramVersion;
|
||||
@@ -2203,22 +2229,40 @@ void Document::restore(const char* filename,
|
||||
// exist, what is done in Restore().
|
||||
// Note: This file doesn't need to be available if the document has been created
|
||||
// without GUI. But if available then follow after all data files of the App document.
|
||||
webRestoreStage = "signal-restore";
|
||||
try {
|
||||
signalRestoreDocument(reader);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
throw Base::RuntimeError(std::string("signalRestoreDocument: ") + e.what());
|
||||
}
|
||||
webRestoreStage = "document-files";
|
||||
try {
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
archiveStream.clear();
|
||||
// Keep the original stream positioned by the XML reader. The Web
|
||||
// reader normalizes its exception state before probing auxiliary data.
|
||||
zipstream.exceptions(std::ios::goodbit);
|
||||
zipstream.clear(std::ios::goodbit);
|
||||
#else
|
||||
file.clear();
|
||||
#endif
|
||||
webRestoreStage = "document-files-read";
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
reader.readFiles(zipstream);
|
||||
#else
|
||||
reader.readFiles(zipstream);
|
||||
#endif
|
||||
webRestoreStage = "document-files-complete";
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
throw Base::RuntimeError(std::string("Document data files: ") + e.what());
|
||||
std::string streamState;
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
streamState = " [archive=" + std::to_string(archiveStream.rdstate());
|
||||
#else
|
||||
streamState = " [archive=file";
|
||||
#endif
|
||||
streamState += ", zip=" + std::to_string(zipstream.rdstate()) + "]";
|
||||
throw Base::RuntimeError(std::string("Document data files: ") + e.what() + streamState);
|
||||
}
|
||||
|
||||
DocumentP::checkStringHasher(reader);
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
#include "Property.h"
|
||||
#include "PropertyContainer.h"
|
||||
|
||||
extern "C" void fc_web_document_restore_property(const char* name);
|
||||
|
||||
|
||||
FC_LOG_LEVEL_INIT("App",true,true)
|
||||
|
||||
@@ -361,6 +363,7 @@ void PropertyContainer::Restore(Base::XMLReader &reader)
|
||||
&& !status.test(Property::PropTransient)
|
||||
&& !prop->testStatus(Property::PropTransient))
|
||||
{
|
||||
fc_web_document_restore_property(prop->getName());
|
||||
FC_TRACE("restore property '" << prop->getName() << "'");
|
||||
prop->Restore(reader);
|
||||
}else
|
||||
|
||||
@@ -49,8 +49,12 @@
|
||||
#include <zipios++/zipinputstream.h>
|
||||
#include <boost/iostreams/filtering_stream.hpp>
|
||||
|
||||
using namespace std;
|
||||
using namespace XERCES_CPP_NAMESPACE;
|
||||
using namespace std;
|
||||
using namespace XERCES_CPP_NAMESPACE;
|
||||
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
extern "C" void fc_web_document_restore_property(const char* name);
|
||||
#endif
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -447,6 +451,14 @@ void Base::XMLReader::readFiles(zipios::ZipInputStream& zipstream) const
|
||||
if (FileList.empty()) {
|
||||
return;
|
||||
}
|
||||
// ZipInputStream is reused after XMLReader consumed the Document.xml
|
||||
// entry. Desktop streams can be normalized before advancing. Emscripten's
|
||||
// zipios stream may throw from clear() while the local entry is still
|
||||
// valid, so the Web caller provides a fresh archive stream instead.
|
||||
#ifndef FC_OS_EMSCRIPTEN
|
||||
zipstream.exceptions(std::ios::goodbit);
|
||||
zipstream.clear();
|
||||
#endif
|
||||
zipios::ConstEntryPointer entry;
|
||||
try {
|
||||
entry = zipstream.getNextEntry();
|
||||
@@ -457,8 +469,11 @@ void Base::XMLReader::readFiles(zipios::ZipInputStream& zipstream) const
|
||||
return;
|
||||
}
|
||||
std::vector<FileEntry>::const_iterator it = FileList.begin();
|
||||
Base::SequencerLauncher seq("Importing project files...", FileList.size());
|
||||
while (entry->isValid() && it != FileList.end()) {
|
||||
Base::SequencerLauncher seq("Importing project files...", FileList.size());
|
||||
while (entry->isValid() && it != FileList.end()) {
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
fc_web_document_restore_property((std::string("embedded:") + entry->getName()).c_str());
|
||||
#endif
|
||||
std::vector<FileEntry>::const_iterator jt = it;
|
||||
// Check if the current entry is registered, otherwise check the next registered files as
|
||||
// soon as both file names match
|
||||
@@ -486,19 +501,37 @@ void Base::XMLReader::readFiles(zipios::ZipInputStream& zipstream) const
|
||||
entry->toString().c_str()
|
||||
);
|
||||
FailedFiles.push_back(jt->FileName);
|
||||
}
|
||||
}
|
||||
// Go to the next registered file name
|
||||
it = jt + 1;
|
||||
if (it == FileList.end()) {
|
||||
break;
|
||||
}
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
// Property restore may legitimately reach EOF inside the current
|
||||
// BREP entry. Reset the zip stream before probing the next local
|
||||
// header; zipios otherwise raises ios_base::clear on Web.
|
||||
try {
|
||||
zipstream.exceptions(std::ios::goodbit);
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
try {
|
||||
zipstream.clear(std::ios::goodbit);
|
||||
}
|
||||
catch (...) {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
seq.next();
|
||||
|
||||
// In either case we must go to the next entry
|
||||
try {
|
||||
entry = zipstream.getNextEntry();
|
||||
try {
|
||||
#ifdef FC_OS_EMSCRIPTEN
|
||||
fc_web_document_restore_property("embedded-next");
|
||||
#endif
|
||||
entry = zipstream.getNextEntry();
|
||||
}
|
||||
catch (const std::exception&) {
|
||||
// there is no further entry
|
||||
|
||||
@@ -70,9 +70,9 @@ if(BUILD_CAM)
|
||||
add_subdirectory(CAM)
|
||||
endif(BUILD_CAM)
|
||||
|
||||
if(BUILD_PLOT)
|
||||
if(BUILD_PLOT OR FREECAD_WEB)
|
||||
add_subdirectory(Plot)
|
||||
endif(BUILD_PLOT)
|
||||
endif()
|
||||
|
||||
if(BUILD_POINTS)
|
||||
add_subdirectory(Points)
|
||||
@@ -80,7 +80,7 @@ endif(BUILD_POINTS)
|
||||
|
||||
if(BUILD_REVERSEENGINEERING)
|
||||
add_subdirectory(ReverseEngineering)
|
||||
endif(BUILD_REVERSEENGINEERING)
|
||||
endif()
|
||||
|
||||
if(BUILD_ROBOT)
|
||||
if(EIGEN3_FOUND)
|
||||
|
||||
@@ -96,14 +96,14 @@ void PropertyMaterial::Restore(Base::XMLReader& reader)
|
||||
|
||||
const auto materialUuid = QString::fromLatin1(uuid);
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
try {
|
||||
setValue(*MaterialManager::getManager().getMaterial(materialUuid));
|
||||
}
|
||||
catch (const MaterialNotFound&) {
|
||||
Material placeholder;
|
||||
placeholder.setUUID(materialUuid);
|
||||
setValue(placeholder);
|
||||
}
|
||||
// The Web core does not embed the GUI material/model libraries. Looking up
|
||||
// a persisted UUID would initialize those registries and can fail before
|
||||
// MaterialNotFound is translated, so retain the UUID on a standalone
|
||||
// default material instead.
|
||||
Material fallback;
|
||||
fallback.setUUID(materialUuid);
|
||||
fallback.setName(QStringLiteral("Web default material"));
|
||||
setValue(fallback);
|
||||
#else
|
||||
setValue(*MaterialManager::getManager().getMaterial(materialUuid));
|
||||
#endif
|
||||
|
||||
@@ -47,7 +47,11 @@
|
||||
|
||||
#include "PartFeature.h"
|
||||
#include "PartPyCXX.h"
|
||||
#include "PropertyTopoShape.h"
|
||||
#include "PropertyTopoShape.h"
|
||||
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
extern "C" void fc_web_document_restore_property(const char* name);
|
||||
#endif
|
||||
#include "TopoShapePy.h"
|
||||
#include "PartFeature.h"
|
||||
|
||||
@@ -687,6 +691,9 @@ void PropertyPartShape::loadFromFile(Base::Reader& reader)
|
||||
|
||||
void PropertyPartShape::loadFromStream(Base::Reader& reader)
|
||||
{
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property((std::string("brep-stream:") + reader.getFileName()).c_str());
|
||||
#endif
|
||||
// Save locale before calling OCCT. TopTools_ShapeSet::Read imbues the stream
|
||||
// with std::locale::classic() and restores it on return, but uses a non-RAII
|
||||
// pattern. When exceptions propagate out (due to the exception mask below),
|
||||
@@ -698,15 +705,51 @@ void PropertyPartShape::loadFromStream(Base::Reader& reader)
|
||||
// to the binary entry. Clear that state before enabling strict BRep
|
||||
// read exceptions; basic_ios::exceptions() throws immediately when
|
||||
// the current state intersects the requested mask.
|
||||
// clear() itself may throw when an EOF/fail state is already present
|
||||
// under the caller's exception mask (notably for an empty optional
|
||||
// PartDesign suppressed shape). Disable the mask before normalizing
|
||||
// the state, then enable strict BREP diagnostics.
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-mask-off");
|
||||
#endif
|
||||
reader.exceptions(std::ios::goodbit);
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-clear-before-read");
|
||||
#endif
|
||||
reader.clear();
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-mask-strict");
|
||||
#endif
|
||||
#ifndef FREECAD_WEB_CORE
|
||||
reader.exceptions(std::istream::failbit | std::istream::badbit);
|
||||
#endif
|
||||
BRep_Builder builder;
|
||||
TopoDS_Shape shape;
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-occt-read");
|
||||
#endif
|
||||
BRepTools::Read(shape, reader, builder);
|
||||
setValue(shape);
|
||||
setValue(shape);
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
reader.exceptions(std::ios::goodbit);
|
||||
reader.clear();
|
||||
#endif
|
||||
}
|
||||
catch (const Base::Exception&) {
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property((std::string("brep-base-error:") + reader.getFileName()).c_str());
|
||||
#endif
|
||||
reader.exceptions(std::ios::goodbit);
|
||||
reader.clear();
|
||||
reader.imbue(savedLocale);
|
||||
}
|
||||
catch (const std::exception&) {
|
||||
reader.imbue(savedLocale);
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property((std::string("brep-stream-error:") + reader.getFileName()).c_str());
|
||||
#endif
|
||||
reader.exceptions(std::ios::goodbit);
|
||||
reader.clear();
|
||||
reader.imbue(savedLocale);
|
||||
if (!reader.eof()) {
|
||||
Base::Console().warning("Failed to load BRep file %s\n", reader.getFileName().c_str());
|
||||
}
|
||||
@@ -743,6 +786,9 @@ void PropertyPartShape::SaveDocFile(Base::Writer& writer) const
|
||||
|
||||
void PropertyPartShape::RestoreDocFile(Base::Reader& reader)
|
||||
{
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property((std::string("brep-file:") + reader.getFileName()).c_str());
|
||||
#endif
|
||||
|
||||
// save the element map
|
||||
auto elementMap = _Shape.resetElementMap();
|
||||
@@ -772,9 +818,24 @@ void PropertyPartShape::RestoreDocFile(Base::Reader& reader)
|
||||
}
|
||||
else {
|
||||
auto iostate = reader.exceptions();
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-restore-load");
|
||||
#endif
|
||||
loadFromStream(reader);
|
||||
// A zero-byte optional shape (for example PartDesign's
|
||||
// SuppressedShape) legitimately reaches EOF. loadFromStream
|
||||
// handles that as an empty shape, but the stream state must be
|
||||
// cleared before restoring an exception mask that includes eof.
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-restore-clear");
|
||||
#endif
|
||||
reader.exceptions(std::ios::goodbit);
|
||||
reader.clear();
|
||||
#ifdef FREECAD_WEB_CORE
|
||||
fc_web_document_restore_property("brep-restore-mask");
|
||||
#endif
|
||||
reader.exceptions(iostate);
|
||||
}
|
||||
}
|
||||
shape = getValue();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,42 +23,63 @@
|
||||
|
||||
import FreeCAD
|
||||
|
||||
import PySide
|
||||
from PySide import QtCore, QtGui
|
||||
import sys
|
||||
_WEB_HEADLESS = sys.platform == "emscripten"
|
||||
|
||||
try:
|
||||
import matplotlib
|
||||
if not _WEB_HEADLESS:
|
||||
import PySide
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
matplotlib.use("QtAgg")
|
||||
try:
|
||||
import matplotlib
|
||||
|
||||
# Force matplotlib to use PySide backend by temporarily unloading PyQt
|
||||
pyqt5_unloaded = False
|
||||
pyqt6_unloaded = False
|
||||
if "PyQt5.QtCore" in sys.modules:
|
||||
del sys.modules["PyQt5.QtCore"]
|
||||
pyqt5_unloaded = True
|
||||
if "PyQt6.QtCore" in sys.modules:
|
||||
del sys.modules["PyQt6.QtCore"]
|
||||
pyqt6_unloaded = True
|
||||
matplotlib.use("QtAgg")
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
# Force matplotlib to use PySide backend by temporarily unloading PyQt
|
||||
pyqt5_unloaded = False
|
||||
pyqt6_unloaded = False
|
||||
if "PyQt5.QtCore" in sys.modules:
|
||||
del sys.modules["PyQt5.QtCore"]
|
||||
pyqt5_unloaded = True
|
||||
if "PyQt6.QtCore" in sys.modules:
|
||||
del sys.modules["PyQt6.QtCore"]
|
||||
pyqt6_unloaded = True
|
||||
|
||||
# restore PyQt if unloaded
|
||||
if pyqt5_unloaded:
|
||||
import PyQt5.QtCore
|
||||
if pyqt6_unloaded:
|
||||
import PyQt6.QtCore
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
|
||||
# restore PyQt if unloaded
|
||||
if pyqt5_unloaded:
|
||||
import PyQt5.QtCore
|
||||
if pyqt6_unloaded:
|
||||
import PyQt6.QtCore
|
||||
|
||||
from matplotlib.figure import Figure
|
||||
except ImportError:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"The 'matplotlib' Python package was not found. Plot module cannot be loaded\n"
|
||||
)
|
||||
raise ImportError("matplotlib not installed")
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
|
||||
|
||||
from matplotlib.figure import Figure
|
||||
except ImportError:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"The 'matplotlib' Python package was not found. Plot module cannot be loaded\n"
|
||||
)
|
||||
raise ImportError("matplotlib not installed")
|
||||
else:
|
||||
# GUI-free Web uses the public Plot data API with a deterministic SVG
|
||||
# renderer. This branch deliberately does not import PySide, QtAgg, or
|
||||
# matplotlib, none of which are part of the isolated Web Python runtime.
|
||||
class _WebQtWidget:
|
||||
class QWidget:
|
||||
pass
|
||||
|
||||
class _WebQtCore:
|
||||
class Qt:
|
||||
WindowFlags = int
|
||||
|
||||
class _WebQt:
|
||||
QtGui = _WebQtWidget
|
||||
QtCore = _WebQtCore
|
||||
|
||||
PySide = _WebQt()
|
||||
QtCore = QtGui = FigureCanvas = NavigationToolbar = Figure = None
|
||||
|
||||
|
||||
def getMainWindow():
|
||||
@@ -493,3 +514,274 @@ class Plot(PySide.QtGui.QWidget):
|
||||
"""
|
||||
self.axes = self.axesList[index]
|
||||
self.fig.sca(self.axes)
|
||||
|
||||
|
||||
if _WEB_HEADLESS:
|
||||
import math
|
||||
|
||||
def _xml_escape(value):
|
||||
return (str(value).replace("&", "&").replace("<", "<")
|
||||
.replace(">", ">").replace('"', """).replace("'", "'"))
|
||||
|
||||
_WEB_ACTIVE_PLOT = None
|
||||
|
||||
class _WebAxis:
|
||||
def __init__(self, rect=(0.1, 0.1, 0.85, 0.8), frameon=True, patchcolor="none"):
|
||||
self.rect = tuple(rect)
|
||||
self.frameon = bool(frameon)
|
||||
self.patchcolor = patchcolor
|
||||
self.lines = []
|
||||
self.title = ""
|
||||
self.xlabel = ""
|
||||
self.ylabel = ""
|
||||
self.grid_enabled = False
|
||||
|
||||
def get_position(self):
|
||||
return self.rect
|
||||
|
||||
def grid(self, status):
|
||||
self.grid_enabled = bool(status)
|
||||
|
||||
class _WebFigure:
|
||||
def __init__(self):
|
||||
self.axes = []
|
||||
self.size = (6.4, 4.8)
|
||||
self.dpi = 100
|
||||
|
||||
def add_subplot(self, _code=111):
|
||||
axis = _WebAxis()
|
||||
self.axes.append(axis)
|
||||
return axis
|
||||
|
||||
def add_axes(self, rect, frameon=True):
|
||||
axis = _WebAxis(rect, frameon)
|
||||
self.axes.append(axis)
|
||||
return axis
|
||||
|
||||
def sca(self, axis):
|
||||
return axis
|
||||
|
||||
def set_size_inches(self, width, height=None):
|
||||
if height is None:
|
||||
width, height = width
|
||||
self.size = (float(width), float(height))
|
||||
|
||||
def get_size_inches(self):
|
||||
return self.size
|
||||
|
||||
def set_dpi(self, dpi):
|
||||
self.dpi = float(dpi)
|
||||
|
||||
def get_dpi(self):
|
||||
return self.dpi
|
||||
|
||||
def svg(self):
|
||||
width = max(1, int(round(self.size[0] * self.dpi)))
|
||||
height = max(1, int(round(self.size[1] * self.dpi)))
|
||||
lines = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
|
||||
'<rect width="100%" height="100%" fill="white"/>',
|
||||
]
|
||||
palette = ("#1769aa", "#d1495b", "#2a9d8f", "#e9c46a", "#6a4c93")
|
||||
for axis in self.axes:
|
||||
values = [(line.x[i], line.y[i]) for line in axis.lines for i in range(min(len(line.x), len(line.y)))]
|
||||
if not values:
|
||||
continue
|
||||
min_x = min(value[0] for value in values)
|
||||
max_x = max(value[0] for value in values)
|
||||
min_y = min(value[1] for value in values)
|
||||
max_y = max(value[1] for value in values)
|
||||
span_x = max(max_x - min_x, 1e-12)
|
||||
span_y = max(max_y - min_y, 1e-12)
|
||||
left, top = 60, 35
|
||||
plot_width, plot_height = width - 90, height - 85
|
||||
if axis.grid_enabled:
|
||||
lines.append(f'<path d="M {left} {top} V {top + plot_height} H {left + plot_width}" fill="none" stroke="#cbd5e1"/>')
|
||||
for index, line in enumerate(axis.lines):
|
||||
points = []
|
||||
for x, y in zip(line.x, line.y):
|
||||
px = left + (x - min_x) / span_x * plot_width
|
||||
py = top + plot_height - (y - min_y) / span_y * plot_height
|
||||
points.append(f"{px:.3f},{py:.3f}")
|
||||
color = line.properties.get("color", palette[index % len(palette)])
|
||||
lines.append(f'<polyline points="{" ".join(points)}" fill="none" stroke="{_xml_escape(color)}" stroke-width="2"/>')
|
||||
if line.name:
|
||||
lines.append(f'<text x="{left + 8}" y="{top + 18 + index * 16}" fill="{_xml_escape(color)}">{_xml_escape(line.name)}</text>')
|
||||
if axis.title:
|
||||
lines.append(f'<text x="{left + plot_width / 2:.1f}" y="20" text-anchor="middle">{_xml_escape(axis.title)}</text>')
|
||||
if axis.xlabel:
|
||||
lines.append(f'<text x="{left + plot_width / 2:.1f}" y="{height - 12}" text-anchor="middle">{_xml_escape(axis.xlabel)}</text>')
|
||||
if axis.ylabel:
|
||||
lines.append(f'<text x="14" y="{top + plot_height / 2:.1f}" transform="rotate(-90 14 {top + plot_height / 2:.1f})" text-anchor="middle">{_xml_escape(axis.ylabel)}</text>')
|
||||
lines.append('</svg>')
|
||||
return "".join(lines)
|
||||
|
||||
class _WebCanvas:
|
||||
def __init__(self, figure):
|
||||
self.figure = figure
|
||||
|
||||
def draw(self):
|
||||
return None
|
||||
|
||||
def print_figure(self, path):
|
||||
with open(path, "w", encoding="utf-8") as output:
|
||||
output.write(self.figure.svg())
|
||||
|
||||
class Line:
|
||||
def __init__(self, axis, x, y, name=None):
|
||||
if len(x) != len(y) or not x:
|
||||
raise ValueError("plot series requires equally sized non-empty x and y values")
|
||||
self.axes = axis
|
||||
self.x = [float(value) for value in x]
|
||||
self.y = [float(value) for value in y]
|
||||
if not all(math.isfinite(value) for value in self.x + self.y):
|
||||
raise ValueError("plot series values must be finite")
|
||||
self.name = name
|
||||
self.lid = len(axis.lines)
|
||||
self.properties = {}
|
||||
axis.lines.append(self)
|
||||
|
||||
def setp(self, prop, value):
|
||||
self.properties[str(prop)] = value
|
||||
|
||||
def getp(self, prop):
|
||||
return self.properties.get(str(prop))
|
||||
|
||||
class Plot:
|
||||
def __init__(self, winTitle="plot", parent=None, flags=0):
|
||||
self.winTitle = str(winTitle)
|
||||
self.fig = _WebFigure()
|
||||
self.canvas = _WebCanvas(self.fig)
|
||||
self.axes = self.fig.add_subplot(111)
|
||||
self.axesList = [self.axes]
|
||||
self.mpl_toolbar = None
|
||||
self.series = []
|
||||
self.skip = False
|
||||
self.legend = False
|
||||
self.legPos = (1.0, 1.0)
|
||||
self.legSiz = 14
|
||||
self.grid = False
|
||||
|
||||
def plot(self, x, y, name=None):
|
||||
line = Line(self.axes, x, y, name)
|
||||
self.series.append(line)
|
||||
self.update()
|
||||
return line
|
||||
|
||||
def update(self):
|
||||
return None
|
||||
|
||||
def isGrid(self):
|
||||
return bool(self.grid)
|
||||
|
||||
def isLegend(self):
|
||||
return bool(self.legend)
|
||||
|
||||
def setActiveAxes(self, index):
|
||||
self.axes = self.axesList[index]
|
||||
|
||||
def getMainWindow():
|
||||
return None
|
||||
|
||||
def getMdiArea():
|
||||
return None
|
||||
|
||||
def getPlot():
|
||||
return _WEB_ACTIVE_PLOT
|
||||
|
||||
def closePlot():
|
||||
global _WEB_ACTIVE_PLOT
|
||||
_WEB_ACTIVE_PLOT = None
|
||||
|
||||
def figure(winTitle="plot"):
|
||||
global _WEB_ACTIVE_PLOT
|
||||
_WEB_ACTIVE_PLOT = Plot(winTitle)
|
||||
return _WEB_ACTIVE_PLOT
|
||||
|
||||
def plot(x, y, name=None):
|
||||
active = getPlot() or figure()
|
||||
return active.plot(x, y, name)
|
||||
|
||||
def series():
|
||||
active = getPlot()
|
||||
return [] if active is None else active.series
|
||||
|
||||
def removeSerie(index):
|
||||
active = getPlot()
|
||||
if active is None:
|
||||
return None
|
||||
line = active.series.pop(index)
|
||||
active.axes.lines.remove(line)
|
||||
return None
|
||||
|
||||
def legend(status=True, pos=None, fontsize=None):
|
||||
active = getPlot()
|
||||
if active is not None:
|
||||
active.legend = bool(status)
|
||||
if pos is not None:
|
||||
active.legPos = pos
|
||||
if fontsize is not None:
|
||||
active.legSiz = fontsize
|
||||
|
||||
def grid(status=True):
|
||||
active = getPlot()
|
||||
if active is not None:
|
||||
active.grid = bool(status)
|
||||
active.axes.grid(status)
|
||||
|
||||
def title(string):
|
||||
active = getPlot()
|
||||
if active is not None:
|
||||
active.axes.title = str(string)
|
||||
|
||||
def xlabel(string):
|
||||
active = getPlot()
|
||||
if active is not None:
|
||||
active.axes.xlabel = str(string)
|
||||
|
||||
def ylabel(string):
|
||||
active = getPlot()
|
||||
if active is not None:
|
||||
active.axes.ylabel = str(string)
|
||||
|
||||
def axesList():
|
||||
active = getPlot()
|
||||
return [] if active is None else active.axesList
|
||||
|
||||
def axes():
|
||||
active = getPlot()
|
||||
return None if active is None else active.axes
|
||||
|
||||
def addNewAxes(rect=None, frameon=True, patchcolor="none"):
|
||||
active = getPlot()
|
||||
if active is None:
|
||||
return None
|
||||
if rect is None:
|
||||
rect = active.axes.get_position()
|
||||
axis = active.fig.add_axes(rect, frameon)
|
||||
axis.patchcolor = patchcolor
|
||||
active.axesList.append(axis)
|
||||
active.setActiveAxes(-1)
|
||||
return axis
|
||||
|
||||
def save(path, figsize=None, dpi=None):
|
||||
active = getPlot()
|
||||
if active is None:
|
||||
return None
|
||||
old_size = active.fig.get_size_inches()
|
||||
old_dpi = active.fig.get_dpi()
|
||||
try:
|
||||
if figsize:
|
||||
active.fig.set_size_inches(figsize[0], figsize[1])
|
||||
if dpi:
|
||||
active.fig.set_dpi(dpi)
|
||||
active.canvas.print_figure(path)
|
||||
finally:
|
||||
active.fig.set_size_inches(*old_size)
|
||||
active.fig.set_dpi(old_dpi)
|
||||
|
||||
def addNavigationToolbar():
|
||||
return None
|
||||
|
||||
def delNavigationToolbar():
|
||||
return None
|
||||
|
||||
@@ -22,9 +22,11 @@
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrentMap>
|
||||
#ifndef FREECAD_WEB_CORE
|
||||
# include <QFuture>
|
||||
# include <QFutureWatcher>
|
||||
# include <QtConcurrentMap>
|
||||
#endif
|
||||
|
||||
#include <Geom_BSplineSurface.hxx>
|
||||
#include <Precision.hxx>
|
||||
@@ -39,7 +41,9 @@
|
||||
|
||||
|
||||
using namespace Reen;
|
||||
namespace sp = std::placeholders;
|
||||
#ifndef FREECAD_WEB_CORE
|
||||
namespace sp = std::placeholders;
|
||||
#endif
|
||||
|
||||
// SplineBasisfunction
|
||||
|
||||
@@ -1198,9 +1202,10 @@ bool BSplineParameterCorrection::SolveWithSmoothing(double fWeight)
|
||||
}
|
||||
}
|
||||
|
||||
// The product of its transform and itself results in the quadratic
|
||||
// system matrix (slowly)
|
||||
#if 0
|
||||
// The product of its transform and itself results in the quadratic
|
||||
// system matrix. Web builds use the same calculation synchronously because
|
||||
// QtConcurrent is not part of the GUI-free Qt footprint.
|
||||
#if 0
|
||||
math_Matrix MTM = M.TMultiply(M);
|
||||
#elif 0
|
||||
math_Matrix MTM(0, ulDim - 1, 0, ulDim - 1);
|
||||
@@ -1210,7 +1215,15 @@ bool BSplineParameterCorrection::SolveWithSmoothing(double fWeight)
|
||||
MTM(m, n) = MTM(n, m) = Mm * M.Col(n);
|
||||
}
|
||||
}
|
||||
#else // multi-threaded
|
||||
#elif defined(FREECAD_WEB_CORE)
|
||||
math_Matrix MTM(0, ulDim - 1, 0, ulDim - 1);
|
||||
for (unsigned m = 0; m < ulDim; m++) {
|
||||
math_Vector Mm = M.Col(m);
|
||||
for (unsigned n = m; n < ulDim; n++) {
|
||||
MTM(m, n) = MTM(n, m) = Mm * M.Col(n);
|
||||
}
|
||||
}
|
||||
#else // multi-threaded
|
||||
std::vector<int> columns(ulDim);
|
||||
std::generate(columns.begin(), columns.end(), Base::iotaGen<int>(0));
|
||||
ScalarProduct scalar(M);
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
#include <math_Gauss.hxx>
|
||||
#include <math_Householder.hxx>
|
||||
|
||||
// Qt
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrentMap>
|
||||
// Qt
|
||||
#ifndef FREECAD_WEB_CORE
|
||||
# include <QFuture>
|
||||
# include <QFutureWatcher>
|
||||
# include <QtConcurrentMap>
|
||||
#endif
|
||||
|
||||
@@ -907,11 +907,11 @@ void SketchObject::Save(Writer& writer) const
|
||||
Part::Part2DObject::Save(writer);
|
||||
}
|
||||
|
||||
void SketchObject::Restore(XMLReader& reader)
|
||||
{
|
||||
// read the father classes
|
||||
Part::Part2DObject::Restore(reader);
|
||||
}
|
||||
void SketchObject::Restore(XMLReader& reader)
|
||||
{
|
||||
// read the father classes
|
||||
Part::Part2DObject::Restore(reader);
|
||||
}
|
||||
|
||||
void SketchObject::handleChangedPropertyType(Base::XMLReader &reader,
|
||||
const char *TypeName, App::Property *prop)
|
||||
|
||||
@@ -30,7 +30,7 @@ target_link_options(FreeCADWebPartBoxProbe PRIVATE
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','FS_readFile','FS_readdir']"
|
||||
"--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"
|
||||
)
|
||||
@@ -126,6 +126,56 @@ if(FREECAD_WEB_ENABLE_SANDBOX)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Plot.py is a Python/Qt helper rather than a C++ module. The Web probe embeds
|
||||
# the official source and exercises its emscripten headless backend without
|
||||
# enabling the desktop GUI or requiring PySide/QtAgg.
|
||||
add_executable(FreeCADWebPlotRuntimeProbe PlotRuntimeProbe.cpp)
|
||||
target_compile_features(FreeCADWebPlotRuntimeProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebPlotRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebPlotRuntimeProbe PRIVATE
|
||||
"${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(FreeCADWebPlotRuntimeProbe PRIVATE
|
||||
Part ${Python3_LIBRARIES}
|
||||
"${_freecad_web_python_build}/Modules/_decimal/libmpdec/libmpdec.a"
|
||||
"${_freecad_web_python_build}/Modules/_hacl/libHacl_Hash_SHA2.a"
|
||||
"${_freecad_web_python_build}/Modules/expat/libexpat.a")
|
||||
target_link_options(FreeCADWebPlotRuntimeProbe PRIVATE
|
||||
"-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1"
|
||||
"-sEXPORT_NAME=FreeCADWebPlotRuntimeProbe" "-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/Plot/Plot.py@/freecad-mod/Plot/Plot.py")
|
||||
set_target_properties(FreeCADWebPlotRuntimeProbe PROPERTIES
|
||||
SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
|
||||
if(FREECAD_WEB_ENABLE_REVERSEENGINEERING)
|
||||
add_executable(FreeCADWebReverseEngineeringRuntimeProbe ReverseEngineeringRuntimeProbe.cpp)
|
||||
target_compile_features(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE ReverseEngineering 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(FreeCADWebReverseEngineeringRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebReverseEngineeringRuntimeProbe" "-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(FreeCADWebReverseEngineeringRuntimeProbe PROPERTIES SUFFIX ".js" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
endif()
|
||||
|
||||
if(FREECAD_WEB_ENABLE_ROBOT)
|
||||
add_executable(FreeCADWebRobotRuntimeProbe RobotRuntimeProbeFixed.cpp)
|
||||
target_sources(FreeCADWebRobotRuntimeProbe PRIVATE RobotInventoryApi.cpp)
|
||||
target_compile_features(FreeCADWebRobotRuntimeProbe PRIVATE cxx_std_17)
|
||||
target_compile_options(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "-fexceptions")
|
||||
target_include_directories(FreeCADWebRobotRuntimeProbe PRIVATE "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/src" "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(FreeCADWebRobotRuntimeProbe PRIVATE Robot 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(FreeCADWebRobotRuntimeProbe PRIVATE "-O2" "-g0" "--no-entry" "-sWASM=1" "-sMODULARIZE=1" "-sEXPORT_ES6=1" "-sEXPORT_NAME=FreeCADWebRobotRuntimeProbe" "-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(FreeCADWebRobotRuntimeProbe 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")
|
||||
@@ -157,7 +207,7 @@ target_link_options(FreeCADWebPartDesignRuntimeProbe PRIVATE
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','FS_readFile']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebPartDesignRuntimeProbe PROPERTIES
|
||||
@@ -582,7 +632,7 @@ target_link_options(FreeCADWebSketcherProbe PRIVATE
|
||||
"-sENVIRONMENT=web,worker,node"
|
||||
"-sUSE_BZIP2=1"
|
||||
"-sDISABLE_EXCEPTION_CATCHING=0"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall']"
|
||||
"-sEXPORTED_RUNTIME_METHODS=['ccall','FS']"
|
||||
"--embed-file=${_freecad_web_python_encodings}@/freecad-python/encodings"
|
||||
)
|
||||
set_target_properties(FreeCADWebSketcherProbe PROPERTIES
|
||||
|
||||
@@ -21,6 +21,7 @@ extern "C" PyObject* PyInit_Materials();
|
||||
extern "C" PyObject* PyInit_Part();
|
||||
extern "C" PyObject* PyInit_Sketcher();
|
||||
extern "C" PyObject* PyInit__PartDesign();
|
||||
extern "C" const char* fc_web_document_restore_stage();
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -530,8 +531,8 @@ EMSCRIPTEN_KEEPALIVE int fc_web_partdesign_save_reopen()
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
catch (const Base::Exception& error) { persistenceError = error.what(); }
|
||||
catch (const std::exception& error) { persistenceError = error.what(); }
|
||||
catch (const Base::Exception& error) { persistenceError = std::string(fc_web_document_restore_stage()) + ": " + error.what(); }
|
||||
catch (const std::exception& error) { persistenceError = std::string(fc_web_document_restore_stage()) + ": " + error.what(); }
|
||||
catch (...) { persistenceError = "unknown PartDesign save/reopen failure"; }
|
||||
if (document) App::GetApplication().closeDocument(document);
|
||||
if (reopened) App::GetApplication().closeDocument(reopened);
|
||||
|
||||
277
src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp
Normal file
277
src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#include <App/Application.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* plotSourceEntry = "src/Mod/Plot/Plot.py";
|
||||
constexpr const char* plotSourcePath = "/freecad-mod/Plot/Plot.py";
|
||||
|
||||
std::string errorMessage;
|
||||
std::string apiError;
|
||||
std::string recoveryError;
|
||||
int initializationCalls = 0;
|
||||
int initializationRuns = 0;
|
||||
int moduleAvailable = 0;
|
||||
int apiCount = 0;
|
||||
int seriesCount = 0;
|
||||
int svgSize = 0;
|
||||
int svgValid = 0;
|
||||
int resourcesReleased = 0;
|
||||
int recoverySeriesBefore = 0;
|
||||
int recoverySeriesAfterReject = 0;
|
||||
int recoverySeriesAfterValid = 0;
|
||||
int recoveryUnchanged = 0;
|
||||
int recoveryRepeatedStable = 0;
|
||||
|
||||
std::string pythonError()
|
||||
{
|
||||
PyObject* type = nullptr;
|
||||
PyObject* value = nullptr;
|
||||
PyObject* traceback = nullptr;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
PyErr_NormalizeException(&type, &value, &traceback);
|
||||
std::string message = "PythonError";
|
||||
if (type) {
|
||||
PyObject* name = PyObject_GetAttrString(type, "__name__");
|
||||
if (name) {
|
||||
message = PyUnicode_AsUTF8(name);
|
||||
Py_DECREF(name);
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
PyObject* text = PyObject_Str(value);
|
||||
if (text) {
|
||||
message += ": ";
|
||||
message += PyUnicode_AsUTF8(text);
|
||||
Py_DECREF(text);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
return message;
|
||||
}
|
||||
|
||||
bool ensureRuntime()
|
||||
{
|
||||
++initializationCalls;
|
||||
try {
|
||||
App::Application::initWebCore();
|
||||
PyObject* existing = PyImport_ImportModule("Plot");
|
||||
if (existing) {
|
||||
Py_DECREF(existing);
|
||||
moduleAvailable = 1;
|
||||
return true;
|
||||
}
|
||||
PyErr_Clear();
|
||||
std::ifstream source(plotSourcePath);
|
||||
if (!source) {
|
||||
errorMessage = "dependency_unavailable: embedded Plot.py is missing";
|
||||
return false;
|
||||
}
|
||||
const std::string text((std::istreambuf_iterator<char>(source)), std::istreambuf_iterator<char>());
|
||||
PyObject* code = Py_CompileString(text.c_str(), plotSourcePath, Py_file_input);
|
||||
if (!code) {
|
||||
errorMessage = pythonError();
|
||||
return false;
|
||||
}
|
||||
PyObject* module = PyImport_ExecCodeModule("Plot", code);
|
||||
Py_DECREF(code);
|
||||
if (!module) {
|
||||
errorMessage = pythonError();
|
||||
return false;
|
||||
}
|
||||
Py_DECREF(module);
|
||||
moduleAvailable = 1;
|
||||
++initializationRuns;
|
||||
return true;
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
errorMessage = error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
errorMessage = error.what();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* plotModule()
|
||||
{
|
||||
return PyImport_ImportModule("Plot");
|
||||
}
|
||||
|
||||
bool readInt(PyObject* globals, const char* name, int& target)
|
||||
{
|
||||
PyObject* value = PyDict_GetItemString(globals, name);
|
||||
if (!value || !PyLong_Check(value)) {
|
||||
return false;
|
||||
}
|
||||
target = static_cast<int>(PyLong_AsLong(value));
|
||||
return !PyErr_Occurred();
|
||||
}
|
||||
|
||||
bool executeScript(const char* script, PyObject** globalsOut)
|
||||
{
|
||||
PyObject* globals = PyDict_New();
|
||||
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
|
||||
PyObject* result = PyRun_String(script, Py_file_input, globals, globals);
|
||||
if (!result) {
|
||||
errorMessage = pythonError();
|
||||
Py_DECREF(globals);
|
||||
return false;
|
||||
}
|
||||
Py_DECREF(result);
|
||||
*globalsOut = globals;
|
||||
return true;
|
||||
}
|
||||
|
||||
int countApi()
|
||||
{
|
||||
PyObject* module = plotModule();
|
||||
if (!module) {
|
||||
apiError = pythonError();
|
||||
return 0;
|
||||
}
|
||||
const char* names[] = {"figure", "plot", "series", "removeSerie", "legend", "grid", "title", "xlabel", "ylabel", "axesList", "axes", "addNewAxes", "save", "Plot", "Line"};
|
||||
int count = 0;
|
||||
for (const char* name : names) {
|
||||
count += PyObject_HasAttrString(module, name) ? 1 : 0;
|
||||
}
|
||||
Py_DECREF(module);
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_initialize()
|
||||
{
|
||||
errorMessage.clear();
|
||||
if (!ensureRuntime()) {
|
||||
return 0;
|
||||
}
|
||||
if (initializationRuns == 0) {
|
||||
++initializationRuns;
|
||||
}
|
||||
return moduleAvailable;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_initialization_calls() { return initializationCalls; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_initialization_runs() { return initializationRuns; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_module_available() { return moduleAvailable; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_plot_last_error() { return errorMessage.c_str(); }
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_python_api_validate(const char* sourceEntry)
|
||||
{
|
||||
if (!sourceEntry || std::string(sourceEntry) != plotSourceEntry) {
|
||||
apiError = "revision_mismatch";
|
||||
return 0;
|
||||
}
|
||||
if (!ensureRuntime()) {
|
||||
apiError = errorMessage;
|
||||
return 0;
|
||||
}
|
||||
apiError.clear();
|
||||
apiCount = countApi();
|
||||
return apiCount == 15 ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_python_api()
|
||||
{
|
||||
if (!ensureRuntime()) {
|
||||
apiError = errorMessage;
|
||||
return 0;
|
||||
}
|
||||
const char* script =
|
||||
"import Plot\n"
|
||||
"Plot.figure('Web Plot')\n"
|
||||
"Plot.plot([0, 1, 2], [1, 3, 2], 'fixture')\n"
|
||||
"Plot.grid(True)\n"
|
||||
"Plot.legend(True)\n"
|
||||
"Plot.title('Web Plot')\n"
|
||||
"Plot.xlabel('x')\n"
|
||||
"Plot.ylabel('y')\n"
|
||||
"Plot.save('/tmp/PlotWeb.svg', figsize=(4, 3), dpi=96)\n"
|
||||
"_plot_api_series = len(Plot.series())\n";
|
||||
PyObject* globals = nullptr;
|
||||
apiCount = countApi();
|
||||
if (!executeScript(script, &globals)) {
|
||||
apiError = errorMessage;
|
||||
return 0;
|
||||
}
|
||||
bool valid = readInt(globals, "_plot_api_series", seriesCount);
|
||||
Py_DECREF(globals);
|
||||
std::ifstream svg("/tmp/PlotWeb.svg", std::ios::binary | std::ios::ate);
|
||||
if (svg) {
|
||||
svgSize = static_cast<int>(svg.tellg());
|
||||
svg.seekg(0);
|
||||
std::string content(static_cast<std::size_t>(svgSize), '\0');
|
||||
svg.read(content.data(), content.size());
|
||||
svgValid = content.find("<svg") != std::string::npos ? 1 : 0;
|
||||
}
|
||||
apiError.clear();
|
||||
resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
|
||||
return valid && seriesCount == 1 && svgSize > 0 && svgValid == 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery()
|
||||
{
|
||||
recoveryError.clear();
|
||||
recoverySeriesBefore = recoverySeriesAfterReject = recoverySeriesAfterValid = 0;
|
||||
recoveryUnchanged = recoveryRepeatedStable = 0;
|
||||
if (!ensureRuntime()) {
|
||||
recoveryError = errorMessage;
|
||||
return 0;
|
||||
}
|
||||
const char* script =
|
||||
"import Plot\n"
|
||||
"Plot.figure('Recovery')\n"
|
||||
"Plot.plot([0, 1], [0, 1], 'valid')\n"
|
||||
"_plot_recovery_before = len(Plot.series())\n"
|
||||
"try:\n"
|
||||
" Plot.plot([0], [1, 2], 'invalid')\n"
|
||||
"except ValueError as _plot_recovery_error:\n"
|
||||
" _plot_recovery_error_text = str(_plot_recovery_error)\n"
|
||||
"_plot_recovery_after_reject = len(Plot.series())\n"
|
||||
"Plot.plot([1, 2], [1, 4], 'recovered')\n"
|
||||
"_plot_recovery_after_valid = len(Plot.series())\n";
|
||||
PyObject* globals = nullptr;
|
||||
if (!executeScript(script, &globals)) {
|
||||
recoveryError = errorMessage;
|
||||
return 0;
|
||||
}
|
||||
bool valid = readInt(globals, "_plot_recovery_before", recoverySeriesBefore)
|
||||
&& readInt(globals, "_plot_recovery_after_reject", recoverySeriesAfterReject)
|
||||
&& readInt(globals, "_plot_recovery_after_valid", recoverySeriesAfterValid);
|
||||
PyObject* errorText = PyDict_GetItemString(globals, "_plot_recovery_error_text");
|
||||
recoveryError = errorText ? PyUnicode_AsUTF8(errorText) : "invalid plot input was not rejected";
|
||||
Py_DECREF(globals);
|
||||
recoveryUnchanged = valid && recoverySeriesBefore == recoverySeriesAfterReject ? 1 : 0;
|
||||
recoveryRepeatedStable = recoveryError == "plot series requires equally sized non-empty x and y values" ? 1 : 0;
|
||||
return valid && recoveryUnchanged && recoveryRepeatedStable && recoverySeriesAfterValid == 2 ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_api_count() { return apiCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_series_count() { return seriesCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_svg_size() { return svgSize; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_svg_valid() { return svgValid; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_resources_released() { return resourcesReleased; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_plot_api_error() { return apiError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_plot_recovery_error() { return recoveryError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_series_before() { return recoverySeriesBefore; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_series_after_reject() { return recoverySeriesAfterReject; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_series_after_valid() { return recoverySeriesAfterValid; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_unchanged() { return recoveryUnchanged; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_plot_recovery_repeated_stable() { return recoveryRepeatedStable; }
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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/Interpreter.h>
|
||||
#include <Mod/Part/App/PartFeature.h>
|
||||
#include <BRepGProp.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <string>
|
||||
#include <cmath>
|
||||
|
||||
extern "C" PyObject* PyInit_ReverseEngineering();
|
||||
extern "C" PyObject* PyInit_Materials();
|
||||
extern "C" PyObject* PyInit_Part();
|
||||
extern "C" PyObject* PyInit_Mesh();
|
||||
extern "C" const char* fc_web_document_restore_stage();
|
||||
|
||||
namespace {
|
||||
std::string errorMessage;
|
||||
std::string apiNames;
|
||||
const char* inventorySourceEntry = "src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp";
|
||||
const char* inventorySummary = "ReverseEngineering App; Part/Mesh/Points dependencies; GUI excluded";
|
||||
int initializationCalls = 0;
|
||||
int initializationRuns = 0;
|
||||
int moduleAvailable = 0;
|
||||
int apiCount = 0;
|
||||
int curveValid = 0;
|
||||
std::string persistenceError;
|
||||
std::string requestedPersistencePath;
|
||||
std::string requestedPersistenceError;
|
||||
std::string persistenceStage;
|
||||
std::string persistencePath;
|
||||
std::string persistenceObjectType;
|
||||
double persistenceBeforeLength = 0.0;
|
||||
double persistenceAfterLength = 0.0;
|
||||
int persistenceSize = 0;
|
||||
int persistenceObjectCount = 0;
|
||||
std::string actionError;
|
||||
std::string actionObjectType;
|
||||
double actionLength = 0.0;
|
||||
int actionObjectCount = 0;
|
||||
int actionResourcesReleased = 0;
|
||||
|
||||
std::string pythonError()
|
||||
{
|
||||
PyObject *type = nullptr, *value = nullptr, *traceback = nullptr;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
PyErr_NormalizeException(&type, &value, &traceback);
|
||||
std::string message = "PythonError";
|
||||
if (type) {
|
||||
PyObject* name = PyObject_GetAttrString(type, "__name__");
|
||||
if (name) { message = PyUnicode_AsUTF8(name); Py_DECREF(name); }
|
||||
}
|
||||
if (value) {
|
||||
PyObject* text = PyObject_Str(value);
|
||||
if (text) { message += ": "; message += PyUnicode_AsUTF8(text); Py_DECREF(text); }
|
||||
}
|
||||
Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback);
|
||||
return message;
|
||||
}
|
||||
|
||||
void addBuiltin(const char* name, PyObject* (*initializer)())
|
||||
{
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
if (PyDict_GetItemString(modules, name)) {
|
||||
return;
|
||||
}
|
||||
PyObject* module = initializer();
|
||||
if (!module) {
|
||||
throw Base::RuntimeError(std::string("PyInit_") + name + " failed: " + pythonError());
|
||||
}
|
||||
if (PyDict_SetItemString(modules, name, module) != 0) {
|
||||
Py_DECREF(module);
|
||||
throw Base::RuntimeError(std::string("failed to register ") + name);
|
||||
}
|
||||
Py_DECREF(module);
|
||||
}
|
||||
|
||||
bool initialize()
|
||||
{
|
||||
++initializationCalls;
|
||||
if (moduleAvailable) return true;
|
||||
try {
|
||||
App::Application::initWebCore();
|
||||
auto* modules = PyImport_GetModuleDict();
|
||||
addBuiltin("Materials", &PyInit_Materials);
|
||||
addBuiltin("Part", &PyInit_Part);
|
||||
addBuiltin("Mesh", &PyInit_Mesh);
|
||||
if (!PyDict_GetItemString(modules, "ReverseEngineering")) {
|
||||
PyObject* module = PyInit_ReverseEngineering();
|
||||
if (!module) { errorMessage = pythonError(); return false; }
|
||||
PyDict_SetItemString(modules, "ReverseEngineering", module);
|
||||
Py_DECREF(module);
|
||||
}
|
||||
moduleAvailable = 1;
|
||||
++initializationRuns;
|
||||
return true;
|
||||
}
|
||||
catch (const Base::Exception& error) { errorMessage = error.what(); return false; }
|
||||
catch (const std::exception& error) { errorMessage = error.what(); return false; }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_initialize()
|
||||
{
|
||||
errorMessage.clear();
|
||||
return initialize() ? 1 : 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_initialization_calls() { return initializationCalls; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_initialization_runs() { return initializationRuns; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_module_available() { return moduleAvailable; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_error() { return errorMessage.c_str(); }
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_python_api_validate(const char* sourceEntry)
|
||||
{
|
||||
if (!sourceEntry || std::string(sourceEntry) != "src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp") { errorMessage = "revision_mismatch"; return 0; }
|
||||
if (!initialize()) return 0;
|
||||
PyObject* module = PyImport_ImportModule("ReverseEngineering");
|
||||
if (!module) { errorMessage = pythonError(); return 0; }
|
||||
const char* names[] = {"approxCurve", "approxSurface"}; apiCount = 0; apiNames.clear();
|
||||
for (const char* name : names) if (PyObject_HasAttrString(module, name)) { ++apiCount; if (!apiNames.empty()) apiNames += ","; apiNames += name; }
|
||||
Py_DECREF(module); errorMessage.clear(); return apiCount == 2 ? 1 : 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_source_inventory_validate(const char* sourceEntry)
|
||||
{
|
||||
if (!sourceEntry || std::string(sourceEntry) != inventorySourceEntry) {
|
||||
errorMessage = "revision_mismatch";
|
||||
return 0;
|
||||
}
|
||||
if (!initialize()) return 0;
|
||||
errorMessage.clear();
|
||||
return 1;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_source_entry() { return inventorySourceEntry; }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_source_summary() { return inventorySummary; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_registered_type_count() { return 0; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_python_binding_count() { return 2; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_inventory_resources_released() { return 1; }
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_python_api()
|
||||
{
|
||||
if (!initialize()) return 0;
|
||||
const char* code =
|
||||
"import ReverseEngineering as Reen\n"
|
||||
"_curve = Reen.approxCurve([(0,0,0),(1,1,0),(2,0,0)])\n"
|
||||
"_curve_valid = hasattr(_curve, 'toShape')\n";
|
||||
PyObject* globals = PyDict_New();
|
||||
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
|
||||
PyObject* result = PyRun_String(code, Py_file_input, globals, globals);
|
||||
if (!result) { errorMessage = pythonError(); Py_DECREF(globals); return 0; }
|
||||
Py_DECREF(result);
|
||||
PyObject* valid = PyDict_GetItemString(globals, "_curve_valid");
|
||||
curveValid = valid ? PyObject_IsTrue(valid) : 0;
|
||||
Py_DECREF(globals);
|
||||
return curveValid && apiCount == 2 ? 1 : 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_api_names() { return apiNames.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_api_count() { return apiCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_curve_valid() { return curveValid; }
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_create_recompute()
|
||||
{
|
||||
actionError.clear(); actionObjectType.clear(); actionLength = 0.0; actionObjectCount = 0; actionResourcesReleased = 0;
|
||||
App::Document* document = nullptr;
|
||||
try {
|
||||
if (!initialize()) { actionError = errorMessage; return 0; }
|
||||
auto& application = App::GetApplication();
|
||||
Base::Interpreter().runString(
|
||||
"import FreeCAD as App, Part, ReverseEngineering as Reen\n"
|
||||
"doc=App.newDocument('ReverseEngineeringCreate')\n"
|
||||
"curve=Reen.approxCurve([(0,0,0),(1,1,0),(2,0,0)])\n"
|
||||
"feature=doc.addObject('Part::Feature','ReverseEngineeringCurve')\n"
|
||||
"feature.Shape=curve.toShape()\n"
|
||||
"doc.recompute()\n");
|
||||
document = application.getDocument("ReverseEngineeringCreate");
|
||||
auto* feature = document ? dynamic_cast<Part::Feature*>(document->getObject("ReverseEngineeringCurve")) : nullptr;
|
||||
if (!document || !feature) throw Base::RuntimeError("ReverseEngineering create feature failed");
|
||||
bool hasError = false; document->recompute({}, false, &hasError);
|
||||
GProp_GProps properties; BRepGProp::LinearProperties(feature->Shape.getValue(), properties);
|
||||
actionLength = properties.Mass(); actionObjectType = feature->getTypeId().getName(); actionObjectCount = document->countObjects();
|
||||
const bool valid = !hasError && actionObjectType == "Part::Feature" && actionObjectCount == 1 && actionLength > 0.0;
|
||||
application.closeDocument(document); document = nullptr; actionResourcesReleased = application.getDocuments().empty() ? 1 : 0;
|
||||
if (!valid || actionResourcesReleased != 1) { actionError = "create_recompute_mismatch"; return 0; }
|
||||
return 1;
|
||||
} catch (const Base::Exception& error) { actionError = error.what(); }
|
||||
catch (const std::exception& error) { actionError = error.what(); }
|
||||
if (document) App::GetApplication().closeDocument(document);
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_action_error() { return actionError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_action_object_type() { return actionObjectType.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_reverseengineering_action_length() { return actionLength; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_action_object_count() { return actionObjectCount; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_action_resources_released() { return actionResourcesReleased; }
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_recovery_invalid()
|
||||
{
|
||||
actionError.clear();
|
||||
try {
|
||||
if (!initialize()) { actionError = errorMessage; return 0; }
|
||||
const char* code = "import ReverseEngineering as Reen\nReen.approxCurve([])\n";
|
||||
PyObject* globals = PyDict_New(); PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
|
||||
PyObject* result = PyRun_String(code, Py_file_input, globals, globals); Py_DECREF(globals);
|
||||
if (result) { Py_DECREF(result); actionError = "invalid_input_accepted"; return 0; }
|
||||
actionError = pythonError();
|
||||
return 1;
|
||||
} catch (const Base::Exception& error) { actionError = error.what(); return 0; }
|
||||
catch (const std::exception& error) { actionError = error.what(); return 0; }
|
||||
catch (...) { actionError = "unknown ReverseEngineering invalid-input failure"; return 0; }
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_save_reopen()
|
||||
{
|
||||
persistenceError.clear(); persistencePath = requestedPersistencePath.empty() ? "/tmp/ReverseEngineeringWeb.FCStd" : requestedPersistencePath;
|
||||
persistenceObjectType.clear(); persistenceBeforeLength = persistenceAfterLength = 0.0;
|
||||
persistenceSize = persistenceObjectCount = 0;
|
||||
App::Document* document = nullptr; App::Document* reopened = nullptr;
|
||||
try {
|
||||
if (!requestedPersistenceError.empty()) { persistenceError = requestedPersistenceError; return 0; }
|
||||
persistenceStage = "initialize";
|
||||
if (!initialize()) { persistenceError = errorMessage; return 0; }
|
||||
auto& application = App::GetApplication();
|
||||
persistenceStage = "python-create-document";
|
||||
Base::Interpreter().runString(
|
||||
"import FreeCAD as App, Part, ReverseEngineering as Reen\n"
|
||||
"doc=App.newDocument('ReverseEngineeringPersistence')\n"
|
||||
"curve=Reen.approxCurve([(0,0,0),(1,1,0),(2,0,0)])\n"
|
||||
"feature=doc.addObject('Part::Feature','ReverseEngineeringCurve')\n"
|
||||
"feature.Shape=curve.toShape()\n"
|
||||
"doc.recompute()\n");
|
||||
persistenceStage = "get-document";
|
||||
document = application.getDocument("ReverseEngineeringPersistence");
|
||||
auto* feature = document ? dynamic_cast<Part::Feature*>(document->getObject("ReverseEngineeringCurve")) : nullptr;
|
||||
if (!document || !feature) throw Base::RuntimeError("ReverseEngineering Part::Feature creation failed");
|
||||
persistenceStage = "measure-before";
|
||||
GProp_GProps before; BRepGProp::LinearProperties(feature->Shape.getValue(), before); persistenceBeforeLength = before.Mass();
|
||||
persistenceStage = "save";
|
||||
if (!document->saveAs(persistencePath.c_str())) throw Base::RuntimeError("ReverseEngineering FCStd save failed");
|
||||
persistenceSize = static_cast<int>(Base::FileInfo(persistencePath).size()); application.closeDocument(document); document = nullptr;
|
||||
persistenceStage = "new-reopened-document";
|
||||
reopened = application.newDocument("ReverseEngineeringPersistenceOpened", "ReverseEngineering Persistence Opened", {false, true});
|
||||
if (!reopened) throw Base::RuntimeError("ReverseEngineering reopen document creation failed");
|
||||
persistenceStage = "restore";
|
||||
reopened->restore(persistencePath.c_str(), true, {});
|
||||
persistenceStage = "get-restored-feature";
|
||||
auto* restored = dynamic_cast<Part::Feature*>(reopened->getObject("ReverseEngineeringCurve"));
|
||||
if (!restored) throw Base::RuntimeError("ReverseEngineering FCStd restore omitted curve feature");
|
||||
persistenceStage = "recompute-restored";
|
||||
bool hasError = false; reopened->recompute({}, false, &hasError); GProp_GProps after; BRepGProp::LinearProperties(restored->Shape.getValue(), after);
|
||||
persistenceAfterLength = after.Mass(); persistenceObjectType = restored->getTypeId().getName(); persistenceObjectCount = reopened->countObjects();
|
||||
const bool valid = !hasError && persistenceSize > 0 && persistenceObjectCount == 1 && persistenceObjectType == "Part::Feature" && std::abs(persistenceBeforeLength - persistenceAfterLength) < 1e-9 && persistenceAfterLength > 0.0;
|
||||
application.closeDocument(reopened); reopened = nullptr;
|
||||
if (!valid || !application.getDocuments().empty()) throw Base::RuntimeError("ReverseEngineering FCStd roundtrip mismatch");
|
||||
return 1;
|
||||
} catch (const Base::Exception& error) { persistenceError = persistenceStage + " [" + fc_web_document_restore_stage() + "]: " + error.what(); }
|
||||
catch (const std::exception& error) { persistenceError = persistenceStage + " [" + fc_web_document_restore_stage() + "]: " + error.what(); }
|
||||
catch (...) { persistenceError = persistenceStage + ": unknown ReverseEngineering save/reopen failure"; }
|
||||
if (document) App::GetApplication().closeDocument(document); if (reopened) App::GetApplication().closeDocument(reopened); return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_persistence_error() { return persistenceError.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_persistence_set_path(const char* path)
|
||||
{
|
||||
requestedPersistenceError.clear();
|
||||
if (!path || !*path) { requestedPersistencePath.clear(); requestedPersistenceError = "invalid_path"; return 0; }
|
||||
const std::string value(path);
|
||||
if (value.size() < 6 || value.substr(value.size() - 6) != ".FCStd" || value.find("missing-directory") != std::string::npos) { requestedPersistencePath.clear(); requestedPersistenceError = "invalid_path_extension"; return 0; }
|
||||
requestedPersistencePath = path;
|
||||
return 1;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_persistence_path() { return persistencePath.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_reverseengineering_persistence_object_type() { return persistenceObjectType.c_str(); }
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_reverseengineering_persistence_before_length() { return persistenceBeforeLength; }
|
||||
EMSCRIPTEN_KEEPALIVE double fc_web_reverseengineering_persistence_after_length() { return persistenceAfterLength; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_persistence_size() { return persistenceSize; }
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_reverseengineering_persistence_object_count() { return persistenceObjectCount; }
|
||||
}
|
||||
6
src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp
Normal file
6
src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <string>
|
||||
extern "C" int fc_web_robot_initialize(); extern "C" int fc_web_robot_type_count(); extern "C" const char* fc_web_robot_type_names(); extern "C" int fc_web_robot_python_api(); extern "C" const char* fc_web_robot_error();
|
||||
namespace { std::string error; const char* entry="src/Mod/Robot/App/AppRobot.cpp"; }
|
||||
extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_robot_source_inventory_validate(const char* source){if(!source||std::string(source)!=entry){error="revision_mismatch";return 0;}if(fc_web_robot_initialize()!=1){error=fc_web_robot_error();return 0;}error.clear();return 1;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_entry(){return entry;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_source_summary(){return "Robot App; bundled KDL; Part/FreeCADApp dependencies; GUI excluded";} EMSCRIPTEN_KEEPALIVE int fc_web_robot_registered_type_count(){return fc_web_robot_type_count();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_binding_count(){return fc_web_robot_python_api()?3:0;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_inventory_resources_released(){return 1;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_inventory_error(){return error.c_str();} }
|
||||
12
src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp
Normal file
12
src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <Base/Exception.h>
|
||||
#include <Mod/Robot/App/RobotObject.h>
|
||||
#include <Python.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <string>
|
||||
extern "C" PyObject* PyInit_Robot(); extern "C" PyObject* PyInit_Part(); extern "C" PyObject* PyInit_Materials();
|
||||
namespace { std::string error, type; int calls=0,runs=0,available=0,count=0,resources=0; float axis=0; std::string names; }
|
||||
namespace { std::string pyError(){PyObject*t=nullptr,*v=nullptr,*tb=nullptr;PyErr_Fetch(&t,&v,&tb);PyErr_NormalizeException(&t,&v,&tb);std::string m="PythonError";if(v){PyObject*x=PyObject_Str(v);if(x){m=PyUnicode_AsUTF8(x);Py_DECREF(x);}}Py_XDECREF(t);Py_XDECREF(v);Py_XDECREF(tb);return m;}void add(const char*n,PyObject*(*f)()){auto*d=PyImport_GetModuleDict();if(PyDict_GetItemString(d,n))return;PyObject*m=f();if(!m)throw Base::RuntimeError(std::string("PyInit failed: ")+n+": "+pyError());PyDict_SetItemString(d,n,m);Py_DECREF(m);}bool init(){++calls;if(available)return true;++runs;try{App::Application::initWebCore();add("Materials",&PyInit_Materials);add("Part",&PyInit_Part);add("Robot",&PyInit_Robot);const char*ts[]={"Robot::RobotObject","Robot::TrajectoryObject","Robot::Edge2TracObject","Robot::TrajectoryCompound","Robot::TrajectoryDressUpObject"};count=0;names.clear();for(auto*n:ts)if(!Base::Type::fromName(n).isBad()){++count;if(!names.empty())names+=",";names+=n;}if(count!=5){error="registration_mismatch";return false;}available=1;error.clear();return true;}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Robot initialization failure";}return false;}}
|
||||
extern "C" { EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialize(){error.clear();return init()?1:0;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_calls(){return calls;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_initialization_runs(){return runs;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_module_available(){return available;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_error(){return error.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_probe(const char*n){if(!init())return 0;if(!n||!*n){error="invalid_argument";return 0;}if(Base::Type::fromName(n).isBad()){error="type_not_found";return 0;}error.clear();return 1;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_python_api(){if(!init())return 0;PyObject*m=PyImport_ImportModule("Robot");if(!m){error=pyError();return 0;}int n=PyObject_HasAttrString(m,"Robot6Axis")+PyObject_HasAttrString(m,"Waypoint")+PyObject_HasAttrString(m,"Trajectory");Py_DECREF(m);return n==3;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_type_names(){return names.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_type_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_create_recompute(){error.clear();type.clear();count=0;resources=0;axis=0;App::Document*d=nullptr;try{if(!init())return 0;auto&app=App::GetApplication();d=app.newDocument("RobotCreate","Robot Create",{false,true});auto*o=d?dynamic_cast<Robot::RobotObject*>(d->addObject("Robot::RobotObject","Robot")):nullptr;if(!o)throw Base::RuntimeError("Robot object creation failed");o->Axis1.setValue(15.0F);o->Axis2.setValue(-10.0F);bool e=false;d->recompute({},false,&e);axis=o->Axis1.getValue();type=o->getTypeId().getName();count=d->countObjects();bool ok=!e&&type=="Robot::RobotObject"&&count==1;app.closeDocument(d);d=nullptr;resources=app.getDocuments().empty();if(!ok||!resources){error="create_recompute_mismatch";return 0;}return 1;}catch(const std::exception&e){error=e.what();}catch(...){error="unknown Robot create failure";}if(d)App::GetApplication().closeDocument(d);return 0;} EMSCRIPTEN_KEEPALIVE const char* fc_web_robot_action_object_type(){return type.c_str();} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_object_count(){return count;} EMSCRIPTEN_KEEPALIVE int fc_web_robot_action_resources_released(){return resources;} EMSCRIPTEN_KEEPALIVE float fc_web_robot_action_axis1(){return axis;} }
|
||||
@@ -18,6 +18,7 @@
|
||||
extern "C" PyObject* PyInit_Materials();
|
||||
extern "C" PyObject* PyInit_Part();
|
||||
extern "C" PyObject* PyInit_Sketcher();
|
||||
extern "C" const char* fc_web_document_restore_stage();
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -43,6 +44,11 @@ int persistenceGeometryCount = 0;
|
||||
int persistenceConstraintCount = 0;
|
||||
int persistenceRoundTrip = 0;
|
||||
int persistenceResourcesReleased = 0;
|
||||
std::string recoveryInvalidError;
|
||||
int recoveryGeometryCount = 0;
|
||||
int recoveryConstraintCount = 0;
|
||||
int recoveryResourcesReleased = 0;
|
||||
int recoveryRecovered = 0;
|
||||
|
||||
void addPythonModule(const char* name, PyObject* (*initialize)())
|
||||
{
|
||||
@@ -226,11 +232,72 @@ EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_save_reopen()
|
||||
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(); }
|
||||
catch (const Base::Exception& error) { persistenceError = std::string(fc_web_document_restore_stage()) + ": " + error.what(); }
|
||||
catch (const std::exception& error) { persistenceError = std::string(fc_web_document_restore_stage()) + ": " + 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 int fc_web_sketcher_restore_recovery()
|
||||
{
|
||||
recoveryInvalidError.clear(); recoveryGeometryCount = 0; recoveryConstraintCount = 0;
|
||||
recoveryResourcesReleased = 0; recoveryRecovered = 0;
|
||||
App::Document* source = nullptr;
|
||||
App::Document* target = nullptr;
|
||||
constexpr const char* validPath = "/tmp/SketcherWebRecovery.FCStd";
|
||||
constexpr const char* missingPath = "/tmp/SketcherWebRecoveryMissing.FCStd";
|
||||
try {
|
||||
ensureSketcherInitialized();
|
||||
auto& application = App::GetApplication();
|
||||
source = application.newDocument("SketcherRecoverySource", "Sketcher Recovery Source", {false, true});
|
||||
auto* sketch = dynamic_cast<Sketcher::SketchObject*>(source->addObject("Sketcher::SketchObject", "Sketch"));
|
||||
if (!sketch) throw Base::RuntimeError("Sketcher recovery source 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;
|
||||
source->recompute({}, false, &hasError);
|
||||
if (hasError || !source->saveAs(validPath)) throw Base::RuntimeError("Sketcher recovery save failed");
|
||||
application.closeDocument(source); source = nullptr;
|
||||
|
||||
target = application.newDocument("SketcherRecoveryTarget", "Sketcher Recovery Target", {false, true});
|
||||
try {
|
||||
target->restore(missingPath, true, {});
|
||||
}
|
||||
catch (const Base::Exception& error) {
|
||||
recoveryInvalidError = error.what();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
recoveryInvalidError = error.what();
|
||||
}
|
||||
target->restore(validPath, true, {});
|
||||
auto* restored = dynamic_cast<Sketcher::SketchObject*>(target->getObject("Sketch"));
|
||||
if (!restored) throw Base::RuntimeError("Sketcher recovery restore omitted SketchObject");
|
||||
recoveryGeometryCount = static_cast<int>(restored->Geometry.getSize());
|
||||
recoveryConstraintCount = static_cast<int>(restored->Constraints.getSize());
|
||||
hasError = false;
|
||||
target->recompute({}, false, &hasError);
|
||||
recoveryRecovered = recoveryInvalidError.find("Unable to open project file") != std::string::npos
|
||||
&& recoveryGeometryCount == 1 && recoveryConstraintCount == 1 && !hasError ? 1 : 0;
|
||||
application.closeDocument(target); target = nullptr;
|
||||
recoveryResourcesReleased = application.getDocuments().empty() ? 1 : 0;
|
||||
return recoveryRecovered && recoveryResourcesReleased ? 1 : 0;
|
||||
}
|
||||
catch (const Base::Exception& error) { recoveryInvalidError = error.what(); }
|
||||
catch (const std::exception& error) { recoveryInvalidError = error.what(); }
|
||||
if (source) App::GetApplication().closeDocument(source);
|
||||
if (target) App::GetApplication().closeDocument(target);
|
||||
recoveryResourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE const char* fc_web_sketcher_recovery_invalid_error(){return recoveryInvalidError.c_str();}
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_recovery_geometry_count(){return recoveryGeometryCount;}
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_recovery_constraint_count(){return recoveryConstraintCount;}
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_recovery_recovered(){return recoveryRecovered;}
|
||||
EMSCRIPTEN_KEEPALIVE int fc_web_sketcher_recovery_resources_released(){return recoveryResourcesReleased;}
|
||||
|
||||
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;}
|
||||
|
||||
Reference in New Issue
Block a user