451 lines
25 KiB
C++
451 lines
25 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#pragma once
|
|
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/PropertyStandard.h>
|
|
#include <Mod/CAM/App/FeaturePath.h>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonDocument>
|
|
#include <QFileInfo>
|
|
#include <QSaveFile>
|
|
#include <Python.h>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
namespace CAMWeb {
|
|
|
|
bool dispatchRegisteredDesktop(const QString& command, const QJsonObject& args, QJsonObject& result);
|
|
|
|
inline void appendDocumentFile(const QJsonObject& args, QJsonObject& nativeResult)
|
|
{
|
|
if (!args.value("saveDocument").toBool() || nativeResult.value("cancelled").toBool())
|
|
return;
|
|
const auto gil = PyGILState_Ensure();
|
|
PyObject* scope = PyDict_New();
|
|
PyObject* request = PyUnicode_FromString(QJsonDocument(args).toJson(QJsonDocument::Compact).constData());
|
|
PyObject* output = nullptr;
|
|
if (scope && request && PyDict_SetItemString(scope, "request_json", request) == 0
|
|
&& PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()) == 0)
|
|
output = PyRun_String(R"CAM(
|
|
import json, base64, os, tempfile, FreeCAD
|
|
args = json.loads(request_json)
|
|
doc = FreeCAD.getDocument(args['documentId'])
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
filename = doc.Name + '.FCStd'
|
|
path = os.path.join(folder, filename)
|
|
doc.saveCopy(path)
|
|
with open(path, 'rb') as saved:
|
|
content = saved.read()
|
|
file_json = json.dumps({'fileName': filename, 'contentBase64': base64.b64encode(content).decode('ascii'), 'byteLength': len(content)})
|
|
)CAM", Py_file_input, scope, scope);
|
|
PyObject* value = output ? PyDict_GetItemString(scope, "file_json") : nullptr;
|
|
if (value && PyUnicode_Check(value)) {
|
|
auto files = nativeResult.value("files").toArray();
|
|
files.append(QJsonDocument::fromJson(QByteArray(PyUnicode_AsUTF8(value))).object());
|
|
nativeResult.insert("files", files);
|
|
} else {
|
|
PyObject *type = nullptr, *exception = nullptr, *traceback = nullptr;
|
|
PyErr_Fetch(&type, &exception, &traceback);
|
|
PyErr_NormalizeException(&type, &exception, &traceback);
|
|
PyObject* message = exception ? PyObject_Str(exception) : nullptr;
|
|
const char* detail = message ? PyUnicode_AsUTF8(message) : nullptr;
|
|
// The operation has committed; retain its result even when serialization fails.
|
|
nativeResult.insert("persistenceError", detail ? QString::fromUtf8(detail)
|
|
: QStringLiteral("CAM document serialization failed"));
|
|
Py_XDECREF(message); Py_XDECREF(type); Py_XDECREF(exception); Py_XDECREF(traceback);
|
|
PyErr_Clear();
|
|
}
|
|
Py_XDECREF(output); Py_XDECREF(request); Py_XDECREF(scope);
|
|
PyGILState_Release(gil);
|
|
}
|
|
|
|
inline void saveToolBitAs(const QJsonObject& args, QJsonObject& result)
|
|
{
|
|
QString code = QStringLiteral("invalid_request");
|
|
try {
|
|
const QString documentId = args.value(QStringLiteral("documentId")).toString();
|
|
const auto selection = args.value(QStringLiteral("selection")).toArray();
|
|
if (documentId.isEmpty() || selection.size() != 1 || !selection[0].isObject())
|
|
throw std::runtime_error("Select one document toolbit to save");
|
|
const auto selected = selection[0].toObject();
|
|
if (selected.contains(QStringLiteral("documentId"))
|
|
&& selected.value(QStringLiteral("documentId")).toString() != documentId)
|
|
throw std::runtime_error("The toolbit must belong to the selected document");
|
|
const QString path = args.contains(QStringLiteral("path"))
|
|
? args.value(QStringLiteral("path")).toString() : args.value(QStringLiteral("file")).toString();
|
|
if (path.trimmed().isEmpty() || path.contains(QChar(0)))
|
|
throw std::runtime_error("ToolBit Save As requires a file path");
|
|
if (args.value(QStringLiteral("action")).toString() == QStringLiteral("cancel")) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
result.insert(QStringLiteral("result"), QJsonObject{{QStringLiteral("cancelled"), true}});
|
|
return;
|
|
}
|
|
code = QStringLiteral("overwrite_required");
|
|
if (QFileInfo::exists(path) && !args.value(QStringLiteral("overwrite")).toBool())
|
|
throw std::runtime_error("Confirm overwrite of the existing toolbit file");
|
|
code = QStringLiteral("toolbit_serialization_failed");
|
|
QByteArray bytes;
|
|
// Run desktop serializers against the live proxy, with request data in a private scope.
|
|
const auto gil = PyGILState_Ensure();
|
|
PyObject* scope = PyDict_New();
|
|
PyObject* request = PyUnicode_FromString(QJsonDocument(args).toJson(QJsonDocument::Compact).constData());
|
|
PyObject* output = nullptr;
|
|
if (scope && request && PyDict_SetItemString(scope, "request_json", request) == 0
|
|
&& PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()) == 0) {
|
|
output = PyRun_String(
|
|
"import json, pathlib, FreeCAD\n"
|
|
"from Path.Tool.toolbit import ToolBit\n"
|
|
"from Path.Tool.toolbit.serializers import all_serializers\n"
|
|
"args = json.loads(request_json)\n"
|
|
"doc = FreeCAD.getDocument(args['documentId'])\n"
|
|
"obj = doc.getObject(args['selection'][0]['object'])\n"
|
|
"if obj is None or not isinstance(getattr(obj, 'Proxy', None), ToolBit):\n"
|
|
" raise ValueError('Select a document ToolBit object')\n"
|
|
"extension = pathlib.Path(args.get('path', args.get('file'))).suffix.lower()\n"
|
|
"serializer = next((s for s in all_serializers if extension in s.extensions), None)\n"
|
|
"if serializer is None:\n"
|
|
" raise ValueError('No toolbit serializer for extension ' + extension)\n"
|
|
"content = serializer.serialize(obj.Proxy)\n",
|
|
Py_file_input, scope, scope);
|
|
}
|
|
QString error;
|
|
PyObject* content = output ? PyDict_GetItemString(scope, "content") : nullptr;
|
|
if (content && PyBytes_Check(content)) {
|
|
bytes = QByteArray(PyBytes_AS_STRING(content), PyBytes_GET_SIZE(content));
|
|
} else {
|
|
PyObject *type = nullptr, *value = nullptr, *traceback = nullptr;
|
|
PyErr_Fetch(&type, &value, &traceback);
|
|
PyErr_NormalizeException(&type, &value, &traceback);
|
|
PyObject* message = value ? PyObject_Str(value) : nullptr;
|
|
const char* text = message ? PyUnicode_AsUTF8(message) : nullptr;
|
|
error = text ? QString::fromUtf8(text) : QStringLiteral("Toolbit serializer did not return bytes");
|
|
Py_XDECREF(message); Py_XDECREF(type); Py_XDECREF(value); Py_XDECREF(traceback);
|
|
PyErr_Clear();
|
|
}
|
|
Py_XDECREF(output); Py_XDECREF(request); Py_XDECREF(scope);
|
|
PyGILState_Release(gil);
|
|
if (!error.isEmpty()) throw std::runtime_error(error.toStdString());
|
|
code = QStringLiteral("file_write_failed");
|
|
QSaveFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit())
|
|
throw std::runtime_error(file.errorString().toStdString());
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
result.insert(QStringLiteral("result"), QJsonObject{
|
|
{QStringLiteral("documentId"), documentId},
|
|
{QStringLiteral("object"), selected.value(QStringLiteral("object"))},
|
|
{QStringLiteral("fileName"), QFileInfo(path).fileName()},
|
|
{QStringLiteral("path"), path},
|
|
{QStringLiteral("contentBase64"), QString::fromLatin1(bytes.toBase64())},
|
|
{QStringLiteral("byteLength"), bytes.size()}});
|
|
} catch (const std::exception& error) {
|
|
result.insert(QStringLiteral("status"), QStringLiteral("restricted"));
|
|
result.insert(QStringLiteral("code"), code);
|
|
result.insert(QStringLiteral("message"), QString::fromUtf8(error.what()));
|
|
}
|
|
}
|
|
|
|
// Kept inline so the existing CAM runtime target owns this handler and its document.
|
|
inline void toggleOperations(const QJsonObject& args, QJsonObject& result)
|
|
{
|
|
if (args.value("action").toString() == "cancel") {
|
|
result.insert("status", "exposed");
|
|
result.insert("result", QJsonObject{{"cancelled", true}});
|
|
return;
|
|
}
|
|
App::Document* document = nullptr;
|
|
bool transaction = false;
|
|
QString code = QStringLiteral("invalid_request");
|
|
try {
|
|
const QString documentId = args.value(QStringLiteral("documentId")).toString();
|
|
if (documentId.isEmpty())
|
|
throw std::runtime_error("CAM_OpActiveToggle requires documentId");
|
|
if (args.value(QStringLiteral("activeTask")).toBool())
|
|
throw std::runtime_error("Finish the active task before toggling CAM operations");
|
|
code = QStringLiteral("document_not_found");
|
|
document = App::GetApplication().getDocument(documentId.toUtf8().constData());
|
|
if (!document)
|
|
throw std::runtime_error("The selected CAM document does not exist");
|
|
code = QStringLiteral("selection_required");
|
|
const QJsonArray selection = args.value(QStringLiteral("selection")).toArray();
|
|
if (selection.isEmpty())
|
|
throw std::runtime_error("Select CAM operations to toggle");
|
|
|
|
std::vector<Path::Feature*> operations;
|
|
std::set<App::DocumentObject*> seen;
|
|
code = QStringLiteral("invalid_selection");
|
|
for (const auto& entry : selection) {
|
|
if (!entry.isObject())
|
|
throw std::runtime_error("CAM operation selection must contain document objects");
|
|
const QJsonObject selected = entry.toObject();
|
|
if (selected.contains(QStringLiteral("documentId"))
|
|
&& selected.value(QStringLiteral("documentId")).toString() != documentId)
|
|
throw std::runtime_error("CAM operations must belong to the selected document");
|
|
const QString name = selected.value(QStringLiteral("object")).toString();
|
|
auto* operation = name.isEmpty() ? nullptr
|
|
: dynamic_cast<Path::Feature*>(document->getObject(name.toUtf8().constData()));
|
|
if (!operation || !dynamic_cast<App::PropertyBool*>(operation->getPropertyByName("Active")))
|
|
throw std::runtime_error("Select a CAM operation with an Active property");
|
|
if (seen.insert(operation).second)
|
|
operations.push_back(operation);
|
|
}
|
|
|
|
document->openTransaction("Toggle CAM operation active state");
|
|
transaction = true;
|
|
for (auto* operation : operations) {
|
|
auto* active = static_cast<App::PropertyBool*>(operation->getPropertyByName("Active"));
|
|
active->setValue(!active->getValue());
|
|
}
|
|
code = QStringLiteral("recompute_failed");
|
|
bool hasError = false;
|
|
if (document->recompute({}, false, &hasError) < 0 || hasError)
|
|
throw std::runtime_error("CAM operation recompute failed");
|
|
QJsonArray updated;
|
|
for (auto* operation : operations) {
|
|
if (operation->isError())
|
|
throw std::runtime_error("A selected CAM operation has an execution error");
|
|
updated.append(QJsonObject{
|
|
{QStringLiteral("object"), QString::fromUtf8(operation->getNameInDocument())},
|
|
{QStringLiteral("active"), static_cast<App::PropertyBool*>(operation->getPropertyByName("Active"))->getValue()}});
|
|
}
|
|
document->commitTransaction();
|
|
transaction = false;
|
|
result.insert(QStringLiteral("status"), QStringLiteral("exposed"));
|
|
result.insert(QStringLiteral("code"), QStringLiteral("native_operation_active_toggle"));
|
|
QJsonObject nativeResult{
|
|
{QStringLiteral("documentId"), documentId},
|
|
{QStringLiteral("keptOpen"), true},
|
|
{QStringLiteral("operations"), updated},
|
|
{QStringLiteral("selection"), selection},
|
|
{QStringLiteral("recomputed"), true}};
|
|
appendDocumentFile(args, nativeResult);
|
|
result.insert(QStringLiteral("result"), nativeResult);
|
|
}
|
|
catch (const std::exception& error) {
|
|
if (transaction && document)
|
|
document->abortTransaction();
|
|
result.insert(QStringLiteral("status"), QStringLiteral("restricted"));
|
|
result.insert(QStringLiteral("code"), code);
|
|
result.insert(QStringLiteral("message"), QString::fromUtf8(error.what()));
|
|
}
|
|
}
|
|
|
|
inline void toolBitCommand(const QString& command, const QJsonObject& args, QJsonObject& result)
|
|
{
|
|
try {
|
|
const QString docId = args.value("documentId").toString();
|
|
if (docId.isEmpty()) throw std::runtime_error("ToolBit command requires documentId");
|
|
const QString path = args.value("path").toString(args.value("file").toString());
|
|
if (command == "CAM_ToolBitDock" || command == "CAM_ToolBitLibraryOpen") {
|
|
result.insert("status", "exposed"); result.insert("result", QJsonObject{{"opened", true}, {"command", command}}); return;
|
|
}
|
|
if (path.isEmpty()) throw std::runtime_error("ToolBit file path is required");
|
|
if (command == "CAM_ToolBitLoad") {
|
|
const auto gil = PyGILState_Ensure();
|
|
PyObject* scope = PyDict_New(); PyObject* req = PyUnicode_FromString(QJsonDocument(args).toJson(QJsonDocument::Compact).constData());
|
|
PyObject* out = nullptr;
|
|
if (scope && req && PyDict_SetItemString(scope, "request_json", req) == 0)
|
|
out = PyRun_String("import json, FreeCAD\nfrom Path.Tool.toolbit import ToolBit\nfrom Path.Tool.toolbit.serializers import all_serializers\na=json.loads(request_json)\nd=FreeCAD.getDocument(a['documentId'])\next=__import__('pathlib').Path(a['path']).suffix.lower()\ns=next((x for x in all_serializers if ext in x.extensions), None)\nif s is None: raise ValueError('No toolbit serializer for extension '+ext)\nt=s.deserialize(a['path'])\nt.attach_to_doc(d)\nd.recompute()\n", Py_file_input, scope, scope);
|
|
if (!out) { PyErr_Clear(); Py_XDECREF(req); Py_XDECREF(scope); PyGILState_Release(gil); throw std::runtime_error("ToolBit load failed"); }
|
|
Py_DECREF(out); Py_XDECREF(req); Py_XDECREF(scope); PyGILState_Release(gil);
|
|
result.insert("status", "exposed"); result.insert("result", QJsonObject{{"documentId", docId}, {"path", path}, {"loaded", true}}); return;
|
|
}
|
|
const auto sel = args.value("selection").toArray(); if (sel.size()!=1) throw std::runtime_error("Select one ToolBit");
|
|
if (QFileInfo::exists(path) && !args.value("overwrite").toBool()) throw std::runtime_error("Confirm overwrite of the existing toolbit file");
|
|
QJsonObject saveArgs(args); saveArgs.insert("path", path); saveToolBitAs(saveArgs, result);
|
|
} catch (const std::exception& e) { result.insert("status", "restricted"); result.insert("code", "toolbit_command_failed"); result.insert("message", QString::fromUtf8(e.what())); }
|
|
}
|
|
|
|
inline void executeDesktopCommand(const QString& command, const QJsonObject& args, QJsonObject& result)
|
|
{
|
|
try {
|
|
const QString documentId = args.value("documentId").toString();
|
|
if (documentId.isEmpty()) throw std::runtime_error("CAM command requires documentId");
|
|
App::Document* document = App::GetApplication().getDocument(documentId.toUtf8().constData());
|
|
if (!document) throw std::runtime_error("The selected CAM document does not exist");
|
|
QJsonObject requestArgs(args);
|
|
requestArgs.insert("command", command);
|
|
const auto gil = PyGILState_Ensure();
|
|
PyObject* scope = PyDict_New();
|
|
PyObject* request = PyUnicode_FromString(QJsonDocument(requestArgs).toJson(QJsonDocument::Compact).constData());
|
|
PyObject* output = nullptr;
|
|
if (scope && request && PyDict_SetItemString(scope, "request_json", request) == 0
|
|
&& PyDict_SetItemString(scope, "__builtins__", PyEval_GetBuiltins()) == 0)
|
|
output = PyRun_String(R"CAM(
|
|
import json, importlib, base64, os
|
|
import FreeCAD
|
|
from PathScripts import PathUtils
|
|
a = json.loads(request_json)
|
|
d = FreeCAD.getDocument(a['documentId'])
|
|
command = a['command']
|
|
result = {'documentId': d.Name, 'keptOpen': True}
|
|
def configure_operation(obj, job):
|
|
if a.get('toolController'):
|
|
tool = d.getObject(a['toolController'])
|
|
if tool not in job.Tools.Group:
|
|
raise ValueError('Choose a tool controller from the job')
|
|
obj.ToolController = tool
|
|
for key, value in a.get('parameters', {}).items():
|
|
if key not in obj.PropertiesList or key in ('Base', 'ToolController', 'Path', 'ExpressionEngine', 'Proxy'):
|
|
raise ValueError('Unsupported operation parameter: ' + key)
|
|
if any(path == key for path, expression in obj.ExpressionEngine):
|
|
obj.setExpression(key, None)
|
|
setattr(obj, key, value)
|
|
def recompute_operations(operations):
|
|
d.recompute()
|
|
if any('Invalid' in obj.State or 'Error' in obj.State for obj in operations):
|
|
raise RuntimeError('CAM operation recompute failed')
|
|
if any(getattr(obj, 'Active', True) and not obj.Path.Commands for obj in operations):
|
|
raise RuntimeError('CAM operation produced no toolpath; check geometry, tool and operation parameters')
|
|
def run():
|
|
if a.get('action') == 'cancel':
|
|
result['cancelled'] = True
|
|
return
|
|
if a.get('activeTask'):
|
|
raise ValueError('Finish the active task first')
|
|
selection = a.get('selection', [])
|
|
objects = []
|
|
for item in selection:
|
|
if item.get('documentId', d.Name) != d.Name:
|
|
raise ValueError('Selection belongs to another document')
|
|
obj = d.getObject(item['object'])
|
|
if obj is None:
|
|
raise ValueError('Selected object does not exist: ' + item['object'])
|
|
objects.append(obj)
|
|
if command == 'CAM_SelectLoop':
|
|
expanded = []
|
|
for item, obj in zip(selection, objects):
|
|
shape = obj.Shape
|
|
names = item.get('subelements', item.get('subElementNames', []))
|
|
if not names:
|
|
raise ValueError('Select a face or an edge for loop selection')
|
|
edges = []
|
|
for name in names:
|
|
seed = shape.getElement(name)
|
|
if seed.ShapeType == 'Face':
|
|
wires = seed.Wires
|
|
elif seed.ShapeType == 'Edge':
|
|
wires = [w for w in shape.Wires if any(e.isSame(seed) for e in w.Edges)]
|
|
else:
|
|
raise ValueError('Loop selection requires faces or edges')
|
|
for wire in wires:
|
|
for edge in wire.Edges:
|
|
for index, candidate in enumerate(shape.Edges, 1):
|
|
key = 'Edge' + str(index)
|
|
if candidate.isSame(edge) and key not in edges:
|
|
edges.append(key)
|
|
if not edges:
|
|
raise ValueError('No connected wire found for selection')
|
|
expanded.append({'documentId': d.Name, 'object': obj.Name, 'subelements': edges})
|
|
if not expanded:
|
|
raise ValueError('Select a face or edge')
|
|
result['selection'] = expanded
|
|
return
|
|
job = d.getObject(a['job']) if a.get('job') else None
|
|
if job is None:
|
|
candidates = []
|
|
for obj in objects:
|
|
parent = obj if hasattr(obj, 'Operations') else PathUtils.findParentJob(obj)
|
|
if parent and parent not in candidates:
|
|
candidates.append(parent)
|
|
if not candidates:
|
|
candidates = [obj for obj in d.Objects if hasattr(obj, 'Operations') and hasattr(obj, 'Model')]
|
|
if len(candidates) != 1:
|
|
raise ValueError('Choose one CAM job')
|
|
job = candidates[0]
|
|
if not hasattr(job, 'Operations'):
|
|
raise ValueError('Choose a CAM job')
|
|
changed = []
|
|
d.openTransaction(command)
|
|
try:
|
|
if command in ('CAM_Post', 'CAM_PostSelected'):
|
|
from Path.Post.Processor import PostProcessorFactory
|
|
selected = command == 'CAM_PostSelected'
|
|
if selected and (not objects or any(obj not in job.Operations.Group for obj in objects)):
|
|
raise ValueError('Select operations from the chosen job')
|
|
recompute_operations(objects if selected else job.Operations.Group)
|
|
if 'postProcessorArgs' in a:
|
|
job.PostProcessorArgs = a['postProcessorArgs']
|
|
machine = getattr(job, 'Machine', None)
|
|
processor = a.get('postProcessor') or job.PostProcessor
|
|
if machine and not selected:
|
|
from Machine.models.machine import MachineFactory
|
|
processor = MachineFactory.get_machine(machine).postprocessor_file_name
|
|
if not processor:
|
|
raise ValueError('Choose a post processor')
|
|
post = PostProcessorFactory.get_post_processor({'job': job, 'operations': objects} if selected else job, processor)
|
|
data = post.export2() if machine and not selected else post.export()
|
|
if not data:
|
|
raise ValueError('Post processor produced no output')
|
|
result['files'] = []
|
|
requested = os.path.basename(a.get('fileName') or (job.Name + '.nc'))
|
|
stem, extension = os.path.splitext(requested)
|
|
for index, (_, gcode) in enumerate(data):
|
|
content = gcode.encode('utf-8') if isinstance(gcode, str) else bytes(gcode)
|
|
filename = requested if len(data) == 1 else stem + '-' + str(index + 1) + extension
|
|
result['files'].append({'fileName': filename, 'contentBase64': base64.b64encode(content).decode('ascii'), 'byteLength': len(content)})
|
|
elif command == 'CAM_OperationCopy':
|
|
if not objects or any(obj not in job.Operations.Group for obj in objects):
|
|
raise ValueError('Select operations from the chosen job')
|
|
for source in dict.fromkeys(objects):
|
|
obj = d.copyObject(source, False)
|
|
job.Proxy.addOperation(obj)
|
|
configure_operation(obj, job)
|
|
obj.touch()
|
|
changed.append(obj)
|
|
else:
|
|
modules = {'CAM_Pocket3D': 'Pocket', 'CAM_Pocket_Shape': 'PocketShape', 'CAM_Probe': 'Probe', 'CAM_Profile': 'Profile'}
|
|
module = importlib.import_module('Path.Op.' + modules[command])
|
|
obj = module.Create(a.get('name') or modules[command], parentJob=job)
|
|
if objects and command != 'CAM_Probe':
|
|
obj.Base = [(o, s.get('subelements', s.get('subElementNames', []))) for o, s in zip(objects, selection) if o != job]
|
|
configure_operation(obj, job)
|
|
changed.append(obj)
|
|
if changed:
|
|
recompute_operations(changed)
|
|
result['operations'] = [{'object': obj.Name, 'active': getattr(obj, 'Active', True), 'gcode': obj.Path.toGCode(), 'commandCount': len(obj.Path.Commands)} for obj in changed]
|
|
if changed:
|
|
result['selection'] = [{'documentId': d.Name, 'object': obj.Name} for obj in changed]
|
|
result['job'] = job.Name
|
|
result['recomputed'] = True
|
|
d.commitTransaction()
|
|
except Exception:
|
|
d.abortTransaction()
|
|
raise
|
|
previous_document = FreeCAD.ActiveDocument
|
|
try:
|
|
FreeCAD.setActiveDocument(d.Name)
|
|
run()
|
|
finally:
|
|
FreeCAD.setActiveDocument(previous_document.Name if previous_document else '')
|
|
result_json = json.dumps(result)
|
|
)CAM", Py_file_input, scope, scope);
|
|
QString error;
|
|
QJsonObject nativeResult;
|
|
PyObject* value = output ? PyDict_GetItemString(scope, "result_json") : nullptr;
|
|
if (value && PyUnicode_Check(value)) {
|
|
nativeResult = QJsonDocument::fromJson(QByteArray(PyUnicode_AsUTF8(value))).object();
|
|
} else {
|
|
PyObject *type = nullptr, *exception = nullptr, *traceback = nullptr;
|
|
PyErr_Fetch(&type, &exception, &traceback);
|
|
PyErr_NormalizeException(&type, &exception, &traceback);
|
|
PyObject* message = exception ? PyObject_Str(exception) : nullptr;
|
|
const char* detail = message ? PyUnicode_AsUTF8(message) : nullptr;
|
|
error = detail ? QString::fromUtf8(detail) : QStringLiteral("CAM command returned no result");
|
|
Py_XDECREF(message); Py_XDECREF(type); Py_XDECREF(exception); Py_XDECREF(traceback);
|
|
PyErr_Clear();
|
|
}
|
|
Py_XDECREF(output); Py_XDECREF(request); Py_XDECREF(scope);
|
|
PyGILState_Release(gil);
|
|
if (!error.isEmpty()) throw std::runtime_error(error.toStdString());
|
|
appendDocumentFile(args, nativeResult);
|
|
result.insert("status", "exposed");
|
|
result.insert("code", "native_cam_desktop_workflow");
|
|
result.insert("result", nativeResult);
|
|
} catch (const std::exception& e) { result.insert("status", "restricted"); result.insert("code", "cam_command_failed"); result.insert("message", QString::fromUtf8(e.what())); }
|
|
}
|
|
|
|
} // namespace CAMWeb
|