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
1809 lines
98 KiB
C++
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
|