// SPDX-License-Identifier: LGPL-2.1-or-later #include #include #include #include #include #include #include #include #include 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(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"