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

This commit is contained in:
2026-08-25 04:56:08 +08:00
parent 0a2e7ce7f5
commit 2778e3f9c2
18 changed files with 1257 additions and 99 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("&", "&amp;").replace("<", "&lt;")
.replace(">", "&gt;").replace('"', "&quot;").replace("'", "&apos;"))
_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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"

View File

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

View 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();} }

View 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;} }

View File

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