Files
FreeCADWasmDev/src/WebAssembly/FreeCADCoreProbe/PartDesktopCommands.cpp
wangdequan 77925a996d
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Extend FreeCAD WebAssembly runtime support
2026-09-08 15:05:38 +08:00

1809 lines
98 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include <Python.h>
#include "PartDesktopCommands.h"
#include <QJsonDocument>
#include <BRepBuilderAPI_Copy.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <Precision.hxx>
#include <ShapeFix_ShapeTolerance.hxx>
#include <BOPAlgo_Builder.hxx>
#include <TopoDS_Iterator.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Section.hxx>
#include <BRepAlgoAPI_Splitter.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopTools_ListIteratorOfListOfShape.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRepBuilderAPI_Transform.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepFilletAPI_MakeChamfer.hxx>
#include <BRepFilletAPI_MakeFillet.hxx>
#include <BRepGProp.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepPrimAPI_MakeTorus.hxx>
#include <BRepPrimAPI_MakePrism.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
#include <BRepTools.hxx>
#include <BRepOffsetAPI_MakeOffset.hxx>
#include <BRepOffsetAPI_MakePipe.hxx>
#include <BRepOffsetAPI_MakeThickSolid.hxx>
#include <BRepOffsetAPI_ThruSections.hxx>
#include <BRep_Builder.hxx>
#include <GProp_GProps.hxx>
#include <Standard_Failure.hxx>
#include <TopAbs_ShapeEnum.hxx>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Shape.hxx>
#include <gp_Pnt.hxx>
#include <gp_Pln.hxx>
#include <gp_Vec.hxx>
#include <gp_Ax1.hxx>
#include <gp_Dir.hxx>
#include <gp_Trsf.hxx>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <sstream>
#include <vector>
namespace FreeCADWeb {
namespace {
enum class Operation {
Boolean,
Box,
BoxSelection,
SelectionFilter,
Builder,
Chamfer,
CheckGeometry,
Compound,
Copy,
ColorPerFace,
Join,
Offset,
Split,
CoordinateSystem,
DatumGroup,
CrossSections,
Datum,
Export,
Import,
Extrude,
Fillet,
Face,
Solid,
Mirror,
Loft,
NewDocument,
ImportCurveNet,
Offset2D,
PickCurveNet,
Primitives,
ParametricPrimitive,
ProjectionOnSurface,
ReverseShape,
Revolve,
RuledSurface,
Scale,
Section,
Sweep,
Thickness,
ShapeInfo,
Defeaturing,
ElementCopy,
PointsFromMesh,
RefineShape,
ShapeFromMesh,
SimpleCopy,
SimpleCylinder,
TransformedCopy,
};
struct CommandSpec {
const char* name;
Operation operation;
};
// This macro is also the machine-readable registration point consumed by the
// capability generator. It registers behavior, never a precomputed result.
#define FC_WEB_DESKTOP_COMMAND(command, operation) {command, Operation::operation}
// Implemented for solid BREP selections; unsupported dimensions fail explicitly.
#define FC_WEB_IMPLEMENTED_DESKTOP_COMMAND(command)
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Part_JoinCutout")
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Part_JoinEmbed")
FC_WEB_IMPLEMENTED_DESKTOP_COMMAND("Part_Slice")
#undef FC_WEB_IMPLEMENTED_DESKTOP_COMMAND
// Part_JoinConnect has a solid BREP handler, but remains implementation_required
// until Join feature source links, recompute and visibility use the current document.
// Shell/wire connectivity also remains unsupported.
// Batch 53c3881215bdc3314a263fb0 checkpoint: Part_EditAttachment remains
// implementation_required. AttachmentEditor/Commands.py:37-114 selects one
// current document object with Placement and opens AttachmentEditorTaskPanel;
// Apply writes the feature, OK commits, and Cancel aborts the transaction.
// The inspected PartDesktopCommandProbe.cpp dispatcher forwards JSON args to
// this geometry adapter without resolving a current App::DocumentObject.
// Required integration: selected document/object handle resolution, attachment
// extension and support references, native recompute, and a React task editor
// sharing native apply/commit/abort state. The supplied packet has no editable
// document bridge or task-panel entry point for that integration.
// Keep the command unregistered here until that contract executes; neither a
// placement transform nor a JSON interaction summary implements attachment.
// Part_CompoundFilter below provides geometry only. Keep implementation_required
// until specific-items filtering, Base/Stencil links, recompute, visibility and
// abort/continue transactions are connected through the document contract.
constexpr CommandSpec commandSpecs[] = {
// Native inspect/apply only; the transactional GUI editor is still pending.
FC_WEB_DESKTOP_COMMAND("Part_EditAttachment", Compound),
FC_WEB_DESKTOP_COMMAND("Part_CompoundFilter", Compound),
// Geometry-only until feature properties, source links and visibility are wired.
FC_WEB_DESKTOP_COMMAND("Part_ToleranceSet", Compound),
FC_WEB_DESKTOP_COMMAND("Part_Tube", ParametricPrimitive),
FC_WEB_DESKTOP_COMMAND("Part_JoinCutout", Join),
FC_WEB_DESKTOP_COMMAND("Part_JoinEmbed", Join),
FC_WEB_DESKTOP_COMMAND("Part_JoinConnect", Join),
FC_WEB_DESKTOP_COMMAND("Part_Slice", Split),
// Geometry entry points only: document filters/groups and visibility must
// be connected before adding implemented-command markers.
FC_WEB_DESKTOP_COMMAND("Part_SliceApart", Split),
FC_WEB_DESKTOP_COMMAND("Part_ExplodeCompound", Compound),
FC_WEB_DESKTOP_COMMAND("Part_XOR", Split),
FC_WEB_DESKTOP_COMMAND("Part_BooleanFragments", Split),
FC_WEB_DESKTOP_COMMAND("Boolean", Boolean),
FC_WEB_DESKTOP_COMMAND("Compound", Compound),
FC_WEB_DESKTOP_COMMAND("Copy", Copy),
FC_WEB_DESKTOP_COMMAND("Join", Join),
FC_WEB_DESKTOP_COMMAND("Part_ColorPerFace", ColorPerFace),
FC_WEB_DESKTOP_COMMAND("Primitives", Primitives),
FC_WEB_DESKTOP_COMMAND("Solids", Primitives),
FC_WEB_DESKTOP_COMMAND("Split", Split),
FC_WEB_DESKTOP_COMMAND("Part_Boolean", Boolean),
FC_WEB_DESKTOP_COMMAND("Part_Box2", Box),
FC_WEB_DESKTOP_COMMAND("Part_Box3", Box),
FC_WEB_DESKTOP_COMMAND("Part_Box", Box),
FC_WEB_DESKTOP_COMMAND("Part_Cone", ParametricPrimitive),
FC_WEB_DESKTOP_COMMAND("Part_Cylinder", ParametricPrimitive),
FC_WEB_DESKTOP_COMMAND("Part_Sphere", ParametricPrimitive),
FC_WEB_DESKTOP_COMMAND("Part_Torus", ParametricPrimitive),
FC_WEB_DESKTOP_COMMAND("Part_BoxSelection", BoxSelection),
FC_WEB_DESKTOP_COMMAND("Part_EdgeSelection", SelectionFilter),
FC_WEB_DESKTOP_COMMAND("Part_FaceSelection", SelectionFilter),
FC_WEB_DESKTOP_COMMAND("Part_RemoveSelectionGate", SelectionFilter),
FC_WEB_DESKTOP_COMMAND("Part_SelectFilter", SelectionFilter),
FC_WEB_DESKTOP_COMMAND("Part_VertexSelection", SelectionFilter),
FC_WEB_DESKTOP_COMMAND("Part_Builder", Builder),
FC_WEB_DESKTOP_COMMAND("Part_Chamfer", Chamfer),
FC_WEB_DESKTOP_COMMAND("Part_CheckGeometry", CheckGeometry),
FC_WEB_DESKTOP_COMMAND("Part_Common", Boolean),
FC_WEB_DESKTOP_COMMAND("Part_Cut", Boolean),
FC_WEB_DESKTOP_COMMAND("Part_Compound", Compound),
FC_WEB_DESKTOP_COMMAND("Part_CompCompoundTools", Compound),
FC_WEB_DESKTOP_COMMAND("Part_CompJoinFeatures", Join),
FC_WEB_DESKTOP_COMMAND("Part_CompOffset", Offset),
FC_WEB_DESKTOP_COMMAND("Part_CompSplitFeatures", Split),
FC_WEB_DESKTOP_COMMAND("Part_CoordinateSystem", CoordinateSystem),
FC_WEB_DESKTOP_COMMAND("Part_Datums", DatumGroup),
FC_WEB_DESKTOP_COMMAND("Part_CrossSections", CrossSections),
FC_WEB_DESKTOP_COMMAND("Part_DatumLine", Datum),
FC_WEB_DESKTOP_COMMAND("Part_DatumPlane", Datum),
FC_WEB_DESKTOP_COMMAND("Part_DatumPoint", Datum),
FC_WEB_DESKTOP_COMMAND("Part_Export", Export),
FC_WEB_DESKTOP_COMMAND("Part_Import", Import),
FC_WEB_DESKTOP_COMMAND("Part_Extrude", Extrude),
FC_WEB_DESKTOP_COMMAND("Part_Fillet", Fillet),
FC_WEB_DESKTOP_COMMAND("Part_Fuse", Boolean),
FC_WEB_DESKTOP_COMMAND("Part_ImportCurveNet", ImportCurveNet),
FC_WEB_DESKTOP_COMMAND("Part_Loft", Loft),
FC_WEB_DESKTOP_COMMAND("Part_MakeFace", Face),
FC_WEB_DESKTOP_COMMAND("Part_MakeSolid", Solid),
FC_WEB_DESKTOP_COMMAND("Part_Mirror", Mirror),
FC_WEB_DESKTOP_COMMAND("Part_NewDoc", NewDocument),
FC_WEB_DESKTOP_COMMAND("Part_Offset", Offset),
FC_WEB_DESKTOP_COMMAND("Part_Offset2D", Offset2D),
FC_WEB_DESKTOP_COMMAND("Part_PickCurveNet", PickCurveNet),
FC_WEB_DESKTOP_COMMAND("Part_Primitives", Primitives),
FC_WEB_DESKTOP_COMMAND("Part_ProjectionOnSurface", ProjectionOnSurface),
FC_WEB_DESKTOP_COMMAND("Part_ReverseShape", ReverseShape),
FC_WEB_DESKTOP_COMMAND("Part_Revolve", Revolve),
FC_WEB_DESKTOP_COMMAND("Part_RuledSurface", RuledSurface),
FC_WEB_DESKTOP_COMMAND("Part_Scale", Scale),
FC_WEB_DESKTOP_COMMAND("Part_Section", Section),
FC_WEB_DESKTOP_COMMAND("Part_SectionCut", Section),
FC_WEB_DESKTOP_COMMAND("Part_Sweep", Sweep),
FC_WEB_DESKTOP_COMMAND("Part_Thickness", Thickness),
FC_WEB_DESKTOP_COMMAND("Part_ShapeInfo", ShapeInfo),
FC_WEB_DESKTOP_COMMAND("Part_Defeaturing", Defeaturing),
FC_WEB_DESKTOP_COMMAND("Part_ElementCopy", ElementCopy),
FC_WEB_DESKTOP_COMMAND("Part_PointsFromMesh", PointsFromMesh),
FC_WEB_DESKTOP_COMMAND("Part_RefineShape", RefineShape),
FC_WEB_DESKTOP_COMMAND("Part_ShapeFromMesh", ShapeFromMesh),
FC_WEB_DESKTOP_COMMAND("Part_SimpleCopy", SimpleCopy),
FC_WEB_DESKTOP_COMMAND("Part_SimpleCylinder", SimpleCylinder),
FC_WEB_DESKTOP_COMMAND("Part_TransformedCopy", TransformedCopy),
};
#undef FC_WEB_DESKTOP_COMMAND
const CommandSpec* findCommand(const QString& command)
{
const auto match = std::find_if(
std::begin(commandSpecs),
std::end(commandSpecs),
[&command](const CommandSpec& spec) { return command == QLatin1String(spec.name); }
);
return match == std::end(commandSpecs) ? nullptr : match;
}
bool readPoint(const QJsonValue& value, gp_Pnt& point)
{
const QJsonArray coordinates = value.toArray();
if (coordinates.size() != 3) {
return false;
}
const double x = coordinates.at(0).toDouble(std::numeric_limits<double>::quiet_NaN());
const double y = coordinates.at(1).toDouble(std::numeric_limits<double>::quiet_NaN());
const double z = coordinates.at(2).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z)) {
return false;
}
point.SetCoord(x, y, z);
return true;
}
bool readShape(const QJsonValue& value, TopoDS_Shape& shape)
{
const std::string brep = value.toString().toStdString();
if (brep.empty()) {
return false;
}
std::istringstream input(brep);
BRep_Builder builder;
BRepTools::Read(shape, input, builder);
return !shape.IsNull();
}
bool readShapes(const QJsonObject& args, std::vector<TopoDS_Shape>& shapes, QString& error)
{
const QJsonArray input = args.value(QStringLiteral("shapes")).toArray();
for (const QJsonValue& value : input) {
TopoDS_Shape shape;
if (!readShape(value, shape)) {
error = QStringLiteral("args.shapes must contain valid BREP strings");
return false;
}
shapes.push_back(shape);
}
return true;
}
QString writeShape(const TopoDS_Shape& shape)
{
std::ostringstream output;
BRepTools::Write(shape, output);
return QString::fromStdString(output.str());
}
void publishShape(const TopoDS_Shape& shape, QJsonObject& result)
{
GProp_GProps properties;
BRepGProp::VolumeProperties(shape, properties);
int edgeCount = 0;
for (TopExp_Explorer explorer(shape, TopAbs_EDGE); explorer.More(); explorer.Next()) {
++edgeCount;
}
result.insert(QStringLiteral("brep"), writeShape(shape));
result.insert(QStringLiteral("volume"), properties.Mass());
result.insert(QStringLiteral("edgeCount"), edgeCount);
result.insert(
QStringLiteral("shapeType"),
shape.ShapeType() == TopAbs_SOLID ? QStringLiteral("Solid") : QStringLiteral("Shape")
);
result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shape).IsValid());
}
bool readDimensions(const QJsonObject& args, std::array<double, 3>& dimensions, QString& error)
{
dimensions = {
args.value(QStringLiteral("length")).toDouble(std::numeric_limits<double>::quiet_NaN()),
args.value(QStringLiteral("width")).toDouble(std::numeric_limits<double>::quiet_NaN()),
args.value(QStringLiteral("height")).toDouble(std::numeric_limits<double>::quiet_NaN()),
};
if (std::any_of(dimensions.begin(), dimensions.end(), [](double value) { return !std::isfinite(value) || value <= 0.0; })) {
error = QStringLiteral("length, width and height must be positive finite numbers");
return false;
}
return true;
}
} // namespace
bool supportsPartDesktopCommand(const QString& command)
{
return findCommand(command) != nullptr;
}
bool executePartDesktopCommand(
const QString& command,
const QJsonObject& args,
QJsonObject& result,
QString& error
)
{
if (command == QLatin1String("Part_EditAttachment")) {
if (!Py_IsInitialized()) {
error = QStringLiteral("Attachment editing requires the initialized FreeCAD Python runtime");
return false;
}
const PyGILState_STATE gil = PyGILState_Ensure();
PyObject* globals = PyDict_New();
const QByteArray request = QJsonDocument(args).toJson(QJsonDocument::Compact);
PyObject* input = PyUnicode_DecodeUTF8(request.constData(), request.size(), "strict");
const bool ready = globals && input
&& PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins()) == 0
&& PyDict_SetItemString(globals, "_request", input) == 0;
Py_XDECREF(input);
PyObject* executed = ready ? PyRun_String(R"PY(
import json
import math
import FreeCAD as App
def _attachment_command(args):
if not isinstance(args, dict):
raise ValueError("Attachment arguments must be an object")
doc = App.ActiveDocument
if doc is None or args.get("document") != doc.Name:
raise ValueError("document must identify the current FreeCAD document")
name = args.get("object")
if not isinstance(name, str) or not name:
raise ValueError("object must identify the selected document object")
obj = doc.getObject(name)
if obj is None or not hasattr(obj, "Placement"):
raise ValueError("Select one current document object with Placement")
action = args.get("action", "inspect")
if action not in ("inspect", "apply"):
raise ValueError("Only inspect and atomic apply are implemented")
def snapshot():
placement = obj.Placement
state = {
"document": doc.Name, "object": obj.Name,
"placement": {"position": list(placement.Base),
"quaternion": list(placement.Rotation.Q)},
"state": list(obj.State),
"attachable": hasattr(obj, "MapMode") and hasattr(obj, "Support"),
}
if state["attachable"]:
state["mapMode"] = str(obj.MapMode)
state["mapModes"] = obj.getEnumerationsOfProperty("MapMode")
state["support"] = [{"object": source.Name, "subelements": list(elements)}
for source, elements in obj.Support]
if hasattr(obj, "MapReversed"):
state["mapReversed"] = bool(obj.MapReversed)
if hasattr(obj, "AttachmentOffset"):
offset = obj.AttachmentOffset
state["attachmentOffset"] = {"position": list(offset.Base),
"quaternion": list(offset.Rotation.Q)}
if hasattr(obj, "Shape") and not obj.Shape.isNull():
state["brep"] = obj.Shape.exportBrepToString()
return state
if action == "inspect":
return snapshot()
if not hasattr(obj, "MapMode") or not hasattr(obj, "Support"):
raise ValueError("The selected object needs an attachment extension before apply")
changes = args.get("properties")
allowed = {"MapMode", "Support", "MapReversed", "AttachmentOffset"}
if not isinstance(changes, dict) or not changes or set(changes) - allowed:
raise ValueError("properties must contain supported attachment properties")
prepared = dict(changes)
if "MapMode" in prepared and prepared["MapMode"] not in obj.getEnumerationsOfProperty("MapMode"):
raise ValueError("Unknown MapMode for the selected attachment engine")
if "MapReversed" in prepared and not isinstance(prepared["MapReversed"], bool):
raise ValueError("MapReversed must be boolean")
if "Support" in prepared:
references = prepared["Support"]
if not isinstance(references, list):
raise ValueError("Support must be an array of object/subelements references")
links = []
for reference in references:
if not isinstance(reference, dict) or not isinstance(reference.get("object"), str):
raise ValueError("Each support must identify a document object")
source = doc.getObject(reference["object"])
elements = reference.get("subelements", [])
if source is None or source == obj:
raise ValueError("Support must resolve to another object in the current document")
if not isinstance(elements, list) or any(not isinstance(e, str) for e in elements):
raise ValueError("Support subelements must be strings")
links.append((source, elements))
prepared["Support"] = links
if "AttachmentOffset" in prepared:
offset = prepared["AttachmentOffset"]
if not isinstance(offset, dict):
raise ValueError("AttachmentOffset requires position and quaternion")
position, quaternion = offset.get("position"), offset.get("quaternion")
for values, size in ((position, 3), (quaternion, 4)):
if (not isinstance(values, list) or len(values) != size or
any(isinstance(v, bool) or not isinstance(v, (int, float)) or
not math.isfinite(v) for v in values)):
raise ValueError("AttachmentOffset requires finite position[3] and quaternion[4]")
if not any(quaternion):
raise ValueError("AttachmentOffset quaternion must be nonzero")
prepared["AttachmentOffset"] = App.Placement(App.Vector(*position), App.Rotation(*quaternion))
# Never commit an unrelated editor's pending transaction.
pending = getattr(doc, "HasPendingTransaction", None)
if pending is None:
pending = getattr(doc, "hasPendingTransaction", None)
if callable(pending):
pending = pending()
if pending is None:
raise RuntimeError("Native document transaction ownership inspection is unavailable")
if pending:
raise ValueError("Finish the current document transaction before atomic attachment apply")
doc.openTransaction("Edit attachment")
try:
for property_name in ("Support", "MapMode", "MapReversed", "AttachmentOffset"):
if property_name in prepared:
setattr(obj, property_name, prepared[property_name])
if doc.recompute() is False or "Invalid" in obj.State or "Error" in obj.State:
raise RuntimeError("FreeCAD rejected attachment recompute")
value = snapshot()
doc.commitTransaction()
return value
except Exception:
doc.abortTransaction()
doc.recompute()
raise
try:
_reply = json.dumps({"ok": True, "result": _attachment_command(json.loads(_request))}, allow_nan=False)
except Exception as exc:
_reply = json.dumps({"ok": False, "error": str(exc)})
)PY", Py_file_input, globals, globals) : nullptr;
bool success = false;
if (executed) {
PyObject* reply = PyDict_GetItemString(globals, "_reply");
const char* encoded = reply && PyUnicode_Check(reply) ? PyUnicode_AsUTF8(reply) : nullptr;
if (encoded) {
const QJsonObject payload = QJsonDocument::fromJson(QByteArray(encoded)).object();
success = payload.value(QStringLiteral("ok")).toBool(false);
if (success) {
result = payload.value(QStringLiteral("result")).toObject();
result.insert(QStringLiteral("command"), command);
}
else {
error = payload.value(QStringLiteral("error")).toString();
}
}
}
if (!success && error.isEmpty()) {
error = QStringLiteral("FreeCAD attachment Python execution failed");
}
Py_XDECREF(executed);
Py_XDECREF(globals);
if (PyErr_Occurred()) PyErr_Clear();
PyGILState_Release(gil);
return success;
}
const CommandSpec* spec = findCommand(command);
if (!spec) {
error = QStringLiteral("Part desktop command is not registered");
return false;
}
result.insert(QStringLiteral("command"), command);
result.insert(QStringLiteral("input"), args);
try {
if (command == QLatin1String("Part_CompoundFilter")) {
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (shapes.empty() || shapes.size() > 2) {
error = QStringLiteral("CompoundFilter requires a base BREP and an optional stencil BREP");
return false;
}
const QString mode = args.value(QStringLiteral("FilterType")).toString();
const bool collision = mode == QLatin1String("collision-pass");
const bool distance = mode == QLatin1String("window-distance");
const bool volume = mode == QLatin1String("window-volume");
const bool area = mode == QLatin1String("window-area");
const bool length = mode == QLatin1String("window-length");
const bool bypass = mode == QLatin1String("bypass");
if (!collision && !distance && !volume && !area && !length && !bypass) {
error = QStringLiteral("Explicit FilterType required; specific items remains implementation_required");
return false;
}
if ((collision || distance) && shapes.size() != 2) {
error = QStringLiteral("This filter requires a stencil");
return false;
}
const QJsonValue invertValue = args.value(QStringLiteral("Invert"));
if (!invertValue.isUndefined() && !invertValue.isBool()) {
error = QStringLiteral("Invert must be a boolean");
return false;
}
const bool invert = invertValue.toBool(false);
const auto measure = [&](const TopoDS_Shape& shape) {
if (collision || distance) {
BRepExtrema_DistShapeShape extrema(shape, shapes[1]);
if (!extrema.IsDone()) {
return std::numeric_limits<double>::quiet_NaN();
}
return extrema.Value();
}
GProp_GProps properties;
if (volume) BRepGProp::VolumeProperties(shape, properties);
else if (area) BRepGProp::SurfaceProperties(shape, properties);
else BRepGProp::LinearProperties(shape, properties);
return properties.Mass();
};
std::vector<TopoDS_Shape> children;
std::vector<double> values;
double maximum = -std::numeric_limits<double>::infinity();
for (TopoDS_Iterator child(shapes.front()); child.More(); child.Next()) {
children.push_back(child.Value());
const double value = bypass ? 0.0 : measure(child.Value());
if (!std::isfinite(value)) {
error = QStringLiteral("OpenCascade could not measure a base child");
return false;
}
values.push_back(value);
if (value > maximum) maximum = value;
}
if (children.empty()) {
error = QStringLiteral("CompoundFilter base has no direct children");
return false;
}
double lower = 0.0;
double upper = 0.0;
if (!collision && !bypass) {
const auto number = [&](const char* key) {
const QJsonValue value = args.value(QLatin1String(key));
return value.isDouble() ? value.toDouble()
: std::numeric_limits<double>::quiet_NaN();
};
const double from = number("WindowFrom");
const double to = number("WindowTo");
const double overrideMaximum = args.contains(QStringLiteral("OverrideMaxVal"))
? number("OverrideMaxVal") : 0.0;
if (shapes.size() == 2 && !distance) maximum = measure(shapes[1]);
if (overrideMaximum != 0.0) maximum = overrideMaximum;
if (!std::isfinite(from) || !std::isfinite(to) || !std::isfinite(maximum)) {
error = QStringLiteral("WindowFrom, WindowTo and OverrideMaxVal must be finite numbers");
return false;
}
lower = from / 100.0 * maximum;
upper = to / 100.0 * maximum;
if (!std::isfinite(lower) || !std::isfinite(upper)) {
error = QStringLiteral("Filter window exceeds finite numeric range");
return false;
}
}
BRep_Builder builder;
TopoDS_Compound compound;
builder.MakeCompound(compound);
TopoDS_Shape single;
QJsonArray indices;
for (std::size_t i = 0; i < children.size(); ++i) {
const bool passes = collision ? values[i] < Precision::Confusion()
: values[i] >= lower && values[i] <= upper;
if (bypass || (passes != invert)) {
single = children[i];
builder.Add(compound, single);
indices.append(static_cast<int>(i));
}
}
if (indices.isEmpty()) {
error = QStringLiteral("Nothing passes through the filter");
return false;
}
publishShape(indices.size() == 1 ? single : TopoDS_Shape(compound), result);
result.insert(QStringLiteral("selectedChildIndices"), indices);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("compound-filter"));
return true;
}
if (command == QLatin1String("Part_ToleranceSet")) {
const auto tolerance = [&args](const char* name, double fallback) {
const QJsonValue value = args.value(QLatin1String(name));
return value.isUndefined() ? fallback : value.isDouble()
? value.toDouble() : std::numeric_limits<double>::quiet_NaN();
};
const double minimum = tolerance("minTolerance", 1e-7);
const double maximum = tolerance("maxTolerance", 0.0);
if (!std::isfinite(minimum) || !std::isfinite(maximum)
|| minimum < 0.0 || maximum < 0.0) {
error = QStringLiteral("minTolerance and maxTolerance must be finite nonnegative numbers");
return false;
}
const QJsonValue refine = args.value(QStringLiteral("Refine"));
if (!refine.isUndefined() && !refine.isBool()) {
error = QStringLiteral("Refine must be a boolean");
return false;
}
if (refine.isUndefined() || refine.toBool()) {
error = QStringLiteral("Part_ToleranceSet requires explicit Refine=false until the FreeCAD fix/removeSplitter/fix contract is connected");
return false;
}
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (shapes.empty()) {
error = QStringLiteral("Part_ToleranceSet requires at least one selected BREP shape");
return false;
}
// FeatureToleranceSet.execute copies geometry before limiting its
// vertex, edge and face tolerances; zero maximum is OCCT's sentinel.
BRep_Builder builder;
TopoDS_Compound compound;
builder.MakeCompound(compound);
TopoDS_Shape single;
for (const TopoDS_Shape& shape : shapes) {
BRepBuilderAPI_Copy copy(shape, true, false);
if (!copy.IsDone() || copy.Shape().IsNull()) {
error = QStringLiteral("OpenCascade could not copy the selected shape");
return false;
}
single = copy.Shape();
ShapeFix_ShapeTolerance limiter;
limiter.LimitTolerance(single, minimum, maximum, TopAbs_SHAPE);
builder.Add(compound, single);
}
publishShape(shapes.size() == 1 ? single : TopoDS_Shape(compound), result);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("tolerance-set"));
return true;
}
if (command == QLatin1String("Part_Tube")) {
// BasicShapes.Shapes.TubeFeature.execute and makeTube geometry.
// No implemented marker until document properties, attachment and
// the default edit panel are connected to this geometry operation.
const auto parameter = [&args](const char* name, double fallback) {
const QJsonValue value = args.value(QLatin1String(name));
return value.isUndefined() ? fallback
: value.isDouble() ? value.toDouble()
: std::numeric_limits<double>::quiet_NaN();
};
const double outerRadius = parameter("OuterRadius", 5.0);
const double innerRadius = parameter("InnerRadius", 2.0);
const double height = parameter("Height", 10.0);
if (!std::isfinite(outerRadius) || !std::isfinite(innerRadius)
|| !std::isfinite(height) || outerRadius <= 0.0
|| innerRadius < 0.0 || height <= 0.0) {
error = QStringLiteral("OuterRadius and Height must be positive finite numbers; InnerRadius must be finite and nonnegative");
return false;
}
if (innerRadius >= outerRadius) {
error = QStringLiteral("Inner radius must be smaller than outer radius");
return false;
}
TopoDS_Shape tube = BRepPrimAPI_MakeCylinder(outerRadius, height).Shape();
if (innerRadius > 0.0) {
const TopoDS_Shape bore = BRepPrimAPI_MakeCylinder(innerRadius, height).Shape();
BRepAlgoAPI_Cut cut(tube, bore);
if (!cut.IsDone() || cut.Shape().IsNull()) {
error = QStringLiteral("OpenCascade could not cut the tube bore");
return false;
}
tube = cut.Shape();
}
if (!BRepCheck_Analyzer(tube).IsValid()) {
error = QStringLiteral("OpenCascade produced an invalid tube");
return false;
}
publishShape(tube, result);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("create-tube"));
return true;
}
if (command == QLatin1String("Part_ExplodeCompound")) {
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (shapes.size() != 1) {
error = QStringLiteral("Part_ExplodeCompound requires exactly one BREP shape");
return false;
}
// Explode.explodeCompound selects direct children, not recursively
// collected solids. Preserve their placement in the supplied BREP.
QJsonArray parts;
for (TopoDS_Iterator child(shapes.front()); child.More(); child.Next()) {
QJsonObject item;
publishShape(child.Value(), item);
item.insert(QStringLiteral("childIndex"), parts.size());
parts.append(item);
}
result.insert(QStringLiteral("parts"), parts);
result.insert(QStringLiteral("partCount"), parts.size());
result.insert(QStringLiteral("documentOperation"), QStringLiteral("explode-compound"));
return true;
}
if (command == QLatin1String("Part_JoinConnect")) {
std::vector<TopoDS_Shape> inputs;
if (!readShapes(args, inputs, error)) {
return false;
}
const QJsonValue value = args.value(QStringLiteral("tolerance"));
const double tolerance = value.toDouble(0.0);
if ((!value.isUndefined() && !value.isDouble())
|| !std::isfinite(tolerance) || tolerance < 0.0) {
error = QStringLiteral("Connect tolerance must be finite and nonnegative");
return false;
}
// JoinAPI.compoundLeaves recursively expands compounds before generalFuse.
std::vector<TopoDS_Shape> sources;
const auto appendLeaves = [&](const auto& self, const TopoDS_Shape& shape) -> bool {
if (shape.ShapeType() == TopAbs_COMPOUND) {
for (TopoDS_Iterator child(shape); child.More(); child.Next()) {
if (!self(self, child.Value())) return false;
}
return true;
}
if (shape.ShapeType() != TopAbs_SOLID) return false;
sources.push_back(shape);
return true;
};
for (const auto& input : inputs) {
if (!appendLeaves(appendLeaves, input)) {
error = QStringLiteral("Connect currently requires solid BREP leaves; shells, wires and compsolids remain implementation_required");
return false;
}
}
if (sources.empty()) {
error = QStringLiteral("Connect requires at least one nonempty solid selection");
return false;
}
TopoDS_Compound output;
BRep_Builder compoundBuilder;
compoundBuilder.MakeCompound(output);
if (sources.size() == 1) {
compoundBuilder.Add(output, sources.front());
}
else {
BOPAlgo_Builder split;
split.SetNonDestructive(true);
split.SetFuzzyValue(tolerance);
for (const auto& source : sources) split.AddArgument(source);
split.Perform();
if (split.HasErrors()) {
error = QStringLiteral("Connect generalFuse failed");
return false;
}
struct Piece {
TopoDS_Shape shape;
std::vector<std::size_t> owners;
};
std::vector<Piece> pieces;
for (TopExp_Explorer it(split.Shape(), TopAbs_SOLID); it.More(); it.Next()) {
bool duplicate = false;
for (const auto& piece : pieces) duplicate |= piece.shape.IsSame(it.Current());
if (!duplicate) pieces.push_back({it.Current(), {}});
}
for (std::size_t source = 0; source < sources.size(); ++source) {
const auto& modified = split.Modified(sources[source]);
for (auto& piece : pieces) {
bool belongs = piece.shape.IsSame(sources[source]);
for (TopTools_ListIteratorOfListOfShape it(modified); it.More(); it.Next()) {
belongs |= piece.shape.IsSame(it.Value());
for (TopExp_Explorer solid(it.Value(), TopAbs_SOLID); solid.More(); solid.Next()) {
belongs |= piece.shape.IsSame(solid.Current());
}
}
if (belongs) piece.owners.push_back(source);
}
}
for (const auto& piece : pieces) {
if (piece.owners.empty()) {
error = QStringLiteral("Connect could not resolve generalFuse source membership");
return false;
}
}
std::vector<TopoDS_Shape> keepers;
for (std::size_t source = 0; source < sources.size(); ++source) {
double maximum = -1.0;
int count = 0;
TopoDS_Shape largest;
for (const auto& piece : pieces) {
if (piece.owners.size() != 1 || piece.owners.front() != source) continue;
GProp_GProps properties;
BRepGProp::VolumeProperties(piece.shape, properties);
const double mass = std::abs(properties.Mass());
if (!std::isfinite(mass)) {
error = QStringLiteral("Connect fragment has nonfinite volume");
return false;
}
if (mass > maximum * (1.0 + 1e-8)) {
maximum = mass;
count = 1;
largest = piece.shape;
}
else if (mass >= maximum * (1.0 - 1e-8)) ++count;
}
if (count > 1) {
error = QStringLiteral("There is more than one largest piece!");
return false;
}
if (!largest.IsNull()) keepers.push_back(largest);
}
auto frontier = keepers;
for (std::size_t overlap = 2; overlap <= sources.size(); ++overlap) {
std::vector<TopoDS_Shape> next;
for (const auto& piece : pieces) {
if (piece.owners.size() != overlap) continue;
bool connected = false;
for (const auto& previous : frontier) {
for (TopExp_Explorer a(piece.shape, TopAbs_FACE); a.More(); a.Next()) {
for (TopExp_Explorer b(previous, TopAbs_FACE); b.More(); b.Next()) {
connected |= a.Current().IsSame(b.Current());
}
}
}
if (connected) next.push_back(piece.shape);
}
if (next.empty()) break;
keepers.insert(keepers.end(), next.begin(), next.end());
frontier = next;
}
if (!keepers.empty()) {
TopoDS_Shape merged = keepers.front();
for (std::size_t i = 1; i < keepers.size(); ++i) {
BRepAlgoAPI_Fuse fuse(merged, keepers[i]);
if (!fuse.IsDone() || fuse.Shape().IsNull()) {
error = QStringLiteral("Connect could not merge retained fragments");
return false;
}
merged = fuse.Shape();
}
for (TopExp_Explorer solid(merged, TopAbs_SOLID); solid.More(); solid.Next()) {
compoundBuilder.Add(output, solid.Current());
}
}
}
publishShape(output, result);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("join-connect"));
return true;
}
if (command == QLatin1String("Part_JoinCutout")
|| command == QLatin1String("Part_JoinEmbed")
|| command == QLatin1String("Part_Slice")
|| command == QLatin1String("Part_SliceApart")) {
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
const bool sliceApart = command == QLatin1String("Part_SliceApart");
const bool slice = command == QLatin1String("Part_Slice") || sliceApart;
if ((slice && shapes.size() < 2) || (!slice && shapes.size() != 2)) {
error = slice
? QStringLiteral("Part_Slice requires a base shape followed by at least one cutting tool")
: QStringLiteral("Join requires exactly two shapes in base, tool order");
return false;
}
if (shapes.front().ShapeType() != TopAbs_SOLID
|| (!slice && shapes[1].ShapeType() != TopAbs_SOLID)) {
error = QStringLiteral("This command currently supports solid bases and solid Join tools; aggregate, shell and wire Join semantics remain implementation_required");
return false;
}
const QJsonValue toleranceValue = args.value(QStringLiteral("tolerance"));
if (!toleranceValue.isUndefined() && !toleranceValue.isDouble()) {
error = QStringLiteral("tolerance must be a finite nonnegative number");
return false;
}
const double tolerance = toleranceValue.toDouble(0.0);
if (!std::isfinite(tolerance) || tolerance < 0.0) {
error = QStringLiteral("tolerance must be a finite nonnegative number");
return false;
}
TopoDS_Shape output;
if (slice) {
if (args.contains(QStringLiteral("mode"))
&& args.value(QStringLiteral("mode")).toString() != QLatin1String("Split")) {
error = QStringLiteral("Part_Slice desktop command requires mode Split");
return false;
}
TopTools_ListOfShape bases;
TopTools_ListOfShape tools;
bases.Append(shapes.front());
for (std::size_t i = 1; i < shapes.size(); ++i) {
tools.Append(shapes[i]);
}
// Split only the base: tool fragments must not enter the result.
BRepAlgoAPI_Splitter splitter;
splitter.SetArguments(bases);
splitter.SetTools(tools);
splitter.SetNonDestructive(true);
splitter.SetFuzzyValue(tolerance);
splitter.Build();
if (!splitter.IsDone() || splitter.Shape().IsNull()) {
error = QStringLiteral("OpenCascade could not slice the supplied base with the supplied tools");
return false;
}
QJsonArray parts;
TopoDS_Shape onlyPart;
for (TopExp_Explorer piece(splitter.Shape(), TopAbs_SOLID); piece.More(); piece.Next()) {
QJsonObject item;
onlyPart = piece.Current();
publishShape(onlyPart, item);
parts.append(item);
}
if (parts.isEmpty()) {
error = QStringLiteral("Slice produced no solid pieces");
return false;
}
output = parts.size() == 1 ? onlyPart : splitter.Shape();
result.insert(QStringLiteral("parts"), parts);
result.insert(QStringLiteral("partCount"), parts.size());
result.insert(QStringLiteral("documentOperation"), sliceApart
? QStringLiteral("slice-apart") : QStringLiteral("slice"));
}
else {
// JoinAPI's legacy Cutout/Embed ignore tolerance; do not silently
// apply fuzzy booleans with different semantics.
if (tolerance != 0.0) {
error = QStringLiteral("Join Cutout/Embed currently require tolerance 0, matching the legacy geometry handler");
return false;
}
BRepAlgoAPI_Cut cut(shapes.front(), shapes[1]);
if (!cut.IsDone() || cut.Shape().IsNull()) {
error = QStringLiteral("OpenCascade Join base-minus-tool operation failed");
return false;
}
double largestVolume = -1.0;
for (TopExp_Explorer piece(cut.Shape(), TopAbs_SOLID); piece.More(); piece.Next()) {
GProp_GProps properties;
BRepGProp::VolumeProperties(piece.Current(), properties);
const double volume = std::abs(properties.Mass());
if (std::isfinite(volume) && volume > largestVolume) {
largestVolume = volume;
output = piece.Current();
}
}
if (output.IsNull()) {
error = QStringLiteral("Join cut leaves no solid base fragment");
return false;
}
if (command == QLatin1String("Part_JoinEmbed")) {
BRepAlgoAPI_Fuse embed(output, shapes[1]);
if (!embed.IsDone() || embed.Shape().IsNull()) {
error = QStringLiteral("OpenCascade could not fuse the largest base fragment with the embedding tool");
return false;
}
output = embed.Shape();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("join-embed"));
}
else {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("join-cutout"));
}
}
publishShape(output, result);
return true;
}
if (command == QLatin1String("Part_XOR")) {
const double tolerance = args.value(QStringLiteral("tolerance")).toDouble(
args.contains(QStringLiteral("tolerance"))
? std::numeric_limits<double>::quiet_NaN() : 0.0);
if (!std::isfinite(tolerance) || tolerance < 0.0) {
error = QStringLiteral("XOR tolerance must be finite and nonnegative");
return false;
}
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (shapes.size() == 1 && shapes.front().ShapeType() == TopAbs_COMPOUND) {
const TopoDS_Shape compound = shapes.front();
shapes.clear();
for (TopoDS_Iterator child(compound); child.More(); child.Next()) {
shapes.push_back(child.Value());
}
}
if (shapes.size() < 2) {
error = QStringLiteral("XOR requires two shapes or a compound containing two shapes");
return false;
}
std::vector<TopoDS_Shape> pending = shapes;
while (!pending.empty()) {
const TopoDS_Shape shape = pending.back();
pending.pop_back();
if (shape.ShapeType() == TopAbs_SOLID) {
continue;
}
if (shape.ShapeType() != TopAbs_COMPOUND && shape.ShapeType() != TopAbs_COMPSOLID) {
error = QStringLiteral("XOR currently requires solids or aggregates of solids; surface and wire XOR is not implemented");
return false;
}
for (TopoDS_Iterator child(shape); child.More(); child.Next()) {
pending.push_back(child.Value());
}
}
BOPAlgo_Builder fragments;
fragments.SetNonDestructive(true);
fragments.SetFuzzyValue(tolerance);
for (const TopoDS_Shape& shape : shapes) {
fragments.AddArgument(shape);
}
fragments.Perform();
if (fragments.HasErrors() || fragments.Shape().IsNull()) {
error = QStringLiteral("XOR general fuse failed for the supplied shapes");
return false;
}
TopoDS_Compound output;
BRep_Builder builder;
builder.MakeCompound(output);
// SplitAPI.xor keeps general-fuse pieces with odd source membership.
// Volume overlap determines membership for the supported solid domain.
for (TopExp_Explorer piece(fragments.Shape(), TopAbs_SOLID); piece.More(); piece.Next()) {
GProp_GProps properties;
BRepGProp::VolumeProperties(piece.Current(), properties);
const double volume = std::abs(properties.Mass());
if (!std::isfinite(volume) || volume <= 0.0) {
error = QStringLiteral("XOR produced a solid without positive finite volume");
return false;
}
unsigned sources = 0;
for (const TopoDS_Shape& source : shapes) {
BRepAlgoAPI_Common overlap(piece.Current(), source);
if (!overlap.IsDone() || overlap.Shape().IsNull()) {
error = QStringLiteral("XOR source membership calculation failed");
return false;
}
GProp_GProps commonProperties;
BRepGProp::VolumeProperties(overlap.Shape(), commonProperties);
if (std::abs(commonProperties.Mass()) > volume * 0.5) {
++sources;
}
}
if (sources % 2 == 1) {
builder.Add(output, piece.Current());
}
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("boolean"));
publishShape(output, result);
return true;
}
if (command == QLatin1String("Part_BooleanFragments")) {
// SplitFeatures activates Standard mode; preserve general-fuse aggregates.
const QString mode = args.value(QStringLiteral("mode")).toString(QStringLiteral("Standard"));
if (mode != QLatin1String("Standard")) {
error = QStringLiteral("BooleanFragments currently supports Standard mode only");
return false;
}
const double tolerance = args.value(QStringLiteral("tolerance")).toDouble(
args.contains(QStringLiteral("tolerance"))
? std::numeric_limits<double>::quiet_NaN() : 0.0);
if (!std::isfinite(tolerance) || tolerance < 0.0) {
error = QStringLiteral("tolerance must be a finite nonnegative number");
return false;
}
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (shapes.size() == 1 && shapes.front().ShapeType() == TopAbs_COMPOUND) {
const TopoDS_Shape compound = shapes.front();
shapes.clear();
for (TopoDS_Iterator child(compound); child.More(); child.Next()) {
shapes.push_back(child.Value());
}
}
if (shapes.size() < 2) {
error = QStringLiteral("BooleanFragments requires at least two shapes or a compound containing at least two shapes");
return false;
}
BOPAlgo_Builder fragments;
fragments.SetNonDestructive(true);
fragments.SetFuzzyValue(tolerance);
for (const TopoDS_Shape& shape : shapes) {
fragments.AddArgument(shape);
}
fragments.Perform();
if (fragments.HasErrors() || fragments.Shape().IsNull()) {
error = QStringLiteral("BooleanFragments general fuse failed for the supplied shapes");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("boolean"));
result.insert(QStringLiteral("mode"), mode);
publishShape(fragments.Shape(), result);
return true;
}
if (spec->operation == Operation::BoxSelection) {
const QJsonArray selection = args.value(QStringLiteral("selection")).toArray();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("box-selection"));
result.insert(QStringLiteral("selection"), selection);
result.insert(QStringLiteral("selected"), selection.size());
return true;
}
if (spec->operation == Operation::SelectionFilter) {
const QJsonArray selection = args.value(QStringLiteral("selection")).toArray();
QString filter;
if (command == QStringLiteral("Part_EdgeSelection")) filter = QStringLiteral("edge");
else if (command == QStringLiteral("Part_FaceSelection")) filter = QStringLiteral("face");
else if (command == QStringLiteral("Part_VertexSelection")) filter = QStringLiteral("vertex");
else if (command == QStringLiteral("Part_RemoveSelectionGate")) filter = QStringLiteral("none");
else {
filter = args.value(QStringLiteral("filter")).toString(args.value(QStringLiteral("selectionType")).toString()).trimmed().toLower();
if (filter == QStringLiteral("edges")) filter = QStringLiteral("edge");
if (filter == QStringLiteral("faces")) filter = QStringLiteral("face");
if (filter == QStringLiteral("vertices")) filter = QStringLiteral("vertex");
if (filter.isEmpty() || (filter != QStringLiteral("edge") && filter != QStringLiteral("face") && filter != QStringLiteral("vertex") && filter != QStringLiteral("none"))) {
error = QStringLiteral("Part_SelectFilter requires filter edge, face, vertex or none");
return false;
}
}
QJsonArray accepted;
for (const QJsonValue& item : selection) {
if (filter == QStringLiteral("none")) { accepted.append(item); continue; }
QString type;
if (item.isObject()) {
const QJsonObject object = item.toObject();
type = object.value(QStringLiteral("type")).toString(object.value(QStringLiteral("shapeType")).toString()).trimmed().toLower();
} else if (item.isString()) {
type = item.toString().trimmed().toLower();
}
if (type == filter || type == filter + QStringLiteral("s")) accepted.append(item);
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("selection-filter"));
result.insert(QStringLiteral("filter"), filter);
result.insert(QStringLiteral("selection"), accepted);
result.insert(QStringLiteral("selected"), accepted.size());
result.insert(QStringLiteral("inputSelected"), selection.size());
return true;
}
if (spec->operation == Operation::Box) {
std::array<double, 3> dimensions;
if (!readDimensions(args, dimensions, error)) {
return false;
}
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array");
return false;
}
const TopoDS_Shape shape = BRepPrimAPI_MakeBox(origin, dimensions[0], dimensions[1], dimensions[2]).Shape();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("create-box"));
publishShape(shape, result);
return true;
}
if (spec->operation == Operation::ParametricPrimitive) {
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array");
return false;
}
const gp_Ax2 axis(origin, gp_Dir(0, 0, 1));
TopoDS_Shape shape;
if (command == QStringLiteral("Part_Cylinder")) {
const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits<double>::quiet_NaN());
const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(radius) || !std::isfinite(height) || radius <= 0.0 || height <= 0.0) {
error = QStringLiteral("radius and height must be positive finite numbers");
return false;
}
shape = BRepPrimAPI_MakeCylinder(axis, radius, height).Shape();
result.insert(QStringLiteral("radius"), radius);
result.insert(QStringLiteral("height"), height);
}
else if (command == QStringLiteral("Part_Cone")) {
const double radius1 = args.value(QStringLiteral("radius1")).toDouble(args.value(QStringLiteral("radius")).toDouble(std::numeric_limits<double>::quiet_NaN()));
const double radius2 = args.value(QStringLiteral("radius2")).toDouble(0.0);
const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(radius1) || !std::isfinite(radius2) || !std::isfinite(height) || radius1 < 0.0 || radius2 < 0.0 || (radius1 == 0.0 && radius2 == 0.0) || height <= 0.0) {
error = QStringLiteral("radius1/radius2 and height must define a positive finite cone");
return false;
}
shape = BRepPrimAPI_MakeCone(axis, radius1, radius2, height).Shape();
result.insert(QStringLiteral("radius1"), radius1);
result.insert(QStringLiteral("radius2"), radius2);
result.insert(QStringLiteral("height"), height);
}
else if (command == QStringLiteral("Part_Sphere")) {
const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(radius) || radius <= 0.0) {
error = QStringLiteral("radius must be a positive finite number");
return false;
}
shape = BRepPrimAPI_MakeSphere(origin, radius).Shape();
result.insert(QStringLiteral("radius"), radius);
}
else {
const double majorRadius = args.value(QStringLiteral("radius1")).toDouble(args.value(QStringLiteral("majorRadius")).toDouble(std::numeric_limits<double>::quiet_NaN()));
const double minorRadius = args.value(QStringLiteral("radius2")).toDouble(args.value(QStringLiteral("minorRadius")).toDouble(std::numeric_limits<double>::quiet_NaN()));
if (!std::isfinite(majorRadius) || !std::isfinite(minorRadius) || majorRadius <= 0.0 || minorRadius <= 0.0 || majorRadius <= minorRadius) {
error = QStringLiteral("major and minor torus radii must be positive with major radius larger");
return false;
}
shape = BRepPrimAPI_MakeTorus(axis, majorRadius, minorRadius).Shape();
result.insert(QStringLiteral("majorRadius"), majorRadius);
result.insert(QStringLiteral("minorRadius"), minorRadius);
}
if (shape.IsNull()) {
error = QStringLiteral("OpenCascade primitive construction failed");
return false;
}
const QString primitiveType = command.mid(QStringLiteral("Part_").size()).toLower();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("create-") + primitiveType);
result.insert(QStringLiteral("primitiveType"), primitiveType);
publishShape(shape, result);
return true;
}
if (spec->operation == Operation::Datum) {
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array");
return false;
}
if (command == QStringLiteral("Part_DatumPoint")) {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-point"));
result.insert(QStringLiteral("point"), QJsonArray{origin.X(), origin.Y(), origin.Z()});
}
else {
gp_Pnt directionPoint;
if (!readPoint(args.value(QStringLiteral("direction")), directionPoint)) {
error = QStringLiteral("direction must be a finite [x,y,z] array");
return false;
}
gp_Vec direction(directionPoint.X(), directionPoint.Y(), directionPoint.Z());
if (direction.SquareMagnitude() <= gp::Resolution()) {
error = QStringLiteral("direction must be non-zero");
return false;
}
if (command == QStringLiteral("Part_DatumLine")) {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-line"));
result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()});
result.insert(QStringLiteral("direction"), QJsonArray{direction.X(), direction.Y(), direction.Z()});
}
else {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-plane"));
result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()});
result.insert(QStringLiteral("normal"), QJsonArray{direction.X(), direction.Y(), direction.Z()});
}
}
return true;
}
if (spec->operation == Operation::DatumGroup) {
const QJsonArray children{
QStringLiteral("Part_CoordinateSystem"),
QStringLiteral("Part_DatumPlane"),
QStringLiteral("Part_DatumLine"),
QStringLiteral("Part_DatumPoint"),
};
const bool hasActiveDocument = args.value(QStringLiteral("hasActiveDocument")).toBool(true);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("datum-group"));
result.insert(QStringLiteral("menuText"), QStringLiteral("Datums"));
result.insert(QStringLiteral("toolTip"), QStringLiteral("Creates a datum object (coordinate system, plane, line, or point) that can be attached to other objects"));
result.insert(QStringLiteral("checkable"), false);
result.insert(QStringLiteral("active"), hasActiveDocument);
result.insert(QStringLiteral("children"), children);
return true;
}
if (spec->operation == Operation::Import) {
TopoDS_Shape shape;
if (!readShape(args.value(QStringLiteral("brep")), shape)) {
error = QStringLiteral("Part_Import requires a valid BREP string");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("import"));
result.insert(QStringLiteral("format"), args.value(QStringLiteral("format")).toString(QStringLiteral("brep")));
publishShape(shape, result);
return true;
}
if (spec->operation == Operation::NewDocument) {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("new-document"));
result.insert(QStringLiteral("documentName"), args.value(QStringLiteral("name")).toString(QStringLiteral("Unnamed")));
return true;
}
if (spec->operation == Operation::SimpleCylinder) {
const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits<double>::quiet_NaN());
const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(radius) || !std::isfinite(height) || radius <= 0.0 || height <= 0.0) { error = QStringLiteral("radius and height must be positive finite numbers"); return false; }
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; }
const TopoDS_Shape cylinder = BRepPrimAPI_MakeCylinder(gp_Ax2(origin, gp_Dir(0, 0, 1)), radius, height).Shape();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("simple-cylinder"));
result.insert(QStringLiteral("radius"), radius); result.insert(QStringLiteral("height"), height);
publishShape(cylinder, result); return true;
}
if (spec->operation == Operation::PointsFromMesh || spec->operation == Operation::ShapeFromMesh) {
const QJsonArray vertices = args.value(QStringLiteral("vertices")).toArray();
const QJsonArray triangles = args.value(QStringLiteral("triangles")).toArray();
if (vertices.isEmpty()) { error = QStringLiteral("mesh vertices are required"); return false; }
if (spec->operation == Operation::PointsFromMesh) {
QJsonArray points;
for (const QJsonValue& value : vertices) { gp_Pnt point; if (!readPoint(value, point)) { error = QStringLiteral("vertices must contain finite [x,y,z] arrays"); return false; } points.append(QJsonArray{point.X(), point.Y(), point.Z()}); }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("points-from-mesh")); result.insert(QStringLiteral("points"), points); result.insert(QStringLiteral("pointCount"), points.size()); return true;
}
if (triangles.isEmpty()) { error = QStringLiteral("mesh triangles are required"); return false; }
TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound);
for (const QJsonValue& triangle : triangles) {
const QJsonArray indices = triangle.toArray(); if (indices.size() != 3) { error = QStringLiteral("triangles must contain three vertex indices"); return false; }
BRepBuilderAPI_MakePolygon polygon;
for (const QJsonValue& indexValue : indices) { const int index = indexValue.toInt(-1); if (index < 0 || index >= vertices.size()) { error = QStringLiteral("triangle index is out of range"); return false; } gp_Pnt point; if (!readPoint(vertices.at(index), point)) { error = QStringLiteral("vertices must contain finite [x,y,z] arrays"); return false; } polygon.Add(point); }
polygon.Close(); BRepBuilderAPI_MakeFace face(polygon.Wire()); if (!face.IsDone()) { error = QStringLiteral("mesh triangle could not be converted to a face"); return false; } builder.Add(compound, face.Shape());
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("shape-from-mesh")); result.insert(QStringLiteral("triangleCount"), triangles.size()); publishShape(compound, result); return true;
}
std::vector<TopoDS_Shape> shapes;
if (!readShapes(args, shapes, error)) {
return false;
}
if (spec->operation == Operation::SimpleCopy || spec->operation == Operation::RefineShape || spec->operation == Operation::Defeaturing) {
if (shapes.size() != 1) { error = QStringLiteral("command requires exactly one BREP shape"); return false; }
result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::SimpleCopy ? QStringLiteral("simple-copy") : spec->operation == Operation::RefineShape ? QStringLiteral("refine-shape") : QStringLiteral("defeaturing"));
publishShape(shapes.front(), result); return true;
}
if (spec->operation == Operation::ElementCopy) {
if (shapes.size() != 1) { error = QStringLiteral("Part_ElementCopy requires exactly one BREP shape"); return false; }
const int elementIndex = args.value(QStringLiteral("elementIndex")).toInt(-1); if (elementIndex < 0) { error = QStringLiteral("elementIndex must be non-negative"); return false; }
const QString type = args.value(QStringLiteral("elementType")).toString(QStringLiteral("FACE")); const TopAbs_ShapeEnum kind = type == QStringLiteral("EDGE") ? TopAbs_EDGE : type == QStringLiteral("VERTEX") ? TopAbs_VERTEX : TopAbs_FACE;
TopExp_Explorer explorer(shapes.front(), kind); for (int index = 0; explorer.More() && index < elementIndex; ++index) explorer.Next(); if (!explorer.More()) { error = QStringLiteral("elementIndex does not select a shape element"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("element-copy")); result.insert(QStringLiteral("elementIndex"), elementIndex); publishShape(explorer.Current(), result); return true;
}
if (spec->operation == Operation::TransformedCopy) {
if (shapes.size() != 1) { error = QStringLiteral("Part_TransformedCopy requires exactly one BREP shape"); return false; }
gp_Pnt translation; if (!readPoint(args.value(QStringLiteral("translation")), translation)) { error = QStringLiteral("translation must be a finite [x,y,z] array"); return false; }
gp_Trsf transform; transform.SetTranslation(gp_Vec(translation.X(), translation.Y(), translation.Z())); BRepBuilderAPI_Transform copied(shapes.front(), transform, true); if (!copied.IsDone() || copied.Shape().IsNull()) { error = QStringLiteral("OpenCascade transformed copy failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("transformed-copy")); publishShape(copied.Shape(), result); return true;
}
if (spec->operation == Operation::Primitives) {
std::array<double, 3> dimensions;
if (!readDimensions(args, dimensions, error)) return false;
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array"); return false;
}
const TopoDS_Shape primitive = BRepPrimAPI_MakeBox(origin, dimensions[0], dimensions[1], dimensions[2]).Shape();
result.insert(QStringLiteral("documentOperation"), QStringLiteral("primitive"));
result.insert(QStringLiteral("primitiveType"), args.value(QStringLiteral("type")).toString(QStringLiteral("box")));
publishShape(primitive, result);
return true;
}
if (spec->operation == Operation::Copy) {
if (shapes.empty()) {
error = QStringLiteral("Copy requires at least one BREP shape");
return false;
}
if (shapes.size() == 1) {
result.insert(QStringLiteral("documentOperation"), QStringLiteral("copy"));
publishShape(shapes.front(), result);
return true;
}
TopoDS_Compound compound;
BRep_Builder builder;
builder.MakeCompound(compound);
for (const TopoDS_Shape& shape : shapes) {
builder.Add(compound, shape);
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("copy"));
result.insert(QStringLiteral("shapeCount"), static_cast<int>(shapes.size()));
publishShape(compound, result);
return true;
}
if (spec->operation == Operation::ColorPerFace) {
if (shapes.size() != 1) {
error = QStringLiteral("Part_ColorPerFace requires exactly one BREP shape");
return false;
}
int faceCount = 0;
for (TopExp_Explorer explorer(shapes.front(), TopAbs_FACE); explorer.More(); explorer.Next()) {
++faceCount;
}
const QJsonArray colors = args.value(QStringLiteral("colors")).toArray();
if (!colors.isEmpty() && colors.size() != faceCount) {
error = QStringLiteral("colors must contain one entry per face");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("color-per-face"));
result.insert(QStringLiteral("faceCount"), faceCount);
result.insert(QStringLiteral("colors"), colors);
publishShape(shapes.front(), result);
return true;
}
if (spec->operation == Operation::PickCurveNet) {
if (shapes.empty()) { error = QStringLiteral("Part_PickCurveNet requires at least one curve BREP shape"); return false; }
TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound);
int edges = 0;
for (const TopoDS_Shape& shape : shapes) { builder.Add(compound, shape); for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) ++edges; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("pick-curve-network"));
result.insert(QStringLiteral("curveCount"), static_cast<int>(shapes.size()));
result.insert(QStringLiteral("edgeCount"), edges);
publishShape(compound, result);
return true;
}
if (spec->operation == Operation::ReverseShape) {
if (shapes.empty()) { error = QStringLiteral("Part_ReverseShape requires at least one BREP shape"); return false; }
TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound);
for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape.Reversed());
result.insert(QStringLiteral("documentOperation"), QStringLiteral("reverse-shapes"));
result.insert(QStringLiteral("shapeCount"), static_cast<int>(shapes.size()));
publishShape(compound, result);
return true;
}
if (spec->operation == Operation::Scale) {
if (shapes.size() != 1) { error = QStringLiteral("Part_Scale requires exactly one BREP shape"); return false; }
const double factor = args.value(QStringLiteral("factor")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(factor) || factor == 0.0) { error = QStringLiteral("factor must be a non-zero finite number"); return false; }
gp_Pnt center;
if (args.contains(QStringLiteral("center")) && !readPoint(args.value(QStringLiteral("center")), center)) { error = QStringLiteral("center must be a finite [x,y,z] array"); return false; }
gp_Trsf transform; transform.SetScale(center, factor);
BRepBuilderAPI_Transform scaled(shapes.front(), transform, true);
if (!scaled.IsDone() || scaled.Shape().IsNull()) { error = QStringLiteral("OpenCascade scale operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("scale")); result.insert(QStringLiteral("factor"), factor);
publishShape(scaled.Shape(), result); return true;
}
if (spec->operation == Operation::Revolve) {
if (shapes.size() != 1) { error = QStringLiteral("Part_Revolve requires exactly one BREP shape"); return false; }
gp_Pnt origin; if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) { error = QStringLiteral("origin must be a finite [x,y,z] array"); return false; }
gp_Pnt directionPoint; if (!readPoint(args.value(QStringLiteral("direction")), directionPoint)) { error = QStringLiteral("direction must be a finite [x,y,z] array"); return false; }
gp_Vec direction(origin, directionPoint); if (direction.SquareMagnitude() <= gp::Resolution()) { error = QStringLiteral("direction must be non-zero"); return false; }
const double angle = args.value(QStringLiteral("angle")).toDouble(2.0 * std::acos(-1.0)); if (!std::isfinite(angle) || angle == 0.0) { error = QStringLiteral("angle must be a non-zero finite number"); return false; }
const TopoDS_Shape revolved = BRepPrimAPI_MakeRevol(shapes.front(), gp_Ax1(origin, gp_Dir(direction)), angle).Shape();
if (revolved.IsNull()) { error = QStringLiteral("OpenCascade revolve operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("revolve")); result.insert(QStringLiteral("angle"), angle); publishShape(revolved, result); return true;
}
if (spec->operation == Operation::Sweep) {
if (shapes.size() != 2 || shapes[0].ShapeType() != TopAbs_WIRE || shapes[1].ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_Sweep requires profile and path wire BREP shapes"); return false; }
BRepOffsetAPI_MakePipe sweep(TopoDS::Wire(shapes[1]), shapes[0]);
if (!sweep.IsDone() || sweep.Shape().IsNull()) { error = QStringLiteral("OpenCascade sweep operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("sweep"));
publishShape(sweep.Shape(), result); return true;
}
if (spec->operation == Operation::Thickness) {
if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_SOLID) { error = QStringLiteral("Part_Thickness requires exactly one solid BREP shape"); return false; }
const double offset = args.value(QStringLiteral("offset")).toDouble(args.value(QStringLiteral("value")).toDouble(std::numeric_limits<double>::quiet_NaN()));
const double tolerance = args.value(QStringLiteral("tolerance")).toDouble(1.0e-3);
if (!std::isfinite(offset) || offset == 0.0 || !std::isfinite(tolerance) || tolerance <= 0.0) { error = QStringLiteral("offset must be a non-zero finite number and tolerance must be positive"); return false; }
const QJsonArray requestedFaces = args.value(QStringLiteral("faceIndices")).toArray();
std::vector<TopoDS_Shape> faces; for (TopExp_Explorer explorer(shapes.front(), TopAbs_FACE); explorer.More(); explorer.Next()) faces.push_back(explorer.Current());
TopTools_ListOfShape removedFaces;
if (requestedFaces.isEmpty()) removedFaces.Append(faces.front());
else for (const QJsonValue& value : requestedFaces) { const int index = value.toInt(-1); if (index < 0 || index >= static_cast<int>(faces.size())) { error = QStringLiteral("faceIndices contains an invalid face index"); return false; } removedFaces.Append(faces[static_cast<size_t>(index)]); }
BRepOffsetAPI_MakeThickSolid thick; thick.MakeThickSolidByJoin(shapes.front(), removedFaces, offset, tolerance, BRepOffset_Skin, Standard_False, Standard_False, GeomAbs_Arc);
if (!thick.IsDone() || thick.Shape().IsNull()) { error = QStringLiteral("OpenCascade thickness operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("thickness")); result.insert(QStringLiteral("offset"), offset); result.insert(QStringLiteral("tolerance"), tolerance); result.insert(QStringLiteral("removedFaceCount"), requestedFaces.isEmpty() ? 1 : requestedFaces.size()); publishShape(thick.Shape(), result); return true;
}
if (spec->operation == Operation::ShapeInfo) {
if (shapes.size() != 1) { error = QStringLiteral("Part_ShapeInfo requires exactly one BREP shape"); return false; }
QJsonObject counts; const TopAbs_ShapeEnum kinds[] = {TopAbs_COMPOUND, TopAbs_COMPSOLID, TopAbs_SOLID, TopAbs_SHELL, TopAbs_FACE, TopAbs_WIRE, TopAbs_EDGE, TopAbs_VERTEX}; const char* names[] = {"compound", "compsolid", "solid", "shell", "face", "wire", "edge", "vertex"};
for (size_t i = 0; i < std::size(kinds); ++i) { int count = 0; for (TopExp_Explorer explorer(shapes.front(), kinds[i]); explorer.More(); explorer.Next()) ++count; counts.insert(QString::fromLatin1(names[i]), count); }
GProp_GProps volumeProps, surfaceProps; BRepGProp::VolumeProperties(shapes.front(), volumeProps); BRepGProp::SurfaceProperties(shapes.front(), surfaceProps);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("shape-info")); result.insert(QStringLiteral("counts"), counts); result.insert(QStringLiteral("volume"), volumeProps.Mass()); result.insert(QStringLiteral("surfaceArea"), surfaceProps.Mass()); result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shapes.front()).IsValid()); result.insert(QStringLiteral("brep"), writeShape(shapes.front())); return true;
}
if (spec->operation == Operation::Section || spec->operation == Operation::ProjectionOnSurface) {
if (shapes.size() != 2) { error = QStringLiteral("section/projection commands require exactly two BREP shapes"); return false; }
BRepAlgoAPI_Section operation(shapes[0], shapes[1], false); operation.Build();
if (!operation.IsDone() || operation.Shape().IsNull()) { error = QStringLiteral("OpenCascade section operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Section ? QStringLiteral("section") : QStringLiteral("projection-on-surface")); publishShape(operation.Shape(), result); return true;
}
if (spec->operation == Operation::RuledSurface) {
if (shapes.size() != 2 || shapes[0].ShapeType() != TopAbs_WIRE || shapes[1].ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_RuledSurface requires exactly two wire BREP shapes"); return false; }
BRepOffsetAPI_ThruSections ruled(false, true); ruled.AddWire(TopoDS::Wire(shapes[0])); ruled.AddWire(TopoDS::Wire(shapes[1])); ruled.Build();
if (!ruled.IsDone() || ruled.Shape().IsNull()) { error = QStringLiteral("OpenCascade ruled surface operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("ruled-surface")); publishShape(ruled.Shape(), result); return true;
}
if (spec->operation == Operation::CoordinateSystem) {
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array");
return false;
}
QJsonArray axes;
axes.append(QJsonArray{1.0, 0.0, 0.0});
axes.append(QJsonArray{0.0, 1.0, 0.0});
axes.append(QJsonArray{0.0, 0.0, 1.0});
result.insert(QStringLiteral("documentOperation"), QStringLiteral("coordinate-system"));
result.insert(QStringLiteral("origin"), QJsonArray{origin.X(), origin.Y(), origin.Z()});
result.insert(QStringLiteral("axes"), axes);
return true;
}
if (spec->operation == Operation::Face) {
if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_WIRE) {
error = QStringLiteral("Part_MakeFace requires exactly one wire BREP shape");
return false;
}
BRepBuilderAPI_MakeFace face(TopoDS::Wire(shapes.front()));
if (!face.IsDone()) { error = QStringLiteral("OpenCascade face construction failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("make-face"));
publishShape(face.Shape(), result);
return true;
}
if (spec->operation == Operation::Solid) {
if (shapes.size() != 1 || shapes.front().ShapeType() != TopAbs_SHELL) {
error = QStringLiteral("Part_MakeSolid requires exactly one shell BREP shape");
return false;
}
BRepBuilderAPI_MakeSolid solid(TopoDS::Shell(shapes.front()));
if (!solid.IsDone()) { error = QStringLiteral("OpenCascade solid construction failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("make-solid"));
publishShape(solid.Solid(), result);
return true;
}
if (spec->operation == Operation::Mirror) {
if (shapes.size() != 1) { error = QStringLiteral("Part_Mirror requires exactly one BREP shape"); return false; }
gp_Pnt origin;
if (args.contains(QStringLiteral("origin")) && !readPoint(args.value(QStringLiteral("origin")), origin)) {
error = QStringLiteral("origin must be a finite [x,y,z] array"); return false;
}
gp_Pnt normalPoint;
if (!readPoint(args.value(QStringLiteral("normal")), normalPoint)) {
error = QStringLiteral("normal must be a finite [x,y,z] array"); return false;
}
gp_Vec normal(origin, normalPoint);
if (normal.SquareMagnitude() <= gp::Resolution()) { error = QStringLiteral("normal must be non-zero"); return false; }
gp_Trsf transform;
transform.SetMirror(gp_Ax2(origin, gp_Dir(normal)));
BRepBuilderAPI_Transform mirrored(shapes.front(), transform, true);
if (!mirrored.IsDone() || mirrored.Shape().IsNull()) { error = QStringLiteral("OpenCascade mirror operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("mirror"));
publishShape(mirrored.Shape(), result);
return true;
}
if (spec->operation == Operation::Loft) {
if (shapes.size() < 2) { error = QStringLiteral("Part_Loft requires at least two wire BREP shapes"); return false; }
BRepOffsetAPI_ThruSections loft(true, args.value(QStringLiteral("ruled")).toBool(false));
for (const TopoDS_Shape& shape : shapes) {
if (shape.ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part_Loft requires wire BREP shapes"); return false; }
loft.AddWire(TopoDS::Wire(shape));
}
loft.Build();
if (!loft.IsDone() || loft.Shape().IsNull()) { error = QStringLiteral("OpenCascade loft operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("loft"));
publishShape(loft.Shape(), result);
return true;
}
if (spec->operation == Operation::ImportCurveNet) {
if (shapes.empty()) { error = QStringLiteral("Part_ImportCurveNet requires at least one curve BREP shape"); return false; }
TopoDS_Compound compound;
BRep_Builder builder; builder.MakeCompound(compound);
for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape);
result.insert(QStringLiteral("documentOperation"), QStringLiteral("import-curve-network"));
result.insert(QStringLiteral("curveCount"), static_cast<int>(shapes.size()));
publishShape(compound, result);
return true;
}
if (spec->operation == Operation::Compound || spec->operation == Operation::Join) {
if (shapes.empty()) {
error = QStringLiteral("compound/join commands require at least one BREP shape");
return false;
}
TopoDS_Compound compound;
BRep_Builder builder;
builder.MakeCompound(compound);
for (const TopoDS_Shape& shape : shapes) builder.Add(compound, shape);
result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Join ? QStringLiteral("join") : QStringLiteral("compound"));
result.insert(QStringLiteral("childCount"), static_cast<int>(shapes.size()));
publishShape(compound, result);
return true;
}
if (spec->operation == Operation::Split) {
if (shapes.empty()) {
error = QStringLiteral("Part_CompSplitFeatures requires at least one BREP shape");
return false;
}
QJsonArray parts;
for (const TopoDS_Shape& shape : shapes) {
QJsonObject part;
publishShape(shape, part);
parts.append(part);
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("split"));
result.insert(QStringLiteral("parts"), parts);
result.insert(QStringLiteral("partCount"), parts.size());
return true;
}
if (spec->operation == Operation::CrossSections) {
if (shapes.empty()) {
error = QStringLiteral("Part_CrossSections requires at least one BREP shape");
return false;
}
const double z = args.value(QStringLiteral("z")).toDouble(0.0);
if (!std::isfinite(z)) { error = QStringLiteral("z must be finite"); return false; }
gp_Pln plane(gp_Pnt(0, 0, z), gp_Dir(0, 0, 1));
QJsonArray sections;
for (const TopoDS_Shape& shape : shapes) {
BRepAlgoAPI_Section section(shape, plane, false);
section.Build();
if (section.IsDone() && !section.Shape().IsNull()) { QJsonObject item; publishShape(section.Shape(), item); sections.append(item); }
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("cross-sections"));
result.insert(QStringLiteral("z"), z);
result.insert(QStringLiteral("sections"), sections);
result.insert(QStringLiteral("sectionCount"), sections.size());
return true;
}
if (spec->operation == Operation::Offset || spec->operation == Operation::Offset2D) {
if (shapes.size() != 1) { error = QStringLiteral("Part offset commands require exactly one BREP shape"); return false; }
const double distance = args.value(QStringLiteral("distance")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(distance) || distance == 0.0) { error = QStringLiteral("distance must be a non-zero finite number"); return false; }
if (shapes.front().ShapeType() != TopAbs_WIRE) { error = QStringLiteral("Part offset commands require a wire shape"); return false; }
BRepOffsetAPI_MakeOffset offset;
offset.AddWire(TopoDS::Wire(shapes.front()));
offset.Perform(distance);
if (!offset.IsDone() || offset.Shape().IsNull()) { error = QStringLiteral("OpenCascade offset operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), spec->operation == Operation::Offset2D ? QStringLiteral("offset-2d") : QStringLiteral("offset"));
result.insert(QStringLiteral("distance"), distance);
publishShape(offset.Shape(), result);
return true;
}
if (spec->operation == Operation::CheckGeometry) {
if (shapes.size() != 1) {
error = QStringLiteral("Part_CheckGeometry requires exactly one BREP shape");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("check-geometry"));
publishShape(shapes.front(), result);
return true;
}
if (spec->operation == Operation::Export) {
if (shapes.size() != 1) {
error = QStringLiteral("Part_Export requires exactly one BREP shape");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("export"));
result.insert(QStringLiteral("format"), args.value(QStringLiteral("format")).toString(QStringLiteral("brep")));
result.insert(QStringLiteral("brep"), writeShape(shapes.front()));
result.insert(QStringLiteral("valid"), BRepCheck_Analyzer(shapes.front()).IsValid());
return true;
}
if (spec->operation == Operation::Extrude) {
if (shapes.size() != 1) {
error = QStringLiteral("Part_Extrude requires exactly one BREP shape");
return false;
}
gp_Pnt vectorPoint;
if (!readPoint(args.value(QStringLiteral("vector")), vectorPoint)) {
error = QStringLiteral("vector must be a finite [x,y,z] array");
return false;
}
const gp_Vec vector(vectorPoint.X(), vectorPoint.Y(), vectorPoint.Z());
if (vector.SquareMagnitude() <= gp::Resolution()) {
error = QStringLiteral("vector must be non-zero");
return false;
}
const TopoDS_Shape output = BRepPrimAPI_MakePrism(shapes.front(), vector).Shape();
if (output.IsNull()) { error = QStringLiteral("OpenCascade extrusion failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("extrude"));
result.insert(QStringLiteral("vector"), QJsonArray{vector.X(), vector.Y(), vector.Z()});
publishShape(output, result);
return true;
}
if (spec->operation == Operation::Fillet) {
if (shapes.size() != 1) { error = QStringLiteral("Part_Fillet requires exactly one BREP shape"); return false; }
const double radius = args.value(QStringLiteral("radius")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(radius) || radius <= 0.0) { error = QStringLiteral("radius must be a positive finite number"); return false; }
const int edgeIndex = args.value(QStringLiteral("edgeIndex")).toInt(0);
TopExp_Explorer edges(shapes.front(), TopAbs_EDGE);
for (int index = 0; edges.More() && index < edgeIndex; ++index) edges.Next();
if (!edges.More()) { error = QStringLiteral("edgeIndex does not select an edge"); return false; }
BRepFilletAPI_MakeFillet fillet(shapes.front());
fillet.Add(radius, TopoDS::Edge(edges.Current()));
fillet.Build();
if (!fillet.IsDone() || fillet.Shape().IsNull()) { error = QStringLiteral("OpenCascade fillet operation failed"); return false; }
result.insert(QStringLiteral("documentOperation"), QStringLiteral("fillet"));
result.insert(QStringLiteral("radius"), radius);
publishShape(fillet.Shape(), result);
return true;
}
if (spec->operation == Operation::Builder) {
const QJsonArray points = args.value(QStringLiteral("points")).toArray();
if (points.size() < 3) {
error = QStringLiteral("Part_Builder requires at least three points");
return false;
}
BRepBuilderAPI_MakePolygon polygon;
for (const QJsonValue& value : points) {
gp_Pnt point;
if (!readPoint(value, point)) {
error = QStringLiteral("points must contain finite [x,y,z] arrays");
return false;
}
polygon.Add(point);
}
polygon.Close();
BRepBuilderAPI_MakeFace face(polygon.Wire());
if (!face.IsDone()) {
error = QStringLiteral("OpenCascade shape builder failed");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("build-shape"));
publishShape(face.Shape(), result);
return true;
}
if (spec->operation == Operation::Chamfer) {
if (shapes.size() != 1) {
error = QStringLiteral("Part_Chamfer requires exactly one BREP shape");
return false;
}
const double distance = args.value(QStringLiteral("distance")).toDouble(std::numeric_limits<double>::quiet_NaN());
if (!std::isfinite(distance) || distance <= 0.0) {
error = QStringLiteral("distance must be a positive finite number");
return false;
}
const int edgeIndex = args.value(QStringLiteral("edgeIndex")).toInt(0);
TopExp_Explorer edges(shapes.front(), TopAbs_EDGE);
for (int index = 0; edges.More() && index < edgeIndex; ++index) {
edges.Next();
}
if (!edges.More()) {
error = QStringLiteral("edgeIndex does not select an edge");
return false;
}
BRepFilletAPI_MakeChamfer chamfer(shapes.front());
chamfer.Add(distance, TopoDS::Edge(edges.Current()));
chamfer.Build();
if (!chamfer.IsDone() || chamfer.Shape().IsNull()) {
error = QStringLiteral("OpenCascade chamfer operation failed");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("chamfer"));
publishShape(chamfer.Shape(), result);
return true;
}
if (spec->operation == Operation::Boolean) {
if (shapes.size() < 2) {
error = QStringLiteral("Part boolean commands require at least two BREP shapes");
return false;
}
const QString operation = command == QStringLiteral("Part_Common")
? QStringLiteral("common")
: command == QStringLiteral("Part_Cut")
? QStringLiteral("cut")
: args.value(QStringLiteral("operation")).toString(QStringLiteral("fuse"));
TopoDS_Shape output;
if (operation == QStringLiteral("common")) {
output = BRepAlgoAPI_Common(shapes[0], shapes[1]).Shape();
}
else if (operation == QStringLiteral("cut")) {
output = BRepAlgoAPI_Cut(shapes[0], shapes[1]).Shape();
}
else if (operation == QStringLiteral("fuse")) {
output = BRepAlgoAPI_Fuse(shapes[0], shapes[1]).Shape();
}
else {
error = QStringLiteral("operation must be common, cut or fuse");
return false;
}
if (output.IsNull()) {
error = QStringLiteral("OpenCascade boolean produced an empty shape");
return false;
}
result.insert(QStringLiteral("documentOperation"), QStringLiteral("boolean"));
result.insert(QStringLiteral("operation"), operation);
publishShape(output, result);
return true;
}
}
catch (const Standard_Failure& exception) {
error = QString::fromUtf8(exception.GetMessageString());
return false;
}
catch (const std::exception& exception) {
error = QString::fromUtf8(exception.what());
return false;
}
error = QStringLiteral("Part desktop command has no implementation");
return false;
}
} // namespace FreeCADWeb