Real IFC2X3/IFC4 import tested. Full BIM authoring/export remains out of scope; 349-second building import performance remains to optimize.
151 lines
8.4 KiB
C++
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;
|
|
}
|