Files
FreeCADWasmDev/freecad-source/src/WebAssembly/FreeCADCoreProbe/WebOpenCommand.h
wangdequan 72152ef356 Save verified native IFC import and user validation evidence
Real IFC2X3/IFC4 import tested. Full BIM authoring/export remains out of scope; 349-second building import performance remains to optimize.
2026-09-16 04:27:37 +08:00

151 lines
8.4 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <Python.h>
#include <string>
#include <App/Application.h>
#include <App/Document.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
static bool executeWebOpenCommand(const char* input, std::string& output)
{
PyObject* scope = PyDict_New();
PyObject* request = PyUnicode_FromString(input);
if (!scope || !request || PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()) < 0
|| PyDict_SetItemString(scope, "request_text", request) < 0) {
Py_XDECREF(request); Py_XDECREF(scope); return false;
}
Py_DECREF(request);
PyObject* executed = PyRun_String(R"PY(
import FreeCAD as App, json, os, base64, hashlib, importlib
record = json.loads(request_text)['records'][0]
args = record.get('args', {})
reply = {'apiId': record['apiId'], 'name': 'Std_Open'}
try:
if args.get('activeTask'):
raise ValueError('Finish the active task before opening files')
# Reuse the Part registrations from Mod/Part/Init.py. STEP with colors
# belongs to Import, not a geometry-only substitute in another module.
import Part
try:
import IfcOpenShellNative
if not App.getImportType('ifc'):
App.addImportType('Industry Foundation Classes (*.ifc *.IFC)', 'IfcOpenShellNative')
except ImportError:
pass
try:
import Mesh
if not App.getImportType('obj'): App.addImportType('Wavefront OBJ (*.obj *.OBJ)', 'Mesh')
except ImportError:
pass
for extension, description in (('brep', 'BREP (*.brep *.BREP *.brp *.BRP)'),
('iges', 'IGES (*.iges *.IGES *.igs *.IGS)')):
if not App.getImportType(extension): App.addImportType(description, 'Part')
formats = [{'extension': 'FCStd', 'modules': ['FreeCAD']}]
for extension, modules in App.getImportType().items():
if extension.lower() == 'fcstd': continue
modules = list(dict.fromkeys([modules] if isinstance(modules, str) else list(modules or [])))
if modules: formats.append({'extension': extension, 'modules': modules})
operation = args.get('operation', 'open')
if operation not in ('list', 'open'):
raise ValueError('Open operation must be list or open')
files = args.get('files', [])
if not isinstance(files, list) or operation == 'open' and not files:
raise ValueError('Select one or more files')
opened, errors = [], []
if operation == 'open':
for entry in files:
before = set(App.listDocuments())
previous = App.activeDocument()
previous_name = previous.Name if previous else ''
filename = entry.get('filename', '') if isinstance(entry, dict) else ''
try:
if not isinstance(filename, str) or not filename or filename in ('.', '..') or '/' in filename or chr(92) in filename:
raise ValueError('Filename must not contain directory components')
data = base64.b64decode(entry['fileData'], validate=True)
if not data: raise ValueError('The selected file is empty')
digest = hashlib.sha256(data).hexdigest()
directory = os.path.join(App.getUserAppDataDir(), 'WebOpenFiles', digest)
path = os.path.join(directory, filename)
existing = next((doc for doc in App.listDocuments().values() if doc.FileName == path), None)
if existing:
App.setActiveDocument(existing.Name)
documents = [existing]
else:
os.makedirs(directory, exist_ok=True)
with open(path, 'wb') as stream: stream.write(data)
extension = os.path.splitext(filename)[1][1:]
if extension.lower() == 'fcstd':
document = App.openDocument(path)
if document is None: raise RuntimeError('Native document restore returned no document')
App.setActiveDocument(document.Name)
else:
modules = App.getImportType(extension) or App.getImportType(extension.lower())
modules = list(dict.fromkeys([modules] if isinstance(modules, str) else list(modules or [])))
module_name = entry.get('module')
if not modules: raise ValueError('No native import handler is registered for .' + extension)
if not module_name:
if len(modules) != 1: raise ValueError('Choose an import handler: ' + ', '.join(modules))
module_name = modules[0]
if module_name not in modules: raise ValueError('Import handler is not registered for this extension')
module = importlib.import_module(module_name)
if getattr(module, '_freecad_import_stub', False) or not callable(getattr(module, 'open', None)):
raise RuntimeError('Native import handler is not available in this runtime: ' + module_name)
module.open(path)
documents = [doc for name, doc in App.listDocuments().items() if name not in before]
if not documents: raise RuntimeError('Import handler did not open a native document')
opened.append({'filename': filename, 'sha256': digest, 'reused': existing is not None,
'documents': [{'name': doc.Name, 'label': doc.Label, 'filename': doc.FileName,
'objectCount': len(doc.Objects)} for doc in documents]})
except Exception as error:
# Roll back only documents created by this failed open, never pre-existing work.
for name in set(App.listDocuments()) - before: App.closeDocument(name)
if previous_name in App.listDocuments(): App.setActiveDocument(previous_name)
errors.append({'filename': str(filename), 'message': str(error)})
active = App.activeDocument()
value = {'command': 'Std_Open', 'documentId': args.get('documentId', ''),
'formats': formats, 'opened': opened, 'errors': errors,
'activeDocument': active.Name if active else None, 'operation': operation}
reply.update(status='success', native=True, result=value)
except Exception as error:
reply.update(status='error', code='open_command_failed', message=str(error))
response_text = json.dumps({'ok': reply['status'] == 'success', 'records': [reply]})
)PY", Py_file_input, scope, scope);
if (!executed) { Py_DECREF(scope); return false; }
Py_DECREF(executed);
PyObject* response = PyDict_GetItemString(scope, "response_text");
const char* text = response ? PyUnicode_AsUTF8(response) : nullptr;
if (text) output = text;
const bool ok = text != nullptr;
Py_DECREF(scope);
if (ok) {
auto envelope = QJsonDocument::fromJson(QByteArray::fromStdString(output)).object();
auto rows = envelope.value("records").toArray();
if (rows.size() == 1 && rows.first().toObject().value("status").toString() == "success") {
auto row = rows.first().toObject();
auto value = row.value("result").toObject();
auto files = value.value("opened").toArray();
for (int i = 0; i < files.size(); ++i) {
auto file = files[i].toObject();
auto documents = file.value("documents").toArray();
for (int j = 0; j < documents.size(); ++j) {
auto item = documents[j].toObject();
auto* doc = App::GetApplication().getDocument(item.value("name").toString().toUtf8().constData());
if (doc) {
if (!file.value("reused").toBool()) doc->setUndoMode(1);
item.insert("partialRestore", doc->testStatus(App::Document::PartialRestore));
item.insert("restoreError", doc->testStatus(App::Document::RestoreError));
}
documents[j] = item;
}
file.insert("documents", documents); files[i] = file;
}
value.insert("opened", files); row.insert("result", value); rows[0] = row;
envelope.insert("records", rows);
output = QJsonDocument(envelope).toJson(QJsonDocument::Compact).toStdString();
}
}
return ok;
}