390 lines
15 KiB
C++
390 lines
15 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
#include <App/Application.h>
|
|
#include <Base/Exception.h>
|
|
|
|
#include <Python.h>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <emscripten/emscripten.h>
|
|
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
constexpr const char* sourceEntry = "src/Mod/Show/__init__.py";
|
|
constexpr const char* sourcePath = "/freecad-mod/Show/__init__.py";
|
|
|
|
std::string errorMessage;
|
|
std::string apiError;
|
|
int initializationCalls = 0;
|
|
int initializationRuns = 0;
|
|
int moduleAvailable = 0;
|
|
int apiCount = 0;
|
|
int tempoState = -1;
|
|
int resourcesReleased = 0;
|
|
int dependencyProbe = 0;
|
|
std::string familyDispatchResponse;
|
|
constexpr const char* sourceRevision = "2778e3f9c215da48bf0cfd562478d45d624aaa47";
|
|
|
|
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 addShowPath()
|
|
{
|
|
const char* script =
|
|
"import sys\n"
|
|
"_show_path = '/freecad-mod'\n"
|
|
"if _show_path not in sys.path:\n"
|
|
" sys.path.insert(0, _show_path)\n";
|
|
if (PyRun_SimpleString(script) != 0) {
|
|
errorMessage = pythonError();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool exposeGuiFreeFlag()
|
|
{
|
|
PyObject* freecad = PyImport_ImportModule("FreeCAD");
|
|
if (!freecad) {
|
|
errorMessage = pythonError();
|
|
return false;
|
|
}
|
|
const int result = PyObject_SetAttrString(freecad, "GuiUp", Py_False);
|
|
Py_DECREF(freecad);
|
|
if (result != 0) {
|
|
errorMessage = pythonError();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ensureRuntime()
|
|
{
|
|
++initializationCalls;
|
|
if (moduleAvailable) {
|
|
return true;
|
|
}
|
|
try {
|
|
App::Application::initWebCore();
|
|
if (!exposeGuiFreeFlag()) {
|
|
return false;
|
|
}
|
|
if (!addShowPath()) {
|
|
return false;
|
|
}
|
|
PyObject* module = PyImport_ImportModule("Show");
|
|
if (!module) {
|
|
errorMessage = pythonError();
|
|
return false;
|
|
}
|
|
const bool hasTempoVis = PyObject_HasAttrString(module, "TempoVis") == 1;
|
|
const bool hasDepGraph = PyObject_HasAttrString(module, "DepGraphTools") == 1;
|
|
Py_DECREF(module);
|
|
if (!hasTempoVis || !hasDepGraph) {
|
|
errorMessage = "registration_mismatch";
|
|
return false;
|
|
}
|
|
moduleAvailable = 1;
|
|
++initializationRuns;
|
|
resourcesReleased = App::GetApplication().getDocuments().empty() ? 1 : 0;
|
|
errorMessage.clear();
|
|
return true;
|
|
}
|
|
catch (const Base::Exception& error) {
|
|
errorMessage = error.what();
|
|
}
|
|
catch (const std::exception& error) {
|
|
errorMessage = error.what();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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 = PyImport_ImportModule("Show");
|
|
if (!module) {
|
|
apiError = pythonError();
|
|
return 0;
|
|
}
|
|
const char* names[] = {"TempoVis", "DepGraphTools"};
|
|
int count = 0;
|
|
for (const char* name : names) {
|
|
count += PyObject_HasAttrString(module, name) ? 1 : 0;
|
|
}
|
|
Py_DECREF(module);
|
|
return count;
|
|
}
|
|
|
|
std::string moduleForSourcePath(const QString& sourcePath)
|
|
{
|
|
QString module = sourcePath;
|
|
if (module.startsWith(QStringLiteral("src/Mod/"))) module = module.mid(8);
|
|
if (module.endsWith(QStringLiteral(".py"))) module.chop(3);
|
|
module.replace('/', '.');
|
|
return module.toStdString();
|
|
}
|
|
|
|
QJsonObject probeFamilyRecord(const QJsonObject& input)
|
|
{
|
|
const QString apiId = input.value(QStringLiteral("apiId")).toString();
|
|
const QString name = input.value(QStringLiteral("name")).toString();
|
|
const QString family = input.value(QStringLiteral("family")).toString();
|
|
const QString sourcePath = input.value(QStringLiteral("sourcePath")).toString();
|
|
QJsonObject result;
|
|
result.insert(QStringLiteral("apiId"), apiId);
|
|
result.insert(QStringLiteral("name"), name);
|
|
result.insert(QStringLiteral("family"), family);
|
|
result.insert(QStringLiteral("native"), true);
|
|
result.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty());
|
|
result.insert(QStringLiteral("sourceEntry"), sourcePath);
|
|
result.insert(QStringLiteral("sourceRevision"), QString::fromUtf8(sourceRevision));
|
|
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
|
|
result.insert(QStringLiteral("returnSchema"), QStringLiteral("source-declared"));
|
|
if (!apiId.startsWith(QStringLiteral("py:Show:")) || name.isEmpty() || family.isEmpty() || sourcePath.isEmpty()) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("not_implemented"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("unmatched_source_record"));
|
|
result.insert(QStringLiteral("message"), QStringLiteral("Show API record is not part of this module-scoped adapter"));
|
|
return result;
|
|
}
|
|
if (name == QStringLiteral("Workbench")) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("restricted"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("gui_dependency_unavailable"));
|
|
result.insert(QStringLiteral("message"), QStringLiteral("Show Workbench API requires FreeCADGui unavailable in the headless Web runtime"));
|
|
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
|
|
result.insert(QStringLiteral("returnSchema"), QStringLiteral("gui-resource"));
|
|
return result;
|
|
}
|
|
const std::string moduleName = moduleForSourcePath(sourcePath);
|
|
PyObject* module = PyImport_ImportModule(moduleName.c_str());
|
|
if (!module) {
|
|
PyErr_Clear();
|
|
result.insert(QStringLiteral("status"), QStringLiteral("not_implemented"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("native_python_import_failed"));
|
|
result.insert(QStringLiteral("message"), QStringLiteral("Show Python source module could not be imported by the headless runtime"));
|
|
result.insert(QStringLiteral("parameterSchema"), QStringLiteral("source-declared"));
|
|
return result;
|
|
}
|
|
bool resolved = false;
|
|
if (name.contains(QStringLiteral("."))) {
|
|
const auto parts = name.split('.');
|
|
if (parts.size() == 2) {
|
|
PyObject* owner = PyObject_GetAttrString(module, parts[0].toUtf8().constData());
|
|
resolved = owner && PyObject_HasAttrString(owner, parts[1].toUtf8().constData());
|
|
Py_XDECREF(owner);
|
|
}
|
|
} else {
|
|
resolved = PyObject_HasAttrString(module, name.toUtf8().constData());
|
|
}
|
|
Py_DECREF(module);
|
|
result.insert(QStringLiteral("status"), resolved ? QStringLiteral("exposed") : QStringLiteral("not_implemented"));
|
|
result.insert(QStringLiteral("code"), resolved ? (name.contains('.') ? QStringLiteral("native_object_method") : QStringLiteral("native_python_symbol")) : QStringLiteral("native_python_symbol_probe_failed"));
|
|
result.insert(QStringLiteral("message"), resolved ? QStringLiteral("Show Python symbol/method is registered and resolved by FreeCAD") : QStringLiteral("Show Python symbol/method could not be resolved by the headless runtime"));
|
|
result.insert(QStringLiteral("parameterSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("source-declared"));
|
|
result.insert(QStringLiteral("returnSchema"), name.contains('.') ? QStringLiteral("source-declared") : QStringLiteral("Show.%1").arg(name));
|
|
if (resolved) result.insert(QStringLiteral("replay"), name.contains('.') ? QStringLiteral("native-object-method") : QStringLiteral("native-python-reflection"));
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialize()
|
|
{
|
|
errorMessage.clear();
|
|
return ensureRuntime() ? 1 : 0;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_calls() { return initializationCalls; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_initialization_runs() { return initializationRuns; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_module_available() { return moduleAvailable; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_last_error() { return errorMessage.c_str(); }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_entry() { return sourceEntry; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_source_path() { return sourcePath; }
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_source_inventory_validate(const char* entry)
|
|
{
|
|
if (!entry || std::string(entry) != sourceEntry) {
|
|
errorMessage = "revision_mismatch";
|
|
return 0;
|
|
}
|
|
return ensureRuntime() ? 1 : 0;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_validate(const char* entry)
|
|
{
|
|
if (!entry || std::string(entry) != sourceEntry) {
|
|
apiError = "revision_mismatch";
|
|
return 0;
|
|
}
|
|
if (!ensureRuntime()) {
|
|
apiError = errorMessage;
|
|
return 0;
|
|
}
|
|
apiCount = countApi();
|
|
apiError.clear();
|
|
return apiCount == 2 ? 1 : 0;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api()
|
|
{
|
|
if (!ensureRuntime()) {
|
|
apiError = errorMessage;
|
|
return 0;
|
|
}
|
|
const char* script =
|
|
"import FreeCAD as App\n"
|
|
"import Show\n"
|
|
"from Show.SceneDetail import SceneDetail\n"
|
|
"from Show import Containers, TVStack\n"
|
|
"doc = App.newDocument('ShowWeb')\n"
|
|
"tv = Show.TempoVis(doc, None, tag='web-fixture')\n"
|
|
"_show_tempo_state = tv.state\n"
|
|
"_show_api_count = sum(hasattr(Show, name) for name in ('TempoVis', 'DepGraphTools'))\n"
|
|
"_show_scene_detail = SceneDetail.__name__\n"
|
|
"_show_container = Containers.Container.__name__\n"
|
|
"_show_stack = TVStack.TVStack.__name__\n"
|
|
"tv.restore()\n"
|
|
"App.closeDocument(doc.Name)\n"
|
|
"_show_resources_released = len(App.listDocuments()) == 0\n";
|
|
PyObject* globals = nullptr;
|
|
if (!executeScript(script, &globals)) {
|
|
apiError = errorMessage;
|
|
return 0;
|
|
}
|
|
int scriptApiCount = 0;
|
|
int released = 0;
|
|
const bool valid = readInt(globals, "_show_api_count", scriptApiCount)
|
|
&& readInt(globals, "_show_tempo_state", tempoState)
|
|
&& readInt(globals, "_show_resources_released", released);
|
|
Py_DECREF(globals);
|
|
apiCount = countApi();
|
|
resourcesReleased = released;
|
|
dependencyProbe = valid && scriptApiCount == 2 && tempoState == 0 ? 1 : 0;
|
|
apiError.clear();
|
|
return valid && apiCount == 2 && scriptApiCount == 2 && tempoState == 0 && resourcesReleased == 1;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_probe(const char* name)
|
|
{
|
|
if (!ensureRuntime()) {
|
|
dependencyProbe = 0;
|
|
return 0;
|
|
}
|
|
if (!name || !*name) {
|
|
errorMessage = "invalid_argument";
|
|
dependencyProbe = 0;
|
|
return 0;
|
|
}
|
|
PyObject* module = PyImport_ImportModule("Show");
|
|
if (!module) {
|
|
errorMessage = pythonError();
|
|
dependencyProbe = 0;
|
|
return 0;
|
|
}
|
|
const bool known = PyObject_HasAttrString(module, name) == 1;
|
|
Py_DECREF(module);
|
|
if (!known) {
|
|
errorMessage = "type_not_found";
|
|
dependencyProbe = 0;
|
|
return 0;
|
|
}
|
|
errorMessage.clear();
|
|
dependencyProbe = 1;
|
|
return 1;
|
|
}
|
|
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_python_api_count() { return apiCount; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_tempo_state() { return tempoState; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_resources_released() { return resourcesReleased; }
|
|
EMSCRIPTEN_KEEPALIVE int fc_web_show_dependency_available() { return dependencyProbe; }
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_show_api_error() { return apiError.c_str(); }
|
|
|
|
EMSCRIPTEN_KEEPALIVE const char* fc_web_dispatch_json(const char* request)
|
|
{
|
|
familyDispatchResponse.clear();
|
|
if (!request || *request == '\0') { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch request is empty"})"; return familyDispatchResponse.c_str(); }
|
|
QJsonParseError parseError;
|
|
const auto document = QJsonDocument::fromJson(QByteArray(request), &parseError);
|
|
if (!document.isObject()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"Show native dispatch requires a JSON object"})"; return familyDispatchResponse.c_str(); }
|
|
const auto object = document.object();
|
|
const auto command = object.value(QStringLiteral("cmd")).toString();
|
|
if (command != QStringLiteral("show.pythonFamilyBatch")) { familyDispatchResponse = R"({"ok":false,"code":"unsupported_command","error":"Show native dispatch command is not registered"})"; return familyDispatchResponse.c_str(); }
|
|
const auto args = object.value(QStringLiteral("args")).toObject();
|
|
const auto tier = args.value(QStringLiteral("tier")).toString();
|
|
const auto records = args.value(QStringLiteral("records")).toArray();
|
|
if ((tier != QStringLiteral("tier-1-readonly") && tier != QStringLiteral("tier-2-pure")) || records.isEmpty()) { familyDispatchResponse = R"({"ok":false,"code":"invalid_request","error":"show.pythonFamilyBatch requires tier=tier-1-readonly or tier=tier-2-pure and non-empty args.records"})"; return familyDispatchResponse.c_str(); }
|
|
if (!ensureRuntime()) { QJsonObject failure; failure.insert(QStringLiteral("ok"), false); failure.insert(QStringLiteral("code"), QStringLiteral("runtime_unavailable")); failure.insert(QStringLiteral("error"), QString::fromStdString(errorMessage)); familyDispatchResponse = QJsonDocument(failure).toJson(QJsonDocument::Compact).toStdString(); return familyDispatchResponse.c_str(); }
|
|
QJsonArray outputRecords;
|
|
for (const auto& value : records) outputRecords.push_back(probeFamilyRecord(value.toObject()));
|
|
QJsonObject output;
|
|
output.insert(QStringLiteral("ok"), true);
|
|
output.insert(QStringLiteral("cmd"), command);
|
|
output.insert(QStringLiteral("schema"), QStringLiteral("freecad-web-show/python-family-batch/v1"));
|
|
output.insert(QStringLiteral("module"), QStringLiteral("Show"));
|
|
output.insert(QStringLiteral("tier"), tier);
|
|
output.insert(QStringLiteral("source"), QStringLiteral("freecad-source"));
|
|
output.insert(QStringLiteral("records"), outputRecords);
|
|
output.insert(QStringLiteral("count"), outputRecords.size());
|
|
output.insert(QStringLiteral("resourcesReleased"), App::GetApplication().getDocuments().empty());
|
|
familyDispatchResponse = QJsonDocument(output).toJson(QJsonDocument::Compact).toStdString();
|
|
return familyDispatchResponse.c_str();
|
|
}
|
|
|
|
} // extern "C"
|