From 2778e3f9c215da48bf0cfd562478d45d624aaa47 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Tue, 25 Aug 2026 04:56:08 +0800 Subject: [PATCH] Restore GUI-free Web module runtimes --- CMakeLists.txt | 14 + src/App/Document.cpp | 94 +++-- src/App/PropertyContainer.cpp | 3 + src/Base/Reader.cpp | 47 ++- src/Mod/CMakeLists.txt | 6 +- src/Mod/Material/App/PropertyMaterial.cpp | 16 +- src/Mod/Part/App/PropertyTopoShape.cpp | 69 +++- src/Mod/Plot/Plot.py | 348 ++++++++++++++++-- .../ReverseEngineering/App/ApproxSurface.cpp | 29 +- src/Mod/ReverseEngineering/App/PreCompiled.h | 10 +- src/Mod/Sketcher/App/SketchObject.cpp | 10 +- .../FreeCADCoreProbe/CMakeLists.txt | 56 ++- .../PartDesignRuntimeProbe.cpp | 5 +- .../FreeCADCoreProbe/PlotRuntimeProbe.cpp | 277 ++++++++++++++ .../ReverseEngineeringRuntimeProbe.cpp | 283 ++++++++++++++ .../FreeCADCoreProbe/RobotInventoryApi.cpp | 6 + .../RobotRuntimeProbeFixed.cpp | 12 + .../FreeCADCoreProbe/SketcherProbe.cpp | 71 +++- 18 files changed, 1257 insertions(+), 99 deletions(-) create mode 100644 src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp create mode 100644 src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f44103a..6a6b6d12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/src/App/Document.cpp b/src/App/Document.cpp index b6212626..af0f1300 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -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 : ""); +} static bool globalIsRestoring; static bool globalIsRelabeling; @@ -1118,14 +1130,16 @@ void Document::Save(Base::Writer& writer) const writer.Stream() << "" << '\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("SchemaVersion"); reader.DocumentSchema = static_cast(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 Document::readObjects(Base::XMLReader& reader) // read the object types - reader.readElement("Objects"); + webRestoreStage = "objects-header"; + reader.readElement("Objects"); int Cnt = static_cast(reader.getAttribute("Count")); if (!reader.hasAttribute(fcAttrDependencies)) { @@ -1598,6 +1618,7 @@ std::vector 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("type"); std::string name = reader.getAttribute("name"); @@ -1686,20 +1707,23 @@ std::vector Document::readObjects(Base::XMLReader& reader) // read the features itself reader.clearPartialRestoreDocumentObject(); - reader.readElement("ObjectData"); + webRestoreStage = "object-data-header"; + reader.readElement("ObjectData"); Cnt = static_cast(reader.getAttribute("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("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 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(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 zipstreamPtr; try { zipstreamPtr = std::make_unique(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); diff --git a/src/App/PropertyContainer.cpp b/src/App/PropertyContainer.cpp index 90d2b688..bd68df77 100644 --- a/src/App/PropertyContainer.cpp +++ b/src/App/PropertyContainer.cpp @@ -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 diff --git a/src/Base/Reader.cpp b/src/Base/Reader.cpp index f4df1958..8806ef0a 100644 --- a/src/Base/Reader.cpp +++ b/src/Base/Reader.cpp @@ -49,8 +49,12 @@ #include #include -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::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::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 diff --git a/src/Mod/CMakeLists.txt b/src/Mod/CMakeLists.txt index 4d65209e..c3711654 100644 --- a/src/Mod/CMakeLists.txt +++ b/src/Mod/CMakeLists.txt @@ -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) diff --git a/src/Mod/Material/App/PropertyMaterial.cpp b/src/Mod/Material/App/PropertyMaterial.cpp index 232e91b3..91758e9f 100644 --- a/src/Mod/Material/App/PropertyMaterial.cpp +++ b/src/Mod/Material/App/PropertyMaterial.cpp @@ -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 diff --git a/src/Mod/Part/App/PropertyTopoShape.cpp b/src/Mod/Part/App/PropertyTopoShape.cpp index 91f8bf84..130517d7 100644 --- a/src/Mod/Part/App/PropertyTopoShape.cpp +++ b/src/Mod/Part/App/PropertyTopoShape.cpp @@ -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(); } diff --git a/src/Mod/Plot/Plot.py b/src/Mod/Plot/Plot.py index 25761793..1d36088e 100644 --- a/src/Mod/Plot/Plot.py +++ b/src/Mod/Plot/Plot.py @@ -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'', + '', + ] + 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'') + 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'') + if line.name: + lines.append(f'{_xml_escape(line.name)}') + if axis.title: + lines.append(f'{_xml_escape(axis.title)}') + if axis.xlabel: + lines.append(f'{_xml_escape(axis.xlabel)}') + if axis.ylabel: + lines.append(f'{_xml_escape(axis.ylabel)}') + lines.append('') + 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 diff --git a/src/Mod/ReverseEngineering/App/ApproxSurface.cpp b/src/Mod/ReverseEngineering/App/ApproxSurface.cpp index fad3dc56..30583243 100644 --- a/src/Mod/ReverseEngineering/App/ApproxSurface.cpp +++ b/src/Mod/ReverseEngineering/App/ApproxSurface.cpp @@ -22,9 +22,11 @@ * * ***************************************************************************/ -#include -#include -#include +#ifndef FREECAD_WEB_CORE +# include +# include +# include +#endif #include #include @@ -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 columns(ulDim); std::generate(columns.begin(), columns.end(), Base::iotaGen(0)); ScalarProduct scalar(M); diff --git a/src/Mod/ReverseEngineering/App/PreCompiled.h b/src/Mod/ReverseEngineering/App/PreCompiled.h index 22291509..487cb921 100644 --- a/src/Mod/ReverseEngineering/App/PreCompiled.h +++ b/src/Mod/ReverseEngineering/App/PreCompiled.h @@ -45,7 +45,9 @@ #include #include -// Qt -#include -#include -#include +// Qt +#ifndef FREECAD_WEB_CORE +# include +# include +# include +#endif diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 29f1c45c..efe08171 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -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) diff --git a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt index 7254736c..4bab2448 100644 --- a/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt +++ b/src/WebAssembly/FreeCADCoreProbe/CMakeLists.txt @@ -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 diff --git a/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp index f4d09584..71e0defb 100644 --- a/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/PartDesignRuntimeProbe.cpp @@ -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); diff --git a/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp new file mode 100644 index 00000000..d3d3658e --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/PlotRuntimeProbe.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include + +#include +#include + +#include +#include +#include + +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(source)), std::istreambuf_iterator()); + 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(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(svg.tellg()); + svg.seekg(0); + std::string content(static_cast(svgSize), '\0'); + svg.read(content.data(), content.size()); + svgValid = content.find(" 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" diff --git a/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp new file mode 100644 index 00000000..e468b74d --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/ReverseEngineeringRuntimeProbe.cpp @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(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(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(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; } +} diff --git a/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp b/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp new file mode 100644 index 00000000..0c847a03 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/RobotInventoryApi.cpp @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +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();} } diff --git a/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp b/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp new file mode 100644 index 00000000..ec7bcaf3 --- /dev/null +++ b/src/WebAssembly/FreeCADCoreProbe/RobotRuntimeProbeFixed.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +#include +#include +#include +#include +#include +#include +#include +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(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;} } diff --git a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp index 20ae4974..3cee8371 100644 --- a/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp +++ b/src/WebAssembly/FreeCADCoreProbe/SketcherProbe.cpp @@ -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(source->addObject("Sketcher::SketchObject", "Sketch")); + if (!sketch) throw Base::RuntimeError("Sketcher recovery source creation failed"); + addFixedLine(*sketch); + auto constraint = std::make_unique(); + 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(target->getObject("Sketch")); + if (!restored) throw Base::RuntimeError("Sketcher recovery restore omitted SketchObject"); + recoveryGeometryCount = static_cast(restored->Geometry.getSize()); + recoveryConstraintCount = static_cast(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;}