// SPDX-License-Identifier: LGPL-2.1-or-later #include #include "PartDesktopCommands.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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::quiet_NaN()); const double y = coordinates.at(1).toDouble(std::numeric_limits::quiet_NaN()); const double z = coordinates.at(2).toDouble(std::numeric_limits::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& 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& dimensions, QString& error) { dimensions = { args.value(QStringLiteral("length")).toDouble(std::numeric_limits::quiet_NaN()), args.value(QStringLiteral("width")).toDouble(std::numeric_limits::quiet_NaN()), args.value(QStringLiteral("height")).toDouble(std::numeric_limits::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 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::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 children; std::vector values; double maximum = -std::numeric_limits::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::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(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::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 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::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 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 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 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 owners; }; std::vector 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 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 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 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::quiet_NaN() : 0.0); if (!std::isfinite(tolerance) || tolerance < 0.0) { error = QStringLiteral("XOR tolerance must be finite and nonnegative"); return false; } std::vector 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 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::quiet_NaN() : 0.0); if (!std::isfinite(tolerance) || tolerance < 0.0) { error = QStringLiteral("tolerance must be a finite nonnegative number"); return false; } std::vector 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 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::quiet_NaN()); const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::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::quiet_NaN())); const double radius2 = args.value(QStringLiteral("radius2")).toDouble(0.0); const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::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::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::quiet_NaN())); const double minorRadius = args.value(QStringLiteral("radius2")).toDouble(args.value(QStringLiteral("minorRadius")).toDouble(std::numeric_limits::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::quiet_NaN()); const double height = args.value(QStringLiteral("height")).toDouble(std::numeric_limits::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 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 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(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(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(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::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::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 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(faces.size())) { error = QStringLiteral("faceIndices contains an invalid face index"); return false; } removedFaces.Append(faces[static_cast(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(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(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::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::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::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