Files
OrcaSlicer/src/libslic3r/CadDocument.cpp
T
Tommaso BianchiandClaude Opus 4.8 f8175fc9a4 Design: fix hole placement (top-face default) + invisible internal thread
Hole: with no face explicitly picked, the tool fell back to the XY datum at
z=0 (the model's underside), so placing a hole from a top view read parallax-
shifted. Default to the solid's top face (top_face_index_of) so the footprint
sits on the surface being viewed; the XY/XZ/YZ dropdown still overrides.

Internal thread: the bore was re-cut at the nominal radius, which coincides
with an existing hole's wall — the coincident faces fouled the groove boolean
so it removed ~nothing (invisible thread). Cut the bore at the minor diameter
(radius - depth) instead: strictly inside any existing wall, leaving it clean
for the groove; on solid stock it forms the tap-drill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
2026-07-02 20:18:41 +02:00

1839 lines
78 KiB
C++

#include "CadDocument.hpp"
#include "SketchConstraints.hpp"
#include "SketchSolver.hpp"
#include "SketchImport.hpp" // transform_regions for imported art
#include <array>
#include <Standard_Failure.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <BRepAlgoAPI_Common.hxx>
#include <BRepAlgoAPI_BooleanOperation.hxx>
#include <BRepOffsetAPI_MakePipe.hxx>
#include <BRepOffsetAPI_MakePipeShell.hxx>
#include <BRepOffsetAPI_MakeThickSolid.hxx>
#include <BRepOffsetAPI_DraftAngle.hxx>
#include <Bnd_Box.hxx>
#include <BRepBndLib.hxx>
#include <gp_Pln.hxx>
#include <TopTools_ListOfShape.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepLib.hxx>
#include <Geom_CylindricalSurface.hxx>
#include <Geom2d_TrimmedCurve.hxx>
#include <GCE2d_MakeSegment.hxx>
#include <TopoDS_Edge.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Compound.hxx> // multi-body: compound of bodies for display/compat
#include <BRep_Builder.hxx>
#include <TopAbs_Orientation.hxx> // outward-normal orientation for face-extrude
#include <gp_Circ.hxx>
#include <gp_Ax2.hxx>
#include <gp_Ax3.hxx>
#include <gp_Pnt2d.hxx>
#include <gp_Vec.hxx>
#include <gp_Trsf.hxx> // pattern: rigid copy transforms
#include <STEPControl_Writer.hxx> // STEP export (native B-rep)
#include <STEPControl_StepModelType.hxx>
#include <IFSelect_ReturnStatus.hxx>
#include <gp_Ax1.hxx> // pattern: rotation axis (circular)
#include <BRepBuilderAPI_Transform.hxx>
#include <cmath>
#include <stdexcept>
#include <algorithm>
#include <sstream>
#include <cereal/archives/binary.hpp>
#include <BRepTools.hxx>
namespace Slic3r {
// ---- helical-thread construction helpers (file-local) ----------------------
// Helix spine on a cylinder (radius/pitch/height) about `axis`, as a wire.
static TopoDS_Wire make_helix_wire(const gp_Ax3& axis, double radius,
double pitch, double height)
{
Handle(Geom_CylindricalSurface) cyl = new Geom_CylindricalSurface(axis, radius);
double turns = (pitch > 1e-6) ? (height / pitch) : 1.0;
// In the surface (u,v) parametrization u is the angle, v the axial height.
gp_Pnt2d p0(0.0, 0.0);
gp_Pnt2d p1(2.0 * M_PI * turns, height);
Handle(Geom2d_TrimmedCurve) seg = GCE2d_MakeSegment(p0, p1);
TopoDS_Edge e = BRepBuilderAPI_MakeEdge(seg, cyl).Edge();
BRepLib::BuildCurves3d(e);
return BRepBuilderAPI_MakeWire(e).Wire();
}
// Triangular axial thread profile (a planar face) placed at the helix start
// (origin + radius*xdir). Spans +-pitch/2 axially; apex offset radially by depth.
// Both thread kinds sweep the SAME outward-biting V (base on the cylinder wall,
// apex `depth` into the surrounding material). Only the boolean differs:
// - external: the V is FUSED to the rod -> a raised helical ridge.
// - internal: the V is CUT from the wall -> a sunken helical groove. The cut MUST
// go outward into the wall to be visible; an inward V (the old behaviour) only
// sweeps already-empty bore space and removes nothing.
static TopoDS_Wire make_thread_profile(const gp_Pnt& origin, const gp_Dir& xdir,
const gp_Dir& zdir, double radius,
double pitch, double depth, bool internal)
{
(void)internal;
gp_Vec vx(xdir), vz(zdir);
// Root the V CLEARLY inside the wall (a real overlap, not a 0.05 mm tangency) so the boolean
// has clean intersections — near-coincident faces are what make OCCT's fuse/cut unstable.
const double over = std::min(std::max(depth, 0.25), radius * 0.4);
double inner = radius - over; // base, well inside the wall (solid overlap)
double crest = radius + depth; // apex, `depth` into the surrounding material
// Axial half-height must be < pitch/2 so ADJACENT helix turns don't collide — a full-pitch
// profile makes the swept solid self-intersect (invalid -> never renders, or crashes the
// boolean). 0.42*pitch leaves a clean gap between turns; the V still reads as a thread.
const double half = 0.42 * pitch;
gp_Pnt top (origin.XYZ() + (vx * inner).XYZ() + (vz * ( half)).XYZ());
gp_Pnt bot (origin.XYZ() + (vx * inner).XYZ() + (vz * (-half)).XYZ());
gp_Pnt apex(origin.XYZ() + (vx * crest).XYZ());
BRepBuilderAPI_MakePolygon poly(top, bot, apex, Standard_True);
return poly.Wire(); // closed triangle, swept by MakePipeShell with a fixed binormal
}
// ---------------------------------------------------------------------------
int CadDocument::add_sketch(SketchShape shape, const SketchPlane& plane,
double width, double height, double radius,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Sketch;
f.name = name;
f.shape = shape;
f.plane = plane;
f.width = width;
f.height = height;
f.radius = radius;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Sketch;
f.name = name;
f.plane = plane;
f.profile = profile;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_sketch_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, const std::string& name,
const std::vector<SketchEntityConstraintDef>& constraints)
{
CadFeature f;
f.type = CadFeatureType::Sketch;
f.name = name;
f.plane = plane;
f.entities = entities;
f.entity_constraints = constraints; // driving dimensions, solved by solve_sketch_feature
features.push_back(f);
return int(features.size()) - 1;
}
// Solve Onshape-style constraints on a SketchEntity list (Fase 4.3). All entity
// types participate: Line (P0,P1), Arc (P0,P1,Center), Circle (Center), Point (P0).
// Solved coordinates are written back, with arc angles reflowed from the solved
// center+endpoints. Free function (declared in SketchEngine.hpp) so the in-session
// GUI sketch tool can live-solve the same way committed features do.
bool solve_sketch_entities(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints)
{
// Delegated to the vendored SolveSpace solver (SketchSolver / libslvs): full
// constraint set, real DoF + over-constrained detection.
return sketch_solve(entities, constraints).ok;
}
#if 0 // legacy hand-rolled Gauss-Newton solver — superseded by libslvs, kept for reference
static bool legacy_solve_sketch_entities(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints)
{
if (constraints.empty()) return true;
SketchConstraints sc;
// table[entity][role] -> solver point id, or -1 if that role is unregistered.
std::vector<std::array<int, 3>> table(entities.size(), {-1, -1, -1});
auto reg = [&](int ei, SketchPointRole role, const Vec2d& p) {
table[ei][int(role)] = sc.add_point(p.x(), p.y());
};
for (size_t i = 0; i < entities.size(); ++i) {
const SketchEntity& e = entities[i];
switch (e.type) {
case SketchEntity::Type::Line:
reg(int(i), SketchPointRole::P0, e.p0);
reg(int(i), SketchPointRole::P1, e.p1);
break;
case SketchEntity::Type::Arc:
reg(int(i), SketchPointRole::P0, e.p0);
reg(int(i), SketchPointRole::P1, e.p1);
reg(int(i), SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::Circle:
reg(int(i), SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::Point:
reg(int(i), SketchPointRole::P0, e.p0);
break;
}
}
auto pid = [&](int ei, SketchPointRole role) -> int {
if (ei < 0 || ei >= int(table.size())) return -1;
return table[ei][int(role)];
};
for (const SketchEntityConstraintDef& c : constraints) {
switch (c.type) {
// Point-form: refs A and B name individual entity points.
case SketchConstraintType::Fix: {
int a = pid(c.ea, c.ra);
if (a >= 0) sc.fix_point(a);
break;
}
case SketchConstraintType::Coincident: {
int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb);
if (a >= 0 && b >= 0) sc.coincident(a, b);
break;
}
case SketchConstraintType::Horizontal: {
int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb);
if (a >= 0 && b >= 0) sc.horizontal(a, b);
break;
}
case SketchConstraintType::Vertical: {
int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb);
if (a >= 0 && b >= 0) sc.vertical(a, b);
break;
}
case SketchConstraintType::Distance: {
int a = pid(c.ea, c.ra), b = pid(c.eb, c.rb);
if (a >= 0 && b >= 0) sc.distance(a, b, c.value);
break;
}
case SketchConstraintType::LockX: {
int a = pid(c.ea, c.ra);
if (a >= 0) sc.lock_x(a, c.value);
break;
}
case SketchConstraintType::LockY: {
int a = pid(c.ea, c.ra);
if (a >= 0) sc.lock_y(a, c.value);
break;
}
// Segment-form: ea and eb name whole line segments (their P0->P1).
case SketchConstraintType::Parallel:
case SketchConstraintType::Perpendicular:
case SketchConstraintType::EqualLength: {
int a0 = pid(c.ea, SketchPointRole::P0), a1 = pid(c.ea, SketchPointRole::P1);
int b0 = pid(c.eb, SketchPointRole::P0), b1 = pid(c.eb, SketchPointRole::P1);
if (a0 < 0 || a1 < 0 || b0 < 0 || b1 < 0) break;
if (c.type == SketchConstraintType::Parallel) sc.parallel(a0, a1, b0, b1);
else if (c.type == SketchConstraintType::Perpendicular) sc.perpendicular(a0, a1, b0, b1);
else sc.equal_length(a0, a1, b0, b1);
break;
}
case SketchConstraintType::Concentric: {
int a = pid(c.ea, SketchPointRole::Center);
int b = pid(c.eb, SketchPointRole::Center);
if (a >= 0 && b >= 0) sc.coincident(a, b);
break;
}
case SketchConstraintType::Midpoint: {
int m = pid(c.ea, c.ra);
int a = pid(c.eb, SketchPointRole::P0);
int b = pid(c.eb, SketchPointRole::P1);
if (m >= 0 && a >= 0 && b >= 0) sc.midpoint(m, a, b);
break;
}
case SketchConstraintType::Symmetric: {
int a = pid(c.ea, c.ra);
int b = pid(c.eb, c.rb);
int x0 = pid(c.ec, SketchPointRole::P0);
int x1 = pid(c.ec, SketchPointRole::P1);
if (a >= 0 && b >= 0 && x0 >= 0 && x1 >= 0) sc.symmetric(a, b, x0, x1);
break;
}
case SketchConstraintType::Angle: {
int a0 = pid(c.ea, SketchPointRole::P0), a1 = pid(c.ea, SketchPointRole::P1);
int b0 = pid(c.eb, SketchPointRole::P0), b1 = pid(c.eb, SketchPointRole::P1);
if (a0 >= 0 && a1 >= 0 && b0 >= 0 && b1 >= 0) sc.angle(a0, a1, b0, b1, c.value);
break;
}
case SketchConstraintType::Radius:
case SketchConstraintType::Diameter:
// dimensions: applied in the post-solve pass below, not via the solver.
break;
case SketchConstraintType::PointOnLine: {
// Point `ea`/`ra` is held at signed perpendicular distance `value` from
// line `eb` (value 0 -> on the line). Drives e.g. a circle centre onto a
// construction axis and keeps it there through later edits.
int p = pid(c.ea, c.ra);
int l0 = pid(c.eb, SketchPointRole::P0), l1 = pid(c.eb, SketchPointRole::P1);
if (p >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(p, l0, l1, c.value);
break;
}
case SketchConstraintType::Tangent: {
auto in_range = [&](int e){ return e >= 0 && e < (int)entities.size(); };
if (!in_range(c.ea) || !in_range(c.eb)) break;
const SketchEntity& ea_e = entities[c.ea];
const SketchEntity& eb_e = entities[c.eb];
auto is_round = [](const SketchEntity& e){
return e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc; };
if (is_round(ea_e) && eb_e.type == SketchEntity::Type::Line) {
int cen = pid(c.ea, SketchPointRole::Center);
int l0 = pid(c.eb, SketchPointRole::P0), l1 = pid(c.eb, SketchPointRole::P1);
if (cen >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(cen, l0, l1, ea_e.radius);
} else if (is_round(eb_e) && ea_e.type == SketchEntity::Type::Line) {
int cen = pid(c.eb, SketchPointRole::Center);
int l0 = pid(c.ea, SketchPointRole::P0), l1 = pid(c.ea, SketchPointRole::P1);
if (cen >= 0 && l0 >= 0 && l1 >= 0) sc.point_line_distance(cen, l0, l1, eb_e.radius);
} else if (is_round(ea_e) && is_round(eb_e)) {
int c0 = pid(c.ea, SketchPointRole::Center), c1 = pid(c.eb, SketchPointRole::Center);
if (c0 >= 0 && c1 >= 0) sc.distance(c0, c1, ea_e.radius + eb_e.radius);
}
break;
}
}
}
const bool ok = sc.solve();
// Write solved coordinates back into the participating entities.
for (size_t i = 0; i < entities.size(); ++i) {
SketchEntity& e = entities[i];
int ip0 = table[i][int(SketchPointRole::P0)];
int ip1 = table[i][int(SketchPointRole::P1)];
int ic = table[i][int(SketchPointRole::Center)];
if (ip0 >= 0) e.p0 = sc.get_point(ip0);
if (ip1 >= 0) e.p1 = sc.get_point(ip1);
if (ic >= 0) e.center = sc.get_point(ic);
if (e.type == SketchEntity::Type::Arc && ic >= 0) {
// Reflow arc angles from solved center + endpoints, preserving the
// original sweep direction (CCW vs CW).
const double old_sweep = e.end_angle - e.start_angle; // signed, original
double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x());
double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x());
double sweep = ne - ns;
// Normalize `sweep` into (-2pi, 2pi) then match the sign of old_sweep so
// the arc keeps turning the same way it did before solving.
const double TWO_PI = 2.0 * M_PI;
while (sweep <= -TWO_PI) sweep += TWO_PI;
while (sweep >= TWO_PI) sweep -= TWO_PI;
if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI;
if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI;
e.start_angle = ns;
e.end_angle = ns + sweep;
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
}
if (e.type == SketchEntity::Type::Circle && ic >= 0) {
// p0 mirrors the center for circles; keep them consistent.
e.p0 = e.center;
}
}
// Apply radius/diameter dimensions directly (radius is not a solver variable).
for (const auto& c : constraints) {
if (c.type != SketchConstraintType::Radius &&
c.type != SketchConstraintType::Diameter) continue;
if (c.ea < 0 || c.ea >= (int)entities.size()) continue;
SketchEntity& e = entities[c.ea];
if (e.type != SketchEntity::Type::Circle && e.type != SketchEntity::Type::Arc) continue;
const double r = (c.type == SketchConstraintType::Diameter) ? 0.5 * c.value : c.value;
if (r <= 0.0) continue;
e.radius = r;
if (e.type == SketchEntity::Type::Arc) {
// Rescale endpoints to the new radius around the (solved) center, keeping
// each endpoint's direction so the reflowed start/end angles stay valid.
auto rescale = [&](Vec2d& p) {
Vec2d d = p - e.center;
const double n = d.norm();
if (n > 1e-12) p = e.center + (r / n) * d;
};
rescale(e.p0);
rescale(e.p1);
}
}
return ok;
}
#endif // legacy solver
bool CadDocument::solve_sketch_feature(int index)
{
if (index < 0 || index >= int(features.size())) return false;
CadFeature& f = features[index];
if (f.type != CadFeatureType::Sketch) return false;
// Onshape-style entity sketches solve against entity endpoints (Fase 4.2).
if (!f.entities.empty())
return solve_sketch_entities(f.entities, f.entity_constraints);
if (f.constraints.empty()) return true;
SketchConstraints sc;
for (const Vec2d& p : f.profile.points)
sc.add_point(p.x(), p.y());
for (const SketchConstraintDef& c : f.constraints) {
switch (c.type) {
case SketchConstraintType::Fix: sc.fix_point(c.a); break;
case SketchConstraintType::Coincident: sc.coincident(c.a, c.b); break;
case SketchConstraintType::Horizontal: sc.horizontal(c.a, c.b); break;
case SketchConstraintType::Vertical: sc.vertical(c.a, c.b); break;
case SketchConstraintType::Distance: sc.distance(c.a, c.b, c.value); break;
case SketchConstraintType::LockX: sc.lock_x(c.a, c.value); break;
case SketchConstraintType::LockY: sc.lock_y(c.a, c.value); break;
case SketchConstraintType::EqualLength: sc.equal_length(c.a, c.b, c.c, c.d); break;
case SketchConstraintType::Parallel: sc.parallel(c.a, c.b, c.c, c.d); break;
case SketchConstraintType::Perpendicular:sc.perpendicular(c.a, c.b, c.c, c.d); break;
}
}
const bool ok = sc.solve();
for (size_t i = 0; i < f.profile.points.size(); ++i)
f.profile.points[i] = sc.get_point(int(i));
return ok;
}
int CadDocument::add_extrude(int sketch_ref, double distance, bool symmetric,
BooleanMode mode, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Extrude;
f.name = name;
f.sketch_ref = sketch_ref;
f.distance = distance;
f.symmetric = symmetric;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_extrude_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double distance,
bool symmetric, BooleanMode mode, const std::string& name)
{
// Self-contained extrude of a single loop: the entity subset lives on the feature
// itself (sketch_ref = -1), so build_sketch_wire(f) uses f.entities directly. The
// source sketch stays a separate feature, so its other loops remain selectable.
CadFeature f;
f.type = CadFeatureType::Extrude;
f.name = name;
f.sketch_ref = -1;
f.entities = entities;
f.plane = plane;
f.distance = distance;
f.symmetric = symmetric;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_extrude_face(int src_face, double distance, bool symmetric,
BooleanMode mode, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Extrude;
f.name = name;
f.sketch_ref = -1;
f.extrude_src_face = src_face;
f.distance = distance;
f.symmetric = symmetric;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_fillet(double radius, FaceGroup faces, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Fillet;
f.name = name;
f.dressup_size = radius;
f.face_group = faces;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_fillet(double radius, int edge_id, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Fillet;
f.name = name;
f.dressup_size = radius;
f.dressup_edge = edge_id;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_chamfer(double distance, FaceGroup faces, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Chamfer;
f.name = name;
f.dressup_size = distance;
f.face_group = faces;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_chamfer(double distance, int edge_id, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Chamfer;
f.name = name;
f.dressup_size = distance;
f.dressup_edge = edge_id;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Hole;
f.name = name;
f.plane = plane;
f.hole_diameter = diameter;
f.hole_depth = depth;
f.hole_through = through;
f.hole_x = x;
f.hole_y = y;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_thread(double radius, double pitch, double height, double depth,
bool internal, double x, double y, const SketchPlane& plane,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Thread;
f.name = name;
f.plane = plane;
f.thread_radius = radius;
f.thread_pitch = pitch;
f.thread_height = height;
f.thread_depth = depth;
f.thread_internal = internal;
f.thread_x = x;
f.thread_y = y;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_revolve(int sketch_ref, double angle, int axis, bool flip,
BooleanMode mode, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Revolve;
f.name = name;
f.sketch_ref = sketch_ref;
f.revolve_angle = angle;
f.revolve_axis = axis;
f.flip = flip;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_revolve_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double angle, int axis,
bool flip, BooleanMode mode, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Revolve;
f.name = name;
f.sketch_ref = -1;
f.entities = entities;
f.plane = plane;
f.revolve_angle = angle;
f.revolve_axis = axis;
f.flip = flip;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Sweep;
f.name = name;
f.sketch_ref = profile_sketch_ref;
f.sweep_path_ref = path_sketch_ref;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_loft(const std::vector<int>& profile_refs, bool ruled, BooleanMode mode,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Loft;
f.name = name;
f.loft_profile_refs = profile_refs;
f.loft_ruled = ruled;
f.mode = mode;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_pattern(bool circular, int count, double spacing, int dir,
double angle_deg, int target_body, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Pattern;
f.name = name;
f.pattern_circular = circular;
f.pattern_count = count;
f.pattern_spacing = spacing;
f.pattern_dir = dir;
f.pattern_angle = angle_deg;
f.target_body = target_body;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_shell(double thickness, int face, int target_body, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Shell;
f.name = name;
f.shell_thickness = thickness;
f.shell_face = face;
f.target_body = target_body;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_draft(double angle, int face, int target_body, const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Draft;
f.name = name;
f.draft_angle = angle;
f.draft_face = face;
f.target_body = target_body;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool,
double tolerance, int target_face, int tool_face,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Boolean;
f.name = name;
f.mode = op;
f.target_body = target_body;
f.bool_tool_body = tool_body;
f.bool_keep_tool = keep_tool;
f.bool_tolerance = tolerance;
f.bool_target_face = target_face;
f.bool_tool_face = tool_face;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_cut(const SketchPlane& plane, double offset, bool flip,
bool keep_upper, bool keep_lower, int target_body,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Cut;
f.name = name;
f.plane = plane;
f.cut_offset = offset;
f.cut_flip = flip;
f.cut_keep_upper = keep_upper;
f.cut_keep_lower = keep_lower;
f.target_body = target_body;
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_plane(int base, double offset, double angle_tilt, int axis,
const std::string& name)
{
CadFeature f;
f.type = CadFeatureType::Plane;
f.name = name;
f.plane_base = base;
f.plane_offset = offset;
f.plane_angle_tilt = angle_tilt;
f.plane_axis = axis;
features.push_back(f);
return int(features.size()) - 1;
}
// Derive a SketchPlane: shift `base` along its normal by `offset`, then tilt
// `angle_deg` about the base's X (axis 0) or Y (axis 1) axis (Rodrigues rotation).
static SketchPlane offset_angle_plane(const SketchPlane& base, double offset,
double angle_deg, int axis)
{
SketchPlane p;
p.origin = base.origin + base.normal * offset;
Vec3d n = base.normal, x = base.x_axis, y = base.y_axis;
if (std::abs(angle_deg) > 1e-9) {
const double a = angle_deg * M_PI / 180.0;
const Vec3d k = (axis == 1) ? base.y_axis : base.x_axis; // unit rotation axis
auto rot = [&](const Vec3d& v) -> Vec3d { // -> Vec3d forces eval (no dangling Eigen expr)
return v * std::cos(a) + k.cross(v) * std::sin(a)
+ k * (k.dot(v)) * (1.0 - std::cos(a));
};
n = rot(n);
if (axis == 1) x = rot(x); // tilt about Y rotates X + normal, Y fixed
else y = rot(y); // tilt about X rotates Y + normal, X fixed
}
p.normal = n.normalized();
p.x_axis = x.normalized();
p.y_axis = y.normalized();
return p;
}
// Build a full orthonormal frame from a normal + origin.
static SketchPlane frame_from(const Vec3d& origin, const Vec3d& normal)
{
Vec3d n = normal.normalized();
Vec3d ref = (std::abs(n.z()) < 0.9) ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
Vec3d x = ref.cross(n);
if (x.squaredNorm() < 1e-12) x = Vec3d(1, 0, 0);
x.normalize();
Vec3d y = n.cross(x).normalized();
SketchPlane p;
p.origin = origin;
p.normal = n;
p.x_axis = x;
p.y_axis = y;
return p;
}
std::vector<std::pair<std::string, SketchPlane>> CadDocument::resolve_datum_planes() const
{
std::vector<std::pair<std::string, SketchPlane>> out;
for (const CadFeature& f : features) {
if (f.type != CadFeatureType::Plane || !f.enabled) continue;
// Resolve base reference plane. The default XY/XZ/YZ planes pass through the modeling
// origin (bed centre); datum bases (>=3) are already in world coords from earlier passes.
SketchPlane base;
if (f.plane_base == 1) { base = SketchPlane::XZ(); base.origin += modeling_origin; }
else if (f.plane_base == 2) { base = SketchPlane::YZ(); base.origin += modeling_origin; }
else if (f.plane_base >= 3) {
const int di = f.plane_base - 3;
if (di < int(out.size())) base = out[di].second;
}
else { base = SketchPlane::XY(); base.origin += modeling_origin; }
// --- Resolve refs from bodies ---
auto resolve_face = [&](int body_idx, int face_idx) -> TopoDS_Face {
if (face_idx < 0 || body_idx < 0 || body_idx >= int(bodies.size()))
return TopoDS_Face();
return GeometryEngine::face_by_index(bodies[body_idx].shape, face_idx);
};
auto resolve_edge = [&](int body_idx, int edge_idx,
Vec3d& p0, Vec3d& dir) -> bool {
if (edge_idx < 0 || body_idx < 0 || body_idx >= int(bodies.size()))
return false;
TopoDS_Edge e = GeometryEngine::edge_by_index(bodies[body_idx].shape, edge_idx);
if (e.IsNull()) return false;
auto pts = GeometryEngine::sample_edge_world(e);
if (pts.size() < 2) return false;
p0 = pts.front();
dir = (pts.back() - pts.front()).normalized();
return true;
};
// Face A
TopoDS_Face faceA = resolve_face(f.plane_face_body, f.plane_face);
SketchPlane faceA_plane;
bool has_faceA = false;
if (!faceA.IsNull()) {
faceA_plane = frame_from(
GeometryEngine::face_centroid_world(faceA),
GeometryEngine::face_normal_world(faceA));
has_faceA = true;
}
// Face B
TopoDS_Face faceB = resolve_face(f.plane_face2_body, f.plane_face2);
SketchPlane faceB_plane;
bool has_faceB = false;
if (!faceB.IsNull()) {
faceB_plane = frame_from(
GeometryEngine::face_centroid_world(faceB),
GeometryEngine::face_normal_world(faceB));
has_faceB = true;
}
// Edge A
Vec3d eA_p0, eA_dir;
bool has_edgeA = resolve_edge(f.plane_edge_body, f.plane_edge, eA_p0, eA_dir);
// Edge B
Vec3d eB_p0, eB_dir;
bool has_edgeB = resolve_edge(f.plane_edge2_body, f.plane_edge2, eB_p0, eB_dir);
// --- Dispatch on plane_type ---
auto fallback_offset = [&]() {
return offset_angle_plane(base, f.plane_offset, f.plane_angle_tilt, f.plane_axis);
};
SketchPlane result;
switch (f.plane_type) {
case PlaneType::Offset: {
// From a picked face: pure offset along its normal. From a base/datum plane:
// offset + the legacy tilt-about-axis (keeps the old Offset/Tilt controls live).
if (has_faceA)
result = frame_from(faceA_plane.origin + faceA_plane.normal * f.plane_offset,
faceA_plane.normal);
else
result = offset_angle_plane(base, f.plane_offset, f.plane_angle_tilt, f.plane_axis);
break;
}
case PlaneType::Coincident: {
result = has_faceA ? faceA_plane : base;
break;
}
case PlaneType::Angle: {
if (!has_edgeA) { result = fallback_offset(); break; }
const SketchPlane& ref = has_faceA ? faceA_plane : base;
Vec3d n0 = ref.normal - eA_dir * ref.normal.dot(eA_dir);
if (n0.squaredNorm() < 1e-12) {
Vec3d perp = (std::abs(eA_dir.z()) < 0.9) ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
n0 = perp - eA_dir * perp.dot(eA_dir);
}
n0.normalize();
const double a = f.plane_angle_tilt * M_PI / 180.0;
Vec3d n_rot = n0 * std::cos(a) + eA_dir.cross(n0) * std::sin(a)
+ eA_dir * (eA_dir.dot(n0)) * (1.0 - std::cos(a));
result = frame_from(eA_p0, n_rot);
break;
}
case PlaneType::Midplane: {
if (!has_faceA || !has_faceB) { result = fallback_offset(); break; }
Vec3d origin = 0.5 * (faceA_plane.origin + faceB_plane.origin);
Vec3d nB = (faceA_plane.normal.dot(faceB_plane.normal) >= 0)
? faceB_plane.normal : -faceB_plane.normal;
Vec3d normal = (faceA_plane.normal + nB).normalized();
result = frame_from(origin, normal);
break;
}
case PlaneType::Tangent: {
if (!has_faceA) { result = fallback_offset(); break; }
GeometryEngine::CylinderFace cyl = GeometryEngine::cylinder_of_face(faceA);
if (!cyl.ok) { result = fallback_offset(); break; }
Vec3d refdir = (std::abs(cyl.axis.z()) < 0.9) ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
refdir = refdir - cyl.axis * refdir.dot(cyl.axis);
refdir.normalize();
const double theta = f.plane_angle_tilt * M_PI / 180.0;
Vec3d r = refdir * std::cos(theta) + cyl.axis.cross(refdir) * std::sin(theta);
Vec3d touch = cyl.base + r * cyl.radius;
result = frame_from(touch, r);
break;
}
case PlaneType::TwoEdges: {
if (!has_edgeA) { result = fallback_offset(); break; }
if (!has_edgeB) { result = fallback_offset(); break; }
Vec3d cross = eA_dir.cross(eB_dir);
if (cross.squaredNorm() > 1e-12) {
result = frame_from(eA_p0, cross.normalized());
} else {
Vec3d v = eA_dir.cross(eB_p0 - eA_p0);
if (v.squaredNorm() > 1e-12) {
result = frame_from(eA_p0, v.normalized());
} else {
result = fallback_offset();
}
}
break;
}
}
out.emplace_back(f.name, result);
}
return out;
}
void CadDocument::clear()
{
features.clear();
body = TopoDS_Shape();
bodies.clear(); // multibody result — must clear too (else solids linger)
display_mesh = TriangleMesh{};
display_body_meshes.clear();
display_tri_face.clear();
error.clear();
// A cleared document is a fresh start with no history.
m_undo.clear();
m_redo.clear();
}
void CadDocument::checkpoint()
{
m_undo.push_back(features); // snapshot the pre-mutation recipe
m_redo.clear(); // any new action invalidates the redo branch
if (m_undo.size() > k_undo_cap)
m_undo.erase(m_undo.begin());
}
bool CadDocument::undo()
{
if (m_undo.empty())
return false;
m_redo.push_back(std::move(features)); // current state becomes redoable
features = std::move(m_undo.back());
m_undo.pop_back();
recompute(); // benign-empty (only a sketch / empty doc) is a valid undo target
return true;
}
bool CadDocument::redo()
{
if (m_redo.empty())
return false;
m_undo.push_back(std::move(features));
features = std::move(m_redo.back());
m_redo.pop_back();
recompute();
return true;
}
// Re-run recompute(); if it fails for a GENUINE geometry error, restore `snapshot`
// and recompute that instead, so a rejected edit leaves the document exactly as it
// was. recompute() also returns false for the BENIGN case where the edit simply
// leaves no solid-producing feature (empty document, or only a sketch) — that is a
// valid result of a deletion, not a failure, so we accept it with an empty body.
static bool commit_or_rollback(CadDocument& doc, std::vector<CadFeature>& snapshot)
{
if (doc.recompute())
return true;
bool has_solid_feature = false;
for (const auto& f : doc.features)
if (f.enabled && f.type != CadFeatureType::Sketch) { has_solid_feature = true; break; }
if (!has_solid_feature) {
doc.bodies.clear();
doc.body = TopoDS_Shape();
doc.display_mesh = TriangleMesh{};
doc.display_body_meshes.clear();
doc.display_tri_face.clear();
doc.display_tri_body.clear();
doc.error.clear();
return true;
}
std::string fail_err = doc.error; // why the attempted edit failed
doc.features.swap(snapshot);
doc.recompute(); // restore the previous good body (clears error)
doc.error = fail_err.empty() ? std::string("feature is used by a later feature")
: fail_err;
return false;
}
bool CadDocument::remove_feature(int index)
{
if (index < 0 || index >= int(features.size()))
return false;
std::vector<CadFeature> snapshot = features;
// Deleting a Sketch cascades to every Extrude that consumes it (a dangling
// Extrude would have no wire). A lone Sketch, by contrast, is harmless.
std::vector<int> remove{index};
if (features[index].type == CadFeatureType::Sketch) {
for (int j = 0; j < int(features.size()); ++j)
if (features[j].type == CadFeatureType::Extrude && features[j].sketch_ref == index)
remove.push_back(j);
}
std::sort(remove.begin(), remove.end());
remove.erase(std::unique(remove.begin(), remove.end()), remove.end());
// Erase high-to-low so earlier indices stay valid.
for (auto it = remove.rbegin(); it != remove.rend(); ++it)
features.erase(features.begin() + *it);
// Remap surviving sketch_ref through the deletions: subtract the count of
// removed indices that sat before it; orphaned refs (target removed) -> -1.
for (auto& f : features) {
if (f.type != CadFeatureType::Extrude || f.sketch_ref < 0)
continue;
if (std::binary_search(remove.begin(), remove.end(), f.sketch_ref)) {
f.sketch_ref = -1;
} else {
int shift = 0;
for (int r : remove)
if (r < f.sketch_ref) ++shift;
f.sketch_ref -= shift;
}
}
return commit_or_rollback(*this, snapshot);
}
bool CadDocument::move_feature(int index, int delta)
{
if (index < 0 || index >= int(features.size()))
return false;
int target = index + delta;
if (target < 0 || target >= int(features.size()))
return true; // clamped at the ends — no-op, not a failure
std::vector<CadFeature> snapshot = features;
std::swap(features[index], features[target]);
// The two slots traded places: fix any sketch_ref that pointed at either.
for (auto& f : features) {
if (f.type != CadFeatureType::Extrude) continue;
if (f.sketch_ref == index) f.sketch_ref = target;
else if (f.sketch_ref == target) f.sketch_ref = index;
}
return commit_or_rollback(*this, snapshot);
}
bool CadDocument::replace_feature(int index, const CadFeature& edited)
{
if (index < 0 || index >= int(features.size()))
return false;
std::vector<CadFeature> snapshot = features;
// Preserve identity (name) and the structural link (sketch_ref) from the
// original; only the user-editable parameters come from `edited`.
CadFeature f = edited;
f.name = features[index].name;
f.type = features[index].type;
if (f.type == CadFeatureType::Extrude)
f.sketch_ref = features[index].sketch_ref;
features[index] = f;
return commit_or_rollback(*this, snapshot);
}
bool CadDocument::replace_sketch_extrude(int sketch_idx, int extrude_idx,
const CadFeature& edited)
{
if (sketch_idx < 0 || sketch_idx >= int(features.size())) return false;
if (extrude_idx < 0 || extrude_idx >= int(features.size())) return false;
std::vector<CadFeature> snapshot = features;
// A box in the tree is two linked features: the Sketch consumes the profile
// params (shape/plane/width/height/radius), the Extrude consumes the solid
// params (distance/symmetric/mode). `edited` carries all of them; split it
// back into the two slots, preserving each slot's name/type and the link.
CadFeature& sk = features[sketch_idx];
sk.shape = edited.shape;
sk.plane = edited.plane;
sk.width = edited.width;
sk.height = edited.height;
sk.radius = edited.radius;
CadFeature& ex = features[extrude_idx];
ex.distance = edited.distance;
ex.symmetric = edited.symmetric;
ex.mode = edited.mode;
return commit_or_rollback(*this, snapshot);
}
TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch) const
{
if (!sketch.entities.empty()) {
TopoDS_Wire w = SketchEngine::entities_to_wire(sketch.entities, sketch.plane);
if (!w.IsNull()) return w;
// fall through to legacy paths if entities produced nothing
}
if (!sketch.profile.points.empty()) {
SketchProfile prof = sketch.profile;
prof.closed = true; // extrude needs a closed wire
TopoDS_Wire w = prof.to_occt_wire(sketch.plane);
if (w.IsNull()) throw std::runtime_error("sketch profile wire failed");
return w;
}
if (sketch.shape == SketchShape::Circle) {
gp_Pnt o(sketch.plane.origin.x(), sketch.plane.origin.y(), sketch.plane.origin.z());
gp_Dir n(sketch.plane.normal.x(), sketch.plane.normal.y(), sketch.plane.normal.z());
gp_Circ circ(gp_Ax2(o, n), sketch.radius);
TopoDS_Edge e = BRepBuilderAPI_MakeEdge(circ).Edge();
BRepBuilderAPI_MakeWire wm(e);
if (!wm.IsDone()) throw std::runtime_error("circle wire failed");
return wm.Wire();
}
// Rectangle centered on the plane origin
SketchProfile prof;
double hw = sketch.width * 0.5, hh = sketch.height * 0.5;
prof.points.push_back(Vec2d(-hw, -hh));
prof.points.push_back(Vec2d( hw, -hh));
prof.points.push_back(Vec2d( hw, hh));
prof.points.push_back(Vec2d(-hw, hh));
prof.closed = true;
return prof.to_occt_wire(sketch.plane);
}
void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
const TopoDS_Shape& context, const CadFeature& f) const
{
switch (f.type) {
case CadFeatureType::Sketch:
return; // sketches carry no solid; consumed by an extrude
case CadFeatureType::Boolean:
return; // body-body boolean is handled in route_feature/apply_boolean, never here
case CadFeatureType::Import:
// Imported B-rep (STEP): rigid data carried on the feature, not built from
// parameters — adopt it as the new body (New-path: result starts empty).
result = f.imported_solid;
have_body = !result.IsNull();
return;
case CadFeatureType::Extrude: {
const bool sym = (f.extrude_end == ExtrudeEnd::Symmetric);
const double signed_d = f.flip ? -f.distance : f.distance;
TopoDS_Shape tool;
if (f.extrude_src_face >= 0) {
// The source face is read from `context` (the owner body), which for a New
// face-extrude is the source solid while `result` is the empty new body.
if (context.IsNull()) throw std::runtime_error("face-extrude needs a body");
TopoDS_Face srcf = GeometryEngine::face_by_index(context, f.extrude_src_face);
if (srcf.IsNull()) throw std::runtime_error("face-extrude: invalid face id");
SketchPlane fpl = SketchPlane::from_face(srcf);
// from_face takes the surface's geometric normal and IGNORES the topological
// face orientation, so for a REVERSED face (e.g. the top cap of an extruded
// prism) it points INTO the solid -> a default push would fuse to nothing.
// Orient it outward so push/pull grows away from the material (Onshape default);
// the Flip checkbox (signed_d) still lets the user drive it inward for a cut.
if (srcf.Orientation() == TopAbs_REVERSED) fpl.normal = -fpl.normal;
tool = SketchEngine::make_extrude_face(srcf, fpl, signed_d, sym);
} else {
// Use the referenced sketch when sketch_ref is a valid Sketch index,
// otherwise fall back to f's own inline sketch params (this makes a
// single self-contained candidate previewable).
const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size())
&& features[f.sketch_ref].type == CadFeatureType::Sketch)
? features[f.sketch_ref] : f;
// Imported rigid art (Text/SVG) extrudes via the faces-with-holes path
// (with its placement transform applied); otherwise build a single wire
// from entities/profile/shape.
tool = !sk.imported_regions.empty()
? SketchEngine::make_extrude_regions(
transform_regions(sk.imported_regions, sk.import_offset,
sk.import_scale_x, sk.import_scale_y),
sk.plane,
f.extrude_end == ExtrudeEnd::ThroughAll ? 1e5 : signed_d,
f.extrude_end == ExtrudeEnd::ThroughAll ? true : (sym || f.extrude_end == ExtrudeEnd::TwoSided))
: [&]() {
TopoDS_Wire wire = build_sketch_wire(sk);
TopoDS_Shape t;
switch (f.extrude_end) {
case ExtrudeEnd::Blind:
t = (std::abs(f.taper_deg) > 1e-6)
? SketchEngine::make_extrude_taper(wire, sk.plane, signed_d, f.taper_deg)
: SketchEngine::make_extrude(wire, sk.plane, signed_d, false);
break;
case ExtrudeEnd::Symmetric: t = SketchEngine::make_extrude(wire, sk.plane, f.distance, true); break;
case ExtrudeEnd::TwoSided: t = SketchEngine::make_extrude_two_sided(wire, sk.plane, f.distance, f.distance2); break;
case ExtrudeEnd::ThroughAll: t = SketchEngine::make_extrude(wire, sk.plane, 1.0e5, true); break;
case ExtrudeEnd::UpToFace: {
const TopoDS_Face tgt = GeometryEngine::face_by_index(context, f.up_to_face);
double L = signed_d;
if (!tgt.IsNull()) {
const Vec3d c = GeometryEngine::face_centroid_world(tgt);
L = (c - sk.plane.origin).dot(sk.plane.normal);
}
t = (std::abs(f.taper_deg) > 1e-6)
? SketchEngine::make_extrude_taper(wire, sk.plane, L, f.taper_deg)
: SketchEngine::make_extrude(wire, sk.plane, L, false);
break;
}
case ExtrudeEnd::UpToVertex: {
const double L = (f.up_to_point - sk.plane.origin).dot(sk.plane.normal);
t = SketchEngine::make_extrude(wire, sk.plane, L, false);
break;
}
default: t = SketchEngine::make_extrude(wire, sk.plane, signed_d, false); break;
}
return t;
}();
}
// New / first-of-a-body => result becomes the tool (route_feature sends New extrudes
// here with an empty result, so a face-extrude New builds a fresh body from the source
// face in `context` without touching it). Add/Cut/Intersect boolean into `result`.
if (!have_body || f.mode == BooleanMode::New) {
result = tool;
have_body = true;
} else if (f.mode == BooleanMode::Add) {
BRepAlgoAPI_Fuse fuse(result, tool);
if (!fuse.IsDone()) throw std::runtime_error("fuse failed");
result = fuse.Shape();
} else if (f.mode == BooleanMode::Cut) {
BRepAlgoAPI_Cut cut(result, tool);
if (!cut.IsDone()) throw std::runtime_error("cut failed");
result = cut.Shape();
} else if (f.mode == BooleanMode::Intersect) {
BRepAlgoAPI_Common common(result, tool);
if (!common.IsDone()) throw std::runtime_error("intersect failed");
result = common.Shape();
}
break;
}
case CadFeatureType::Revolve: {
// Resolve the profile sketch like Extrude: referenced Sketch when valid,
// else this feature's own inline entities/profile (self-contained candidate).
const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size())
&& features[f.sketch_ref].type == CadFeatureType::Sketch)
? features[f.sketch_ref] : f;
TopoDS_Wire wire = build_sketch_wire(sk);
const double ang = f.flip ? -f.revolve_angle : f.revolve_angle;
TopoDS_Shape tool = SketchEngine::make_revolve(wire, sk.plane, ang, f.revolve_axis);
if (!have_body || f.mode == BooleanMode::New) {
result = tool;
have_body = true;
} else if (f.mode == BooleanMode::Add) {
BRepAlgoAPI_Fuse fuse(result, tool);
if (!fuse.IsDone()) throw std::runtime_error("fuse failed");
result = fuse.Shape();
} else if (f.mode == BooleanMode::Cut) {
BRepAlgoAPI_Cut cut(result, tool);
if (!cut.IsDone()) throw std::runtime_error("cut failed");
result = cut.Shape();
} else if (f.mode == BooleanMode::Intersect) {
BRepAlgoAPI_Common common(result, tool);
if (!common.IsDone()) throw std::runtime_error("intersect failed");
result = common.Shape();
}
break;
}
case CadFeatureType::Sweep: {
// Resolve the profile sketch like Extrude/Revolve, and the path (spine) from
// the referenced path Sketch. Both build through build_sketch_wire (the path
// sketch is entity-based, so its wire keeps its open/closed shape as drawn).
const CadFeature& sk = (f.sketch_ref >= 0 && f.sketch_ref < int(features.size())
&& features[f.sketch_ref].type == CadFeatureType::Sketch)
? features[f.sketch_ref] : f;
if (f.sweep_path_ref < 0 || f.sweep_path_ref >= int(features.size())
|| features[f.sweep_path_ref].type != CadFeatureType::Sketch)
throw std::runtime_error("sweep needs a valid path sketch");
TopoDS_Wire profile = build_sketch_wire(sk);
TopoDS_Wire path = build_sketch_wire(features[f.sweep_path_ref]);
TopoDS_Shape tool = SketchEngine::make_sweep(profile, path);
if (!have_body || f.mode == BooleanMode::New) {
result = tool;
have_body = true;
} else if (f.mode == BooleanMode::Add) {
BRepAlgoAPI_Fuse fuse(result, tool);
if (!fuse.IsDone()) throw std::runtime_error("fuse failed");
result = fuse.Shape();
} else if (f.mode == BooleanMode::Cut) {
BRepAlgoAPI_Cut cut(result, tool);
if (!cut.IsDone()) throw std::runtime_error("cut failed");
result = cut.Shape();
} else if (f.mode == BooleanMode::Intersect) {
BRepAlgoAPI_Common common(result, tool);
if (!common.IsDone()) throw std::runtime_error("intersect failed");
result = common.Shape();
}
break;
}
case CadFeatureType::Loft: {
// Loft through 2+ closed profile Sketches, in recipe order. Each profile builds
// a wire via build_sketch_wire (so it keeps its own plane); make_loft skins them.
std::vector<TopoDS_Wire> profiles;
for (int ref : f.loft_profile_refs) {
if (ref < 0 || ref >= int(features.size())
|| features[ref].type != CadFeatureType::Sketch)
continue;
profiles.push_back(build_sketch_wire(features[ref]));
}
if (profiles.size() < 2)
throw std::runtime_error("loft needs 2+ valid profile sketches");
TopoDS_Shape tool = SketchEngine::make_loft(profiles, f.loft_ruled);
if (!have_body || f.mode == BooleanMode::New) {
result = tool;
have_body = true;
} else if (f.mode == BooleanMode::Add) {
BRepAlgoAPI_Fuse fuse(result, tool);
if (!fuse.IsDone()) throw std::runtime_error("fuse failed");
result = fuse.Shape();
} else if (f.mode == BooleanMode::Cut) {
BRepAlgoAPI_Cut cut(result, tool);
if (!cut.IsDone()) throw std::runtime_error("cut failed");
result = cut.Shape();
} else if (f.mode == BooleanMode::Intersect) {
BRepAlgoAPI_Common common(result, tool);
if (!common.IsDone()) throw std::runtime_error("intersect failed");
result = common.Shape();
}
break;
}
case CadFeatureType::Pattern: {
// Replicate the target body. Each copy is a rigid gp_Trsf of the seed, all
// fused into one body. Linear: i*spacing along plane axis pattern_dir
// (0=X,1=Y). Circular: i*(angle/count) about the plane normal through the
// plane origin (so a seed offset from the origin orbits the axis).
if (!have_body) throw std::runtime_error("pattern needs a body");
const int n = std::max(1, f.pattern_count);
const TopoDS_Shape seed = result;
for (int i = 1; i < n; ++i) {
gp_Trsf trsf;
if (f.pattern_circular) {
Vec3d o = f.plane.to_world(Vec2d(0, 0));
gp_Ax1 ax(gp_Pnt(o.x(), o.y(), o.z()),
gp_Dir(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z()));
const double step = (f.pattern_angle * M_PI / 180.0) / double(n);
trsf.SetRotation(ax, step * i);
} else {
const Vec3d& d = (f.pattern_dir == 1) ? f.plane.y_axis : f.plane.x_axis;
trsf.SetTranslation(gp_Vec(d.x() * f.pattern_spacing * i,
d.y() * f.pattern_spacing * i,
d.z() * f.pattern_spacing * i));
}
TopoDS_Shape copy = BRepBuilderAPI_Transform(seed, trsf, true).Shape();
BRepAlgoAPI_Fuse fuse(result, copy);
if (!fuse.IsDone()) throw std::runtime_error("pattern fuse failed");
result = fuse.Shape();
}
break;
}
case CadFeatureType::Fillet:
if (!have_body) throw std::runtime_error("fillet needs a body");
if (f.dressup_edge >= 0)
result = GeometryEngine::apply_fillet(result, f.dressup_size, f.dressup_edge);
else
result = GeometryEngine::apply_fillet(result, f.dressup_size, f.face_group);
break;
case CadFeatureType::Chamfer:
if (!have_body) throw std::runtime_error("chamfer needs a body");
if (f.dressup_edge >= 0)
result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.dressup_edge);
else
result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.face_group);
break;
case CadFeatureType::Hole: {
if (!have_body) throw std::runtime_error("hole needs a body");
// Circle wire centered at the positioned point on the plane
Vec3d c = f.plane.to_world(Vec2d(f.hole_x, f.hole_y));
gp_Pnt o(c.x(), c.y(), c.z());
gp_Dir n(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z());
gp_Circ circ(gp_Ax2(o, n), f.hole_diameter * 0.5);
TopoDS_Edge e = BRepBuilderAPI_MakeEdge(circ).Edge();
BRepBuilderAPI_MakeWire wm(e);
if (!wm.IsDone()) throw std::runtime_error("hole wire failed");
// Through = symmetric huge cut (passes fully through any body);
// Blind = +normal extrude of hole_depth into the body.
TopoDS_Shape tool = f.hole_through
? SketchEngine::make_extrude(wm.Wire(), f.plane, 1.0e5, true, 0.0)
: SketchEngine::make_extrude(wm.Wire(), f.plane, f.hole_depth, false, 0.0);
BRepAlgoAPI_Cut cut(result, tool);
if (!cut.IsDone()) throw std::runtime_error("hole cut failed");
result = cut.Shape();
break;
}
case CadFeatureType::Thread: {
// Reject degenerate parameters that make OCCT's helical sweep / boolean unstable (a tiny
// pitch, depth >= half-pitch, an enormous turn count, depth eating the whole wall). Better
// a no-op than a crash. Leave the body unchanged when the spec can't be built safely.
{
const double R = f.thread_radius, P = f.thread_pitch, H = f.thread_height, D = f.thread_depth;
// ISO external thread depth is ~0.61*P, so allow up to 0.7*P (0.49 wrongly rejected
// every real thread -> nothing rendered). Still bound it well under a full pitch.
const bool ok = R > 0.5 && P > 0.1 && D > 1e-3 && D < 0.7 * P && D < 0.45 * R
&& H > 0.5 * P && (H / P) < 400.0;
if (!ok) break; // result/have_body untouched
}
// Axis at the positioned point on the plane; +normal = thread rise.
Vec3d c3 = f.plane.to_world(Vec2d(f.thread_x, f.thread_y));
gp_Pnt c(c3.x(), c3.y(), c3.z());
gp_Dir zdir(f.plane.normal.x(), f.plane.normal.y(), f.plane.normal.z());
gp_Dir xdir(f.plane.x_axis.x(), f.plane.x_axis.y(), f.plane.x_axis.z());
gp_Ax3 ax3(c, zdir, xdir);
gp_Ax2 ax2(c, zdir, xdir);
// Build the swept helical ridge (guarded — never fatal).
TopoDS_Shape ridge;
bool have_ridge = false;
try {
TopoDS_Wire spine = make_helix_wire(ax3, f.thread_radius,
f.thread_pitch, f.thread_height);
TopoDS_Wire prof = make_thread_profile(c, xdir, zdir, f.thread_radius,
f.thread_pitch, f.thread_depth,
f.thread_internal);
// MakePipeShell with a FIXED BINORMAL = cylinder axis keeps the V-profile's orientation
// constant along the helix (axial edge always parallel to the axis, V always pointing
// radially out). The plain MakePipe used a Frenet frame that TWISTED the profile around
// the helix -> the wedge inclination varied and looked mirrored.
BRepOffsetAPI_MakePipeShell pipe(spine);
pipe.SetMode(zdir);
pipe.Add(prof);
pipe.Build();
if (pipe.IsDone() && pipe.MakeSolid()) {
ridge = pipe.Shape();
have_ridge = !ridge.IsNull();
}
} catch (const std::exception&) {
have_ridge = false; // fall back to the bare cylinder/bore below
} catch (const Standard_Failure&) {
have_ridge = false; // OCCT failure (not a std::exception) — must be caught here too
}
if (f.thread_internal) {
if (!have_body) throw std::runtime_error("internal thread needs a body");
// Tapped bore: ensure a clean cylindrical pocket, then carve the
// OUTWARD helical groove into its wall. When the thread is invoked on
// an existing hole the bore cut is coincident (a no-op that may report
// !IsDone) — tolerate it so the visible groove cut below still runs.
// Cut the pocket at the MINOR diameter (radius - depth), not the nominal radius.
// A nominal-radius bore that coincides with an existing hole's wall creates
// coincident faces that foul the following groove boolean (the groove then removes
// ~nothing -> invisible thread). The minor bore stays strictly inside any existing
// hole wall, leaving it clean for the groove; on solid stock it forms the tap-drill.
const double bore_r = std::max(0.5, f.thread_radius - f.thread_depth);
TopoDS_Shape bore = BRepPrimAPI_MakeCylinder(ax2, bore_r,
f.thread_height).Shape();
try {
BRepAlgoAPI_Cut cut_bore(result, bore);
if (cut_bore.IsDone() && !cut_bore.Shape().IsNull())
result = cut_bore.Shape();
} catch (const std::exception&) { /* keep existing bore */ }
if (have_ridge) {
BRepAlgoAPI_Cut cut_ridge(result, ridge);
if (cut_ridge.IsDone() && !cut_ridge.Shape().IsNull())
result = cut_ridge.Shape();
}
} else {
// External thread: FUSE the helical ridge ONTO the existing body (the picked cylinder),
// leaving the rest of the part intact. Replacing the body with a bare rod — the old
// behaviour — wiped whatever the user picked; that was the "mess". With no body yet
// (a thread from scratch on a dropdown plane), fall back to a standalone threaded rod.
if (have_body && !result.IsNull()) {
if (have_ridge) {
BRepAlgoAPI_Fuse fuse(result, ridge);
if (fuse.IsDone() && !fuse.Shape().IsNull()) result = fuse.Shape();
}
} else {
TopoDS_Shape rod = BRepPrimAPI_MakeCylinder(ax2, f.thread_radius,
f.thread_height).Shape();
if (have_ridge) {
BRepAlgoAPI_Fuse fuse(rod, ridge);
if (fuse.IsDone()) rod = fuse.Shape();
}
result = rod;
have_body = true;
}
}
break;
}
case CadFeatureType::Shell: {
if (!have_body) throw std::runtime_error("shell needs a body");
// Hollow the body to a wall thickness; the picked face (if any) is removed so the
// shell is open there. MakeThickSolidByJoin with a NEGATIVE offset shells inward.
TopTools_ListOfShape remove;
if (f.shell_face >= 0) {
TopoDS_Face fc = GeometryEngine::face_by_index(result, f.shell_face);
if (!fc.IsNull()) remove.Append(fc);
}
BRepOffsetAPI_MakeThickSolid mts;
mts.MakeThickSolidByJoin(result, remove, -std::abs(f.shell_thickness), 1.0e-3);
mts.Build();
if (!mts.IsDone()) throw std::runtime_error("shell failed");
result = mts.Shape();
if (result.IsNull()) throw std::runtime_error("shell produced no geometry");
break;
}
case CadFeatureType::Draft: {
if (!have_body) throw std::runtime_error("draft needs a body");
if (f.draft_face < 0) throw std::runtime_error("draft needs a picked face");
TopoDS_Face fc = GeometryEngine::face_by_index(result, f.draft_face);
if (fc.IsNull()) throw std::runtime_error("draft: face not found");
// Neutral plane = horizontal plane through the body's bbox bottom, pull direction +Z.
// The face pivots about the line where it meets the neutral plane and tilts by the angle.
// ponytail: neutral plane / pull direction fixed to world up; pick-based neutral plane
// deferred (same as the datum-plane pick types, snaporca-dgv).
Bnd_Box bb; BRepBndLib::Add(result, bb);
Standard_Real xmin, ymin, zmin, xmax, ymax, zmax;
bb.Get(xmin, ymin, zmin, xmax, ymax, zmax);
gp_Dir pull(0, 0, 1);
gp_Pln neutral(gp_Pnt(0, 0, zmin), pull);
BRepOffsetAPI_DraftAngle draft(result);
draft.Add(fc, pull, f.draft_angle * M_PI / 180.0, neutral);
if (!draft.AddDone())
throw std::runtime_error("draft: face cannot be drafted (is it parallel to the base?)");
draft.Build();
if (!draft.IsDone()) throw std::runtime_error("draft failed");
result = draft.Shape();
if (result.IsNull()) throw std::runtime_error("draft produced no geometry");
break;
}
}
}
// Compound of all body shapes (1 body => that body verbatim, so single-body display and
// global face/edge ids are byte-identical to the pre-multi-body behaviour).
static TopoDS_Shape compound_of(const std::vector<CadBody>& bodies)
{
if (bodies.size() == 1) return bodies[0].shape;
TopoDS_Compound comp;
BRep_Builder bld;
bld.MakeCompound(comp);
for (const CadBody& b : bodies)
if (!b.shape.IsNull()) bld.Add(comp, b.shape);
return comp;
}
// Tessellate every body separately and concatenate into one mesh, recording per-triangle
// (body index, face id WITHIN that body). Single-body => byte-identical to tessellate(body).
static TriangleMesh tessellate_bodies(const std::vector<CadBody>& bodies,
std::vector<int>& tri_face, std::vector<int>& tri_body,
std::vector<TriangleMesh>& body_meshes,
double lin, double ang)
{
tri_face.clear();
tri_body.clear();
body_meshes.clear();
indexed_triangle_set merged;
for (int bi = 0; bi < int(bodies.size()); ++bi) {
std::vector<int> tf;
TriangleMesh bm = SketchEngine::tessellate(bodies[bi].shape, tf, lin, ang);
const indexed_triangle_set& its = bm.its;
const int voff = int(merged.vertices.size());
for (const auto& v : its.vertices) merged.vertices.push_back(v);
for (const auto& t : its.indices)
merged.indices.emplace_back(t[0] + voff, t[1] + voff, t[2] + voff);
for (int fid : tf) { tri_face.push_back(fid); tri_body.push_back(bi); }
body_meshes.push_back(std::move(bm)); // per-body mesh kept for distinct GLVolume colors
}
return TriangleMesh(merged);
}
void CadDocument::apply_boolean(std::vector<CadBody>& bodies, const CadFeature& f) const
{
const int nb = int(bodies.size());
const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1;
const int tool = (f.bool_tool_body >= 0 && f.bool_tool_body < nb) ? f.bool_tool_body : -1;
if (tgt < 0 || tool < 0 || tgt == tool) return; // need two distinct bodies; otherwise no-op
const TopoDS_Shape A = bodies[tgt].shape; // target survives
const TopoDS_Shape B = bodies[tool].shape; // tool, consumed unless kept
if (A.IsNull() || B.IsNull()) return;
TopTools_ListOfShape args, tools;
args.Append(A);
tools.Append(B);
auto run = [&](BRepAlgoAPI_BooleanOperation& bop) -> TopoDS_Shape {
bop.SetArguments(args);
bop.SetTools(tools);
if (f.bool_tolerance > 0.0) bop.SetFuzzyValue(f.bool_tolerance); // OCCT fuzzy: merge near-coincident faces
bop.Build();
if (!bop.IsDone()) throw std::runtime_error("boolean operation failed");
return bop.Shape();
};
TopoDS_Shape result;
switch (f.mode) {
case BooleanMode::Add: { BRepAlgoAPI_Fuse op; result = run(op); break; } // union
case BooleanMode::Cut: { BRepAlgoAPI_Cut op; result = run(op); break; } // target - tool
case BooleanMode::Intersect: { BRepAlgoAPI_Common op; result = run(op); break; } // overlap
default: return; // BooleanMode::New is meaningless between two existing bodies
}
if (result.IsNull()) throw std::runtime_error("boolean produced an empty shape");
bodies[tgt].shape = result;
if (!f.bool_keep_tool) bodies.erase(bodies.begin() + tool); // consume the tool body
}
void CadDocument::apply_cut(std::vector<CadBody>& bodies, const CadFeature& f) const
{
const int nb = int(bodies.size());
if (nb == 0) throw std::runtime_error("cut: no target body");
const int tgt = (f.target_body >= 0 && f.target_body < nb) ? f.target_body : nb - 1;
if (tgt < 0 || bodies[tgt].shape.IsNull()) throw std::runtime_error("cut: no target body");
if (!f.cut_keep_upper && !f.cut_keep_lower)
throw std::runtime_error("cut keeps nothing");
SketchPlane cp = f.plane;
cp.origin += cp.normal * f.cut_offset;
if (f.cut_flip) cp.normal = -cp.normal;
// Build a large square wire in the cut plane, centered at plane origin.
const double L = 1.0e5;
Vec3d x = cp.x_axis * L;
Vec3d y = cp.y_axis * L;
Vec3d o = cp.origin;
auto p = [&](double sx, double sy) {
Vec3d v = o + x * sx + y * sy;
return gp_Pnt(v.x(), v.y(), v.z());
};
BRepBuilderAPI_MakePolygon poly;
poly.Add(p( 1, 1));
poly.Add(p( 1, -1));
poly.Add(p(-1, -1));
poly.Add(p(-1, 1));
poly.Close();
if (!poly.IsDone()) throw std::runtime_error("cut: failed to build cut wire");
TopoDS_Wire wire = poly.Wire();
TopoDS_Shape upper_piece, lower_piece;
const TopoDS_Shape& target = bodies[tgt].shape;
if (f.cut_keep_upper) {
TopoDS_Shape upper_tool = SketchEngine::make_extrude(wire, cp, L, false, 0.0);
BRepAlgoAPI_Common common(target, upper_tool);
if (!common.IsDone()) throw std::runtime_error("cut operation failed");
upper_piece = common.Shape();
}
if (f.cut_keep_lower) {
SketchPlane lp = cp;
lp.normal = -lp.normal;
TopoDS_Shape lower_tool = SketchEngine::make_extrude(wire, lp, L, false, 0.0);
BRepAlgoAPI_Common common(target, lower_tool);
if (!common.IsDone()) throw std::runtime_error("cut operation failed");
lower_piece = common.Shape();
}
if (f.cut_keep_upper && f.cut_keep_lower) {
bodies[tgt].shape = upper_piece;
bodies.insert(bodies.begin() + tgt + 1,
CadBody{ lower_piece, bodies[tgt].name + " (2)" });
} else if (f.cut_keep_upper) {
bodies[tgt].shape = upper_piece;
} else {
bodies[tgt].shape = lower_piece;
}
}
void CadDocument::route_feature(std::vector<CadBody>& bodies, const CadFeature& f) const
{
if (f.type == CadFeatureType::Plane) return; // datum plane: not part of the body pipeline
if (f.type == CadFeatureType::Boolean) { apply_boolean(bodies, f); return; } // body-body op
if (f.type == CadFeatureType::Cut) { apply_cut(bodies, f); return; } // plane-split body
// Resolve the target body: explicit target_body when valid, else the last body.
const int t = (f.target_body >= 0 && f.target_body < int(bodies.size()))
? f.target_body : int(bodies.size()) - 1;
const TopoDS_Shape context = (t >= 0) ? bodies[t].shape : TopoDS_Shape();
// A New extrude (or the very first solid feature) starts a fresh body; everything else
// mutates the target body in place.
const bool starts_new = bodies.empty()
|| f.type == CadFeatureType::Import // an imported solid is always its own base body
|| ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve
|| f.type == CadFeatureType::Sweep || f.type == CadFeatureType::Loft)
&& f.mode == BooleanMode::New);
if (starts_new) {
TopoDS_Shape result; // empty -> apply_feature fills it (New path)
bool have_body = false;
apply_feature(result, have_body, context, f);
if (have_body && !result.IsNull())
bodies.push_back({ result, f.name.empty() ? std::string("Body") : f.name });
} else {
if (t < 0) throw std::runtime_error("feature needs a body");
TopoDS_Shape result = bodies[t].shape; // shallow handle; apply_feature mutates it
bool have_body = true;
apply_feature(result, have_body, context, f);
bodies[t].shape = result;
}
}
bool CadDocument::recompute()
{
error.clear();
std::vector<CadBody> built;
try {
for (const CadFeature& f : features) {
if (!f.enabled) continue;
if (f.type == CadFeatureType::Sketch) continue; // consumed by an extrude
if (f.type == CadFeatureType::Plane) continue; // datum: no solid, derived on demand
route_feature(built, f);
}
} catch (const Standard_Failure& e) {
// OCCT raises Standard_Failure (NOT a std::exception) — must be caught
// here or it escapes the event handler and terminates the app.
error = e.GetMessageString() ? e.GetMessageString() : "OCCT operation failed";
return false;
} catch (const std::exception& e) {
error = e.what();
return false;
} catch (...) {
error = "unknown geometry error";
return false;
}
if (built.empty()) { error = "no solid-producing features"; return false; }
// recompute() replaces the bodies vector wholesale, which would drop any per-body
// colour override (Color tool). Body indices are stable across a rebuild (bodies are
// appended in feature order), so carry the override forward by index — same indexing
// contract the GUI relies on for per-body visibility/Move.
for (size_t i = 0; i < built.size() && i < bodies.size(); ++i) {
if (bodies[i].has_color) {
built[i].has_color = true;
built[i].color = bodies[i].color;
}
}
bodies = std::move(built);
body = compound_of(bodies);
display_mesh = tessellate_bodies(bodies, display_tri_face, display_tri_body,
display_body_meshes,
linear_deflection, angular_deflection);
if (display_mesh.its.indices.empty()) {
error = "tessellation produced an empty mesh";
return false;
}
return true;
}
bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh,
std::vector<TriangleMesh>& out_body_meshes, std::string& err) const
{
err.clear();
out_body_meshes.clear();
std::vector<CadBody> tmp = bodies; // start from the current committed bodies
try {
route_feature(tmp, candidate); // candidate may append a new body or mutate one
} catch (const Standard_Failure& e) {
err = e.GetMessageString() ? e.GetMessageString() : "OCCT operation failed";
return false;
} catch (const std::exception& e) {
err = e.what();
return false;
} catch (...) {
err = "unknown geometry error";
return false;
}
if (tmp.empty()) {
err = "preview produced no geometry";
return false;
}
// Tessellate per body (same path as recompute) so the GUI can re-apply its display-only
// per-body Move transforms to the ghost; out_mesh is the merged whole.
std::vector<int> tf, tb;
out_mesh = tessellate_bodies(tmp, tf, tb, out_body_meshes, linear_deflection, angular_deflection);
if (out_mesh.its.indices.empty()) {
err = "preview produced an empty mesh";
return false;
}
return true;
}
bool CadDocument::preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const
{
std::vector<TriangleMesh> ignore;
return preview(candidate, out_mesh, ignore, err);
}
std::string brep_to_string(const TopoDS_Shape& s)
{
if (s.IsNull()) return {};
std::ostringstream oss;
BRepTools::Write(s, oss);
return oss.str();
}
TopoDS_Shape brep_from_string(const std::string& d)
{
if (d.empty()) return {};
std::istringstream iss(d);
TopoDS_Shape s;
BRep_Builder b;
BRepTools::Read(s, iss, b);
return s;
}
std::string CadDocument::serialize_recipe() const
{
std::ostringstream oss;
{
cereal::BinaryOutputArchive ar(oss);
uint32_t v = SNAPORCA_CAD_RECIPE_VERSION;
ar(v);
ar(features);
}
return oss.str();
}
bool CadDocument::deserialize_recipe(const std::string& blob)
{
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
uint32_t v;
ar(v);
if (v > SNAPORCA_CAD_RECIPE_VERSION)
return false;
ar(features);
return recompute();
} catch (const Standard_Failure&) {
return false;
} catch (...) {
return false;
}
}
bool CadDocument::export_step(const std::string& path,
const std::vector<Transform3d>& body_xforms,
std::string& err) const
{
err.clear();
if (bodies.empty()) { err = "nothing to export"; return false; }
try {
// Compound every body at its displayed (Move-gizmo) position so the STEP matches
// what Commit ships. Move transforms are rigid, so gp_Trsf::SetValues is valid.
BRep_Builder bld;
TopoDS_Compound comp;
bld.MakeCompound(comp);
for (size_t i = 0; i < bodies.size(); ++i) {
if (bodies[i].shape.IsNull()) continue;
TopoDS_Shape s = bodies[i].shape;
if (i < body_xforms.size() && !body_xforms[i].isApprox(Transform3d::Identity())) {
const Transform3d& m = body_xforms[i];
gp_Trsf t;
t.SetValues(m(0,0), m(0,1), m(0,2), m(0,3),
m(1,0), m(1,1), m(1,2), m(1,3),
m(2,0), m(2,1), m(2,2), m(2,3));
s = BRepBuilderAPI_Transform(s, t, true).Shape();
}
bld.Add(comp, s);
}
STEPControl_Writer writer;
if (writer.Transfer(comp, STEPControl_AsIs) != IFSelect_RetDone) {
err = "STEP transfer failed";
return false;
}
if (writer.Write(path.c_str()) != IFSelect_RetDone) {
err = "cannot write STEP file";
return false;
}
} catch (const Standard_Failure& e) {
err = e.GetMessageString() ? e.GetMessageString() : "OCCT failed to write STEP";
return false;
} catch (const std::exception& e) {
err = e.what();
return false;
}
return true;
}
} // namespace Slic3r