Save FreeCAD WebAssembly development state
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

This commit is contained in:
2026-09-07 18:58:02 +08:00
parent 2778e3f9c2
commit f2eaf096d8
113 changed files with 30708 additions and 119 deletions

View File

@@ -0,0 +1,987 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "PartDesktopCommands.h"
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Section.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}
constexpr CommandSpec commandSpecs[] = {
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
)
{
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 (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