mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-20 15:32:33 +00:00
Design tab: reference planes at bed centre, slot dims, hole cube handle, real threads
Port of the snaporca CAD work to the mainline fork. - Onshape default planes (XY/XZ/YZ) at the bed centre (transparent, labelled); modeling origin unified to the bed centre (CadDocument::modeling_origin); world-axis triad moved to the bed centre on the Design canvas only. - Datum plane: clickable ghost-plane base pick + draggable offset arrow. - Slot: dims reassessed to inter-centre distance / radius / angle; fixed the duplicate cap-arc radius quote. - Hole: 3D cube move-handle on the face; binds to the face on the first click; decluttered side-distance construction lines. - Thread: derive the M spec (diameter/pitch/depth) from a picked cylindrical surface or circular edge (GeometryEngine::circle_of_edge); fuse the helical ridge onto the existing body; MakePipeShell fixed-binormal sweep (uniform, no twist) + self-intersection/param guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
852804450c
commit
71b7a73e86
+213
-22
@@ -15,6 +15,7 @@
|
||||
#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>
|
||||
@@ -22,6 +23,7 @@
|
||||
#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>
|
||||
@@ -74,19 +76,26 @@ static TopoDS_Wire make_helix_wire(const gp_Ax3& axis, double radius,
|
||||
// - 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_Face make_thread_profile(const gp_Pnt& origin, const gp_Dir& xdir,
|
||||
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);
|
||||
double inner = radius - 0.05; // base, just inside the wall (overlaps rod / open bore)
|
||||
// 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
|
||||
gp_Pnt top (origin.XYZ() + (vx * inner).XYZ() + (vz * ( 0.5 * pitch)).XYZ());
|
||||
gp_Pnt bot (origin.XYZ() + (vx * inner).XYZ() + (vz * (-0.5 * pitch)).XYZ());
|
||||
// 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 BRepBuilderAPI_MakeFace(poly.Wire(), Standard_True).Face();
|
||||
return poly.Wire(); // closed triangle, swept by MakePipeShell with a fixed binormal
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -697,20 +706,173 @@ static SketchPlane offset_angle_plane(const SketchPlane& base, double offset,
|
||||
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();
|
||||
else if (f.plane_base == 2) base = SketchPlane::YZ();
|
||||
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; // index into earlier datum planes
|
||||
if (di < int(out.size())) base = out[di].second; // else XY default
|
||||
const int di = f.plane_base - 3;
|
||||
if (di < int(out.size())) base = out[di].second;
|
||||
}
|
||||
out.emplace_back(f.name,
|
||||
offset_angle_plane(base, f.plane_offset, f.plane_angle_tilt, f.plane_axis));
|
||||
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;
|
||||
}
|
||||
@@ -1187,6 +1349,17 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
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());
|
||||
@@ -1201,17 +1374,25 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
try {
|
||||
TopoDS_Wire spine = make_helix_wire(ax3, f.thread_radius,
|
||||
f.thread_pitch, f.thread_height);
|
||||
TopoDS_Face prof = make_thread_profile(c, xdir, zdir, f.thread_radius,
|
||||
TopoDS_Wire prof = make_thread_profile(c, xdir, zdir, f.thread_radius,
|
||||
f.thread_pitch, f.thread_depth,
|
||||
f.thread_internal);
|
||||
BRepOffsetAPI_MakePipe pipe(spine, prof);
|
||||
// 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()) {
|
||||
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) {
|
||||
@@ -1233,15 +1414,25 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
result = cut_ridge.Shape();
|
||||
}
|
||||
} else {
|
||||
// External threaded rod = a New body: base cylinder + fused ridge.
|
||||
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();
|
||||
// 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;
|
||||
}
|
||||
result = rod;
|
||||
have_body = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Slic3r {
|
||||
|
||||
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut };
|
||||
enum class SketchShape { Rectangle, Circle };
|
||||
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
|
||||
enum class BooleanMode { New, Add, Cut, Intersect };
|
||||
|
||||
enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex };
|
||||
@@ -169,6 +170,17 @@ struct CadFeature {
|
||||
double plane_offset{20};
|
||||
double plane_angle_tilt{0}; // degrees (named *_tilt to avoid clash w/ revolve)
|
||||
int plane_axis{0}; // tilt axis: 0 = base X, 1 = base Y
|
||||
PlaneType plane_type{PlaneType::Offset};
|
||||
int plane_face_body{-1};
|
||||
int plane_face{-1};
|
||||
int plane_face2_body{-1};
|
||||
int plane_face2{-1};
|
||||
int plane_edge_body{-1};
|
||||
int plane_edge{-1};
|
||||
int plane_edge2_body{-1};
|
||||
int plane_edge2{-1};
|
||||
double plane_u_size{60};
|
||||
double plane_v_size{60};
|
||||
|
||||
// Boolean: combine two EXISTING bodies. `mode` reuses BooleanMode (Add = union,
|
||||
// Cut = subtract tool from target, Intersect = keep overlap; New unused). `target_body`
|
||||
@@ -208,8 +220,10 @@ struct CadFeature {
|
||||
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
|
||||
plane_base, plane_offset, plane_angle_tilt, plane_axis,
|
||||
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
|
||||
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
|
||||
brep);
|
||||
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
|
||||
brep,
|
||||
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
|
||||
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size);
|
||||
}
|
||||
template<class Archive>
|
||||
void load(Archive& ar) {
|
||||
@@ -230,7 +244,9 @@ struct CadFeature {
|
||||
plane_base, plane_offset, plane_angle_tilt, plane_axis,
|
||||
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
|
||||
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
|
||||
brep);
|
||||
brep,
|
||||
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
|
||||
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size);
|
||||
imported_solid = brep_from_string(brep);
|
||||
}
|
||||
};
|
||||
@@ -263,6 +279,11 @@ public:
|
||||
std::vector<int> display_tri_body; // per-triangle source body index (into bodies)
|
||||
std::string error; // last recompute error ("" = ok)
|
||||
|
||||
// Modeling origin: the world point the default XY/XZ/YZ planes pass through. The GUI sets this
|
||||
// to the bed centre so sketches/datums land in the middle of the bed (not the bed corner =
|
||||
// world 0). Not serialized — the GUI re-applies it from the live bed on every tab show.
|
||||
Vec3d modeling_origin{Vec3d::Zero()};
|
||||
|
||||
double linear_deflection{0.01};
|
||||
double angular_deflection{0.5};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <GeomLProp_SLProps.hxx>
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <gp_Circ.hxx>
|
||||
#include <GCPnts_TangentialDeflection.hxx>
|
||||
#include <STEPControl_Reader.hxx>
|
||||
#include <IFSelect_ReturnStatus.hxx>
|
||||
@@ -397,6 +398,23 @@ GeometryEngine::CylinderFace GeometryEngine::cylinder_of_face(const TopoDS_Face&
|
||||
return cf;
|
||||
}
|
||||
|
||||
GeometryEngine::CylinderFace GeometryEngine::circle_of_edge(const TopoDS_Edge& edge)
|
||||
{
|
||||
CylinderFace cf;
|
||||
if (edge.IsNull()) return cf;
|
||||
BRepAdaptor_Curve curve(edge);
|
||||
if (curve.GetType() != GeomAbs_Circle) return cf;
|
||||
const gp_Circ c = curve.Circle();
|
||||
const gp_Ax1 ax = c.Axis();
|
||||
cf.base = Vec3d(c.Location().X(), c.Location().Y(), c.Location().Z());
|
||||
cf.axis = Vec3d(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
|
||||
cf.radius = c.Radius();
|
||||
cf.height = 0.0; // an edge carries no axial extent; the card keeps the current length
|
||||
cf.internal = false; // ambiguous from an edge alone — default external, user can toggle
|
||||
cf.ok = true;
|
||||
return cf;
|
||||
}
|
||||
|
||||
bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
|
||||
const Vec3d& x_axis, const Vec3d& y_axis,
|
||||
double& umin, double& umax, double& vmin, double& vmax)
|
||||
|
||||
@@ -103,6 +103,10 @@ public:
|
||||
bool internal{false};
|
||||
};
|
||||
static CylinderFace cylinder_of_face(const TopoDS_Face& face);
|
||||
// Circular edge (a cylinder's perimeter): base = circle centre, axis = circle normal,
|
||||
// radius = circle radius, height = 0 (unknown from an edge), internal = false. ok=false if
|
||||
// the edge is not a circle. Lets the Thread tool be driven by a picked circular rim.
|
||||
static CylinderFace circle_of_edge(const TopoDS_Edge& edge);
|
||||
|
||||
// Plane-coordinate (u,v) bounding box of a face's vertices, measured from `origin` along
|
||||
// `x_axis`/`y_axis`. Lets the Hole tool dimension the hole from the face SIDES (umin/vmin =
|
||||
|
||||
@@ -134,6 +134,7 @@ public:
|
||||
|
||||
void set_position(Vec2d& position);
|
||||
void set_axes_mode(bool origin);
|
||||
void set_axes_origin(const Vec3d& origin) { m_axes.set_origin(origin); } // Design tab: triad at bed centre
|
||||
const Vec2d& get_position() const { return m_position; }
|
||||
|
||||
// Build volume geometry for various collision detection tasks.
|
||||
|
||||
@@ -53,6 +53,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
|
||||
m_canvas->enable_collapse_toolbar(false);
|
||||
m_canvas->enable_plate_chrome(false);
|
||||
m_canvas->enable_labels(false);
|
||||
m_canvas->set_axes_at_bed_center(true); // triad at bed centre = modeling origin
|
||||
|
||||
m_canvas->set_design_sketch_tool(&m_sketch_tool);
|
||||
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
|
||||
@@ -663,6 +664,48 @@ void DesignCanvas::set_on_extrude_depth_changed(std::function<void(double, bool)
|
||||
m_sketch_tool.on_extrude_depth_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||||
double offset, bool offset_on)
|
||||
{
|
||||
m_sketch_tool.set_datum_gizmo(plane, usize, vsize, base_origin, base_normal, offset, offset_on);
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_datum_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_datum_gizmo();
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_datum_size_changed(std::function<void(double, double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_datum_size_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_datum_offset_changed(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_datum_offset_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||||
std::vector<std::string> labels)
|
||||
{
|
||||
m_sketch_tool.set_base_pick(std::move(planes), std::move(bases), std::move(labels));
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_base_pick()
|
||||
{
|
||||
m_sketch_tool.clear_base_pick();
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_datum_base_picked(std::function<void(int)> cb)
|
||||
{
|
||||
m_sketch_tool.on_datum_base_picked = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_exit(std::function<void()> cb)
|
||||
{
|
||||
m_sketch_tool.on_exit = std::move(cb);
|
||||
@@ -685,9 +728,9 @@ void DesignCanvas::set_display_sketches(std::vector<DesignSketchTool::DisplaySke
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_datum_planes(std::vector<SketchPlane> planes)
|
||||
void DesignCanvas::set_datum_planes(std::vector<SketchPlane> planes, std::vector<Vec2d> sizes)
|
||||
{
|
||||
m_sketch_tool.set_datum_planes(std::move(planes));
|
||||
m_sketch_tool.set_datum_planes(std::move(planes), std::move(sizes));
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
|
||||
@@ -143,11 +143,22 @@ public:
|
||||
double depth, double depth2, bool two_sided, bool flip);
|
||||
void clear_extrude_gizmo();
|
||||
void set_on_extrude_depth_changed(std::function<void(double, bool)> cb);
|
||||
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||||
double offset, bool offset_on); // C3 resize handles + offset arrow
|
||||
void clear_datum_gizmo();
|
||||
void set_on_datum_size_changed(std::function<void(double, double)> cb);
|
||||
void set_on_datum_offset_changed(std::function<void(double)> cb);
|
||||
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||||
std::vector<std::string> labels = {}); // clickable labelled reference planes
|
||||
void clear_base_pick();
|
||||
void set_on_datum_base_picked(std::function<void(int)> cb);
|
||||
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
|
||||
void set_on_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
|
||||
// Persistently draw committed sketches (un-consumed ones stay visible).
|
||||
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
|
||||
void set_datum_planes(std::vector<SketchPlane> planes); // draw datum/reference planes
|
||||
void set_datum_planes(std::vector<SketchPlane> planes,
|
||||
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
|
||||
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
|
||||
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
|
||||
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
|
||||
@@ -200,15 +211,16 @@ public:
|
||||
// Constraint glyph badges (C3.4b): the feature's constraints, drawn as iconic
|
||||
// marks near their entities in Constrain mode; empty clears them.
|
||||
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> cons);
|
||||
|
||||
private:
|
||||
void reload(bool keep_view);
|
||||
// Repaint the embedded canvas the right way for the active GL backend:
|
||||
// hardware GL gets a scheduled wxEVT_PAINT (render() runs inside the paint
|
||||
// cycle); software GL (llvmpipe etc.) gets a direct render() because a
|
||||
// scheduled Refresh() is frequently dropped there. Backend cached on first use.
|
||||
// Public: DesignPanel calls it after a tree edit to force a frame on software GL.
|
||||
void request_repaint();
|
||||
|
||||
private:
|
||||
void reload(bool keep_view);
|
||||
|
||||
wxGLCanvas* m_canvas_widget{nullptr};
|
||||
GLCanvas3D* m_canvas{nullptr};
|
||||
int m_sw_gl{-1}; // -1 unknown, 0 hardware GL, 1 software GL
|
||||
|
||||
+391
-65
@@ -37,6 +37,7 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/BuildVolume.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
|
||||
@@ -394,6 +395,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
auto* b_plane = icon_btn("design_plane", _L("Plane"));
|
||||
b_plane->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
populate_plane_choices(m_plane_base); // refresh base list w/ existing datum planes
|
||||
reset_plane_refs(); // fresh datum: no captured face/edge refs
|
||||
open_tool(Tool::Plane);
|
||||
});
|
||||
fadd(b_plane);
|
||||
@@ -474,38 +476,40 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
open_tool(Tool::Hole);
|
||||
}},
|
||||
{"design_thread", _L("Thread"), _L("Thread a picked cylindrical face (hole bore or cylinder)"),
|
||||
{"design_thread", _L("Thread"), _L("Thread a cylindrical surface (inner bore / outer) or a circular edge"),
|
||||
[this] {
|
||||
// #3: invoke on a picked CYLINDRICAL face — a hole bore (internal thread) or a
|
||||
// cylinder's lateral surface (external) — deriving axis, radius and internal/
|
||||
// external from it. Otherwise fall back to the plane dropdown.
|
||||
// Driven by a picked CYLINDRICAL surface (inner bore = internal, outer = external)
|
||||
// OR a circular EDGE (a cylinder's rim) — axis + diameter come from the geometry, so
|
||||
// the user never types a radius. The diameter field shows what was derived.
|
||||
m_thread_on_face = false;
|
||||
m_thread_face_body = -1;
|
||||
if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0
|
||||
&& m_sel_solid_body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Face face = GeometryEngine::face_by_index(
|
||||
m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face);
|
||||
const GeometryEngine::CylinderFace cf = GeometryEngine::cylinder_of_face(face);
|
||||
if (cf.ok) {
|
||||
SketchPlane p; // plane on the axis (origin at the base)
|
||||
p.origin = cf.base;
|
||||
p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
if (m_thread_radius) m_thread_radius->SetValue(cf.radius);
|
||||
if (m_thread_height) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
if (m_thread_x) m_thread_x->SetValue(0.0); // on the axis
|
||||
if (m_thread_y) m_thread_y->SetValue(0.0);
|
||||
} else if (m_sel_solid_face >= 0) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
m_status->SetLabel(_L("Pick a cylindrical face (hole bore or cylinder) for a thread"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
GeometryEngine::CylinderFace cf;
|
||||
if (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Shape& shape = m_doc.bodies[m_sel_solid_body].shape;
|
||||
if (m_sel_solid_face >= 0)
|
||||
cf = GeometryEngine::cylinder_of_face(GeometryEngine::face_by_index(shape, m_sel_solid_face));
|
||||
if (!cf.ok && m_sel_solid_edge >= 0)
|
||||
cf = GeometryEngine::circle_of_edge(GeometryEngine::edge_by_index(shape, m_sel_solid_edge));
|
||||
}
|
||||
if (cf.ok) {
|
||||
SketchPlane p; // plane on the axis (origin at the base)
|
||||
p.origin = cf.base;
|
||||
p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
infer_thread_spec(2.0 * cf.radius); // M diameter + pitch + depth from the cylinder
|
||||
if (m_thread_height && cf.height > 1e-6) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
if (m_thread_x) m_thread_x->SetValue(0.0); // on the axis
|
||||
if (m_thread_y) m_thread_y->SetValue(0.0);
|
||||
} else if (m_sel_solid_face >= 0 || m_sel_solid_edge >= 0) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
m_status->SetLabel(_L("Pick a cylindrical surface (bore / outer) or a circular edge for a thread"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
open_tool(Tool::Thread);
|
||||
}},
|
||||
@@ -664,6 +668,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
_L("Click a segment to trim it back to its nearest intersection; right-click exits"));
|
||||
skbtn("design_extend", DesignSketchTool::Mode::Extend, _L("Extend"),
|
||||
_L("Click a line or arc to extend it to the nearest entity; right-click exits"));
|
||||
// Constrain — grouped with the edit tools so it's easy to find (nde #13: it was buried
|
||||
// far-right next to Construction and went unnoticed). Commits the live sketch in place
|
||||
// and drops into Constrain mode (geometric/dimensional palette).
|
||||
auto* b_constrain_sk = icon_btn("design_constrain",
|
||||
_L("Constrain — add geometric/dimensional relations"));
|
||||
b_constrain_sk->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { enter_constrain_inline(); });
|
||||
sadd(b_constrain_sk);
|
||||
dropdown("design_move", _L("Move / rotate / scale"), {
|
||||
{"design_move", DesignSketchTool::Mode::Move, _L("Move (translate)"), _L("Pick entities, then drag the handle or click the distance; click empty to apply")},
|
||||
{"design_rotate", DesignSketchTool::Mode::Rotate, _L("Rotate (about centroid)"), _L("Pick entities, then drag around the pivot or click the angle; click empty to apply")},
|
||||
@@ -692,15 +703,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
if (m_viewport) m_viewport->set_sketch_polygon_circumscribed(m_poly_circ->GetValue()); });
|
||||
sadd(m_poly_circ);
|
||||
add_sep(m_tb_sketch);
|
||||
// Constrain — reachable mid-sketch: commits the live sketch in place and drops into
|
||||
// Constrain mode, where the geometric/dimensional palette + Trim + Extend (scissors)
|
||||
// are picked and applied. (Trim/Extend are pick-then-apply, so they live there, not as
|
||||
// bare toolbar buttons.)
|
||||
auto* b_constrain_sk = icon_btn("design_constrain",
|
||||
_L("Constrain — add geometric/dimensional relations; Trim/Extend live here"));
|
||||
b_constrain_sk->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { enter_constrain_inline(); });
|
||||
sadd(b_constrain_sk);
|
||||
add_sep(m_tb_sketch);
|
||||
m_construction->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) {
|
||||
if (m_viewport && m_viewport->is_sketching())
|
||||
m_viewport->set_sketch_construction(m_construction->GetValue()); });
|
||||
@@ -994,8 +996,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_std);
|
||||
|
||||
m_thread_radius = make_spin(m_form, 5.0);
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Radius")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
|
||||
// surface / circular edge, so it's a readout users rarely type. Stored field holds the diameter.
|
||||
m_thread_radius = make_spin(m_form, 10.0);
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Diameter")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_radius);
|
||||
|
||||
m_thread_pitch = make_spin(m_form, 2.0);
|
||||
@@ -1219,6 +1223,21 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_box_plane->Add(card_header("design_sketch", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
// Plane type chooses which inputs matter (Onshape/Fusion parity):
|
||||
// Offset = Base (or Face A) + Offset (+ Tilt about a base axis)
|
||||
// Angle = Edge A (line) + Base/Face A reference + Angle°
|
||||
// Midplane = Face A + Face B (halfway between)
|
||||
// Tangent = Face A (a cylinder) + Angle° around its axis
|
||||
// Two edges = Edge A + Edge B
|
||||
// Coincident = Face A (lie on that face)
|
||||
m_plane_type = new wxChoice(m_form, wxID_ANY);
|
||||
for (const wxString& t : { _L("Offset"), _L("Angle"), _L("Midplane"),
|
||||
_L("Tangent"), _L("Two edges"), _L("Coincident") })
|
||||
m_plane_type->Append(t);
|
||||
m_plane_type->SetSelection(0);
|
||||
m_box_plane->Add(new wxStaticText(m_form, wxID_ANY, _L("Plane type")), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(m_plane_type, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
|
||||
auto* plform = new wxFlexGridSizer(2, 6, 8);
|
||||
|
||||
m_plane_base = new wxChoice(m_form, wxID_ANY);
|
||||
@@ -1231,7 +1250,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
plform->Add(m_plane_offset);
|
||||
|
||||
m_plane_tilt = make_spin(m_form, 0.0, -180.0, 180.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt°")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Angle°")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_tilt);
|
||||
|
||||
m_plane_tilt_axis = new wxChoice(m_form, wxID_ANY);
|
||||
@@ -1241,6 +1260,26 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt axis")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_tilt_axis);
|
||||
|
||||
// Contextual reference picks: arm a target, then click a solid face/edge in the canvas.
|
||||
auto pick_row = [&](const wxString& label, wxButton*& btn, wxStaticText*& lbl, PlanePick target) {
|
||||
btn = new wxButton(m_form, wxID_ANY, label);
|
||||
lbl = new wxStaticText(m_form, wxID_ANY, _L("(none)"));
|
||||
btn->Bind(wxEVT_BUTTON, [this, target](wxCommandEvent&) { arm_plane_pick(target); });
|
||||
plform->Add(btn);
|
||||
plform->Add(lbl, 0, wxALIGN_CENTER_VERTICAL);
|
||||
};
|
||||
pick_row(_L("Pick Face A"), m_plane_pick_faceA, m_plane_faceA_lbl, PlanePick::FaceA);
|
||||
pick_row(_L("Pick Face B"), m_plane_pick_faceB, m_plane_faceB_lbl, PlanePick::FaceB);
|
||||
pick_row(_L("Pick Edge A"), m_plane_pick_edgeA, m_plane_edgeA_lbl, PlanePick::EdgeA);
|
||||
pick_row(_L("Pick Edge B"), m_plane_pick_edgeB, m_plane_edgeB_lbl, PlanePick::EdgeB);
|
||||
|
||||
m_plane_usize = make_spin(m_form, 60.0, 1.0, 100000.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Size U")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_usize);
|
||||
m_plane_vsize = make_spin(m_form, 60.0, 1.0, 100000.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Size V")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_vsize);
|
||||
|
||||
m_box_plane->Add(plform, 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
}
|
||||
root->Add(m_box_plane, 0, wxEXPAND);
|
||||
@@ -1685,6 +1724,60 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
: _L("(pick a side face)"));
|
||||
refresh_preview();
|
||||
}
|
||||
// Hole card open: clicking a solid FACE re-targets the hole ONTO that face (Orca-style),
|
||||
// so the hole lives on the object's face — not on a stale dropdown/datum plane. Uses the
|
||||
// face under the cursor from the FIRST click (handle_solid_click reports it even at the
|
||||
// Whole level), so no whole->face cycle is needed.
|
||||
if (m_active == Tool::Hole && face >= 0 && body >= 0 && body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Face fc = GeometryEngine::face_by_index(m_doc.bodies[body].shape, face);
|
||||
if (!fc.IsNull()) {
|
||||
m_hole_face_plane = face_plane_inward(fc);
|
||||
m_hole_on_face = true;
|
||||
m_hole_face_body = body;
|
||||
m_hole_has_bounds = GeometryEngine::face_plane_bounds(
|
||||
fc, m_hole_face_plane.origin, m_hole_face_plane.x_axis,
|
||||
m_hole_face_plane.y_axis, m_hole_umin, m_hole_umax, m_hole_vmin, m_hole_vmax);
|
||||
if (m_hole_x) m_hole_x->SetValue(0.0); // centre of the picked face
|
||||
if (m_hole_y) m_hole_y->SetValue(0.0);
|
||||
if (m_hole_plane) m_hole_plane->SetSelection(index_from_plane(m_hole_face_plane));
|
||||
refresh_preview(); // re-place the gizmo + ghost on the new face
|
||||
}
|
||||
}
|
||||
// Thread card open: clicking a cylindrical face or circular edge re-derives the thread.
|
||||
if (m_active == Tool::Thread && body >= 0 && body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Shape& shape = m_doc.bodies[body].shape;
|
||||
GeometryEngine::CylinderFace cf;
|
||||
if (face >= 0)
|
||||
cf = GeometryEngine::cylinder_of_face(GeometryEngine::face_by_index(shape, face));
|
||||
if (!cf.ok && edge >= 0)
|
||||
cf = GeometryEngine::circle_of_edge(GeometryEngine::edge_by_index(shape, edge));
|
||||
if (cf.ok) {
|
||||
SketchPlane p; p.origin = cf.base; p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
infer_thread_spec(2.0 * cf.radius); // M diameter + pitch + depth from the cylinder
|
||||
if (m_thread_height && cf.height > 1e-6) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
refresh_preview();
|
||||
}
|
||||
}
|
||||
// Plane tool with a pick armed: capture the right kind of reference (face for Face A/B,
|
||||
// edge for Edge A/B). If the click wasn't the right kind, stay armed so the user retries.
|
||||
if (m_active == Tool::Plane && m_plane_pick != PlanePick::None) {
|
||||
bool got = false;
|
||||
switch (m_plane_pick) {
|
||||
case PlanePick::FaceA: if (m_sel_solid_face >= 0) { m_pl_faceA_body = m_sel_solid_body; m_pl_faceA = m_sel_solid_face; got = true; } break;
|
||||
case PlanePick::FaceB: if (m_sel_solid_face >= 0) { m_pl_faceB_body = m_sel_solid_body; m_pl_faceB = m_sel_solid_face; got = true; } break;
|
||||
case PlanePick::EdgeA: if (m_sel_solid_edge >= 0) { m_pl_edgeA_body = m_sel_solid_body; m_pl_edgeA = m_sel_solid_edge; got = true; } break;
|
||||
case PlanePick::EdgeB: if (m_sel_solid_edge >= 0) { m_pl_edgeB_body = m_sel_solid_body; m_pl_edgeB = m_sel_solid_edge; got = true; } break;
|
||||
default: break;
|
||||
}
|
||||
if (got) { m_plane_pick = PlanePick::None; refresh_plane_labels(); }
|
||||
}
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
const int nb = int(m_doc.bodies.size());
|
||||
const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString();
|
||||
@@ -1703,6 +1796,53 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
refresh_preview();
|
||||
});
|
||||
|
||||
// Datum-plane resize handles (C3): a handle drag reports the new u/v extent. Mirror it into the
|
||||
// Size spins and, when editing a committed datum, into the feature so the rendered rectangle
|
||||
// follows live. SetValue doesn't emit a command event, so no refresh_preview recursion.
|
||||
m_viewport->set_on_datum_size_changed([this](double u, double v) {
|
||||
if (m_plane_usize) m_plane_usize->SetValue(u);
|
||||
if (m_plane_vsize) m_plane_vsize->SetValue(v);
|
||||
if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane) {
|
||||
m_doc.features[m_edit_index].plane_u_size = u;
|
||||
m_doc.features[m_edit_index].plane_v_size = v;
|
||||
refresh_datum_planes(); // committed datum rectangle follows the drag
|
||||
}
|
||||
m_viewport->request_repaint();
|
||||
});
|
||||
|
||||
// Offset arrow drag: mirror the new offset into the spin + (when editing) the committed feature.
|
||||
m_viewport->set_on_datum_offset_changed([this](double off) {
|
||||
if (m_plane_offset) m_plane_offset->SetValue(off);
|
||||
if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane) {
|
||||
m_doc.features[m_edit_index].plane_offset = off;
|
||||
refresh_datum_planes();
|
||||
}
|
||||
m_viewport->request_repaint();
|
||||
});
|
||||
|
||||
// Clicking a ghost base plane sets the base graphically (replaces the dropdown). A base pick
|
||||
// drops any offset-from-face choice so the picked base plane wins, then re-resolves the preview.
|
||||
m_viewport->set_on_datum_base_picked([this](int base) {
|
||||
if (m_active == Tool::Plane) {
|
||||
// Plane tool open: the click sets the datum's base plane (replaces the dropdown).
|
||||
if (m_plane_base && base >= 0 && base < int(m_plane_base->GetCount()))
|
||||
m_plane_base->SetSelection(base);
|
||||
m_pl_faceA_body = m_pl_faceA = -1;
|
||||
refresh_plane_labels();
|
||||
refresh_preview(); // re-resolve the frame + move the gizmo/ghosts to the new base
|
||||
} else {
|
||||
// Fallback (no object yet): clicking a reference plane selects it as the sketch plane.
|
||||
if (m_draw_plane && base >= 0 && base < int(m_draw_plane->GetCount()))
|
||||
m_draw_plane->SetSelection(base);
|
||||
const char* nm = (base == 0) ? "XY" : (base == 1) ? "XZ" : (base == 2) ? "YZ" : "datum";
|
||||
m_status->SetForegroundColour(wxColour(120, 210, 120));
|
||||
m_status->SetLabel(wxString::Format(_L("%s plane selected — press Sketch to draw on it"), nm));
|
||||
m_status->Refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a
|
||||
// display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched,
|
||||
// so face/edge ids the dress-up ops target stay valid).
|
||||
@@ -1742,7 +1882,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_viewport->set_on_thread_changed([this](double x, double y, double radius, double height) {
|
||||
if (m_thread_x) m_thread_x->SetValue(x);
|
||||
if (m_thread_y) m_thread_y->SetValue(y);
|
||||
if (m_thread_radius) m_thread_radius->SetValue(radius);
|
||||
if (m_thread_radius) m_thread_radius->SetValue(2.0 * radius); // gizmo reports radius; field = diameter
|
||||
if (m_thread_height) m_thread_height->SetValue(height);
|
||||
refresh_preview();
|
||||
});
|
||||
@@ -2101,9 +2241,10 @@ void DesignPanel::add_imported_sketch(
|
||||
on_face = true;
|
||||
}
|
||||
}
|
||||
if (!on_face)
|
||||
f.plane = m_draw_plane ? plane_from_choice(m_draw_plane->GetSelection())
|
||||
: SketchPlane::XY();
|
||||
if (!on_face) {
|
||||
if (m_draw_plane) f.plane = plane_from_choice(m_draw_plane->GetSelection());
|
||||
else { f.plane = SketchPlane::XY(); f.plane.origin += m_doc.modeling_origin; }
|
||||
}
|
||||
|
||||
// Drop the live face selection (its body is now remembered on import_face_body): otherwise
|
||||
// the next Extrude would push/pull that face instead of extruding the placed art.
|
||||
@@ -2289,8 +2430,10 @@ void DesignPanel::on_add_dressup()
|
||||
|
||||
SketchPlane DesignPanel::hole_plane() const
|
||||
{
|
||||
return m_hole_on_face ? m_hole_face_plane
|
||||
: plane_from_index(m_hole_plane->GetSelection());
|
||||
if (m_hole_on_face) return m_hole_face_plane;
|
||||
SketchPlane p = plane_from_index(m_hole_plane->GetSelection());
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_hole()
|
||||
@@ -2323,8 +2466,10 @@ void DesignPanel::on_add_hole()
|
||||
|
||||
SketchPlane DesignPanel::thread_plane() const
|
||||
{
|
||||
return m_thread_on_face ? m_thread_face_plane
|
||||
: plane_from_index(m_thread_plane->GetSelection());
|
||||
if (m_thread_on_face) return m_thread_face_plane;
|
||||
SketchPlane p = plane_from_index(m_thread_plane->GetSelection());
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
|
||||
void DesignPanel::apply_thread_standard()
|
||||
@@ -2341,13 +2486,13 @@ void DesignPanel::apply_thread_standard()
|
||||
if (m_thread_pitch) m_thread_pitch->SetValue(s->pitch_mm);
|
||||
if (m_thread_depth) m_thread_depth->SetValue(s->thread_depth_mm());
|
||||
|
||||
// Nominal radius: external rod = major radius; internal tapped bore = minor
|
||||
// (tap-drill) radius. On a picked cylindrical face the radius comes from the
|
||||
// real geometry, so don't override it there.
|
||||
// Nominal diameter: external rod = major diameter; internal tapped bore = minor (tap-drill)
|
||||
// diameter. On a picked cylindrical surface/edge the diameter comes from the real geometry,
|
||||
// so don't override it there. (The field holds DIAMETER.)
|
||||
if (!m_thread_on_face && m_thread_radius) {
|
||||
const bool internal = m_thread_internal && m_thread_internal->GetValue();
|
||||
const double d = internal ? s->minor_diameter_mm() : s->major_diameter_mm;
|
||||
m_thread_radius->SetValue(0.5 * d);
|
||||
m_thread_radius->SetValue(d);
|
||||
}
|
||||
|
||||
if (m_status)
|
||||
@@ -2355,6 +2500,24 @@ void DesignPanel::apply_thread_standard()
|
||||
m_thread_std->GetString(sel), s->pitch_mm));
|
||||
}
|
||||
|
||||
void DesignPanel::infer_thread_spec(double diameter)
|
||||
{
|
||||
// Snap to the nearest standard thread by nominal (major) diameter, so picking a Ø9.9 boss
|
||||
// gives M10 — the M diameter, pitch AND depth all follow from the cylinder's base diameter.
|
||||
const auto& stds = thread_standards();
|
||||
int best = -1; double bestErr = 1e30;
|
||||
for (int i = 0; i < int(stds.size()); ++i) {
|
||||
const double e = std::abs(stds[i].major_diameter_mm - diameter);
|
||||
if (e < bestErr) { bestErr = e; best = i; }
|
||||
}
|
||||
if (best < 0) { if (m_thread_radius) m_thread_radius->SetValue(diameter); return; }
|
||||
const ThreadSpec& s = stds[best];
|
||||
if (m_thread_std) m_thread_std->SetSelection(best + 1); // row 0 is "Custom"
|
||||
if (m_thread_radius) m_thread_radius->SetValue(s.major_diameter_mm); // field = DIAMETER
|
||||
if (m_thread_pitch) m_thread_pitch->SetValue(s.pitch_mm);
|
||||
if (m_thread_depth) m_thread_depth->SetValue(s.thread_depth_mm());
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_thread()
|
||||
{
|
||||
bool internal = m_thread_internal->GetValue();
|
||||
@@ -2365,7 +2528,7 @@ void DesignPanel::on_add_thread()
|
||||
SketchPlane plane = thread_plane();
|
||||
|
||||
m_feature_counter++;
|
||||
const int tidx = m_doc.add_thread(m_thread_radius->GetValue(), m_thread_pitch->GetValue(),
|
||||
const int tidx = m_doc.add_thread(m_thread_radius->GetValue() * 0.5, m_thread_pitch->GetValue(),
|
||||
m_thread_height->GetValue(), m_thread_depth->GetValue(),
|
||||
internal, m_thread_x->GetValue(), m_thread_y->GetValue(),
|
||||
plane, "Thread" + std::to_string(m_feature_counter));
|
||||
@@ -2554,18 +2717,63 @@ void DesignPanel::populate_plane_choices(wxChoice* c) const
|
||||
|
||||
SketchPlane DesignPanel::plane_from_choice(int row) const
|
||||
{
|
||||
if (row < 3) return plane_from_index(row); // 0=XY,1=XZ,2=YZ
|
||||
if (row < 3) { // 0=XY,1=XZ,2=YZ through the modeling origin
|
||||
SketchPlane p = plane_from_index(row);
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
// Datums are already in world coords (resolve_datum_planes applied the origin to their base).
|
||||
auto datums = m_doc.resolve_datum_planes();
|
||||
const int di = row - 3;
|
||||
return (di >= 0 && di < int(datums.size())) ? datums[di].second : SketchPlane::XY();
|
||||
if (di >= 0 && di < int(datums.size())) return datums[di].second;
|
||||
SketchPlane p = SketchPlane::XY(); p.origin += m_doc.modeling_origin; return p;
|
||||
}
|
||||
|
||||
void DesignPanel::apply_plane_refs(CadFeature& f) const
|
||||
{
|
||||
f.plane_type = (PlaneType)m_plane_type->GetSelection();
|
||||
f.plane_face_body = m_pl_faceA_body; f.plane_face = m_pl_faceA;
|
||||
f.plane_face2_body = m_pl_faceB_body; f.plane_face2 = m_pl_faceB;
|
||||
f.plane_edge_body = m_pl_edgeA_body; f.plane_edge = m_pl_edgeA;
|
||||
f.plane_edge2_body = m_pl_edgeB_body; f.plane_edge2 = m_pl_edgeB;
|
||||
f.plane_u_size = m_plane_usize->GetValue();
|
||||
f.plane_v_size = m_plane_vsize->GetValue();
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_plane_labels()
|
||||
{
|
||||
auto txt = [](int idx) { return idx >= 0 ? wxString::Format("#%d", idx) : wxString(_L("(none)")); };
|
||||
if (m_plane_faceA_lbl) m_plane_faceA_lbl->SetLabel(txt(m_pl_faceA));
|
||||
if (m_plane_faceB_lbl) m_plane_faceB_lbl->SetLabel(txt(m_pl_faceB));
|
||||
if (m_plane_edgeA_lbl) m_plane_edgeA_lbl->SetLabel(txt(m_pl_edgeA));
|
||||
if (m_plane_edgeB_lbl) m_plane_edgeB_lbl->SetLabel(txt(m_pl_edgeB));
|
||||
}
|
||||
|
||||
void DesignPanel::reset_plane_refs()
|
||||
{
|
||||
m_pl_faceA_body = m_pl_faceA = -1; m_pl_faceB_body = m_pl_faceB = -1;
|
||||
m_pl_edgeA_body = m_pl_edgeA = -1; m_pl_edgeB_body = m_pl_edgeB = -1;
|
||||
m_plane_pick = PlanePick::None;
|
||||
refresh_plane_labels();
|
||||
}
|
||||
|
||||
void DesignPanel::arm_plane_pick(PlanePick target)
|
||||
{
|
||||
m_plane_pick = target;
|
||||
const bool face = (target == PlanePick::FaceA || target == PlanePick::FaceB);
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(face ? _L("Click a solid FACE in the viewport")
|
||||
: _L("Click a solid EDGE in the viewport"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_plane()
|
||||
{
|
||||
m_feature_counter++;
|
||||
m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
|
||||
"Plane" + std::to_string(m_feature_counter));
|
||||
if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]);
|
||||
m_doc.recompute(); // datum-only docs yield no body; that is expected/benign
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(_L("Plane added — pick it as a sketch plane"));
|
||||
@@ -2644,6 +2852,13 @@ void DesignPanel::on_tab_shown()
|
||||
{
|
||||
if (m_viewport) m_viewport->refresh_bed();
|
||||
|
||||
// Modeling origin = bed centre, set BEFORE any recompute/datum-resolve so sketches and datums
|
||||
// land in the middle of the bed (not the bed corner = world 0).
|
||||
if (Plater* pl = wxGetApp().plater()) {
|
||||
const Vec2d bc = pl->build_volume().bed_center();
|
||||
m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0);
|
||||
}
|
||||
|
||||
// Rehydrate the parametric model from a freshly loaded project (the 3MF carried the
|
||||
// recipe in Metadata/SnapOrca_cad.bin). Only when nothing is in progress here, so we
|
||||
// never clobber an active design when the user just toggles back to the Design tab.
|
||||
@@ -2653,6 +2868,7 @@ void DesignPanel::on_tab_shown()
|
||||
if (!blob.empty()) load_recipe(blob);
|
||||
}
|
||||
}
|
||||
update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
|
||||
}
|
||||
|
||||
void DesignPanel::load_recipe(const std::string& blob)
|
||||
@@ -2681,11 +2897,8 @@ void DesignPanel::refresh_tree()
|
||||
wxTreeItemId root = m_tree->AddRoot("root");
|
||||
// Datum/reference planes carry no solid; feed them to the viewport so they render as
|
||||
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
|
||||
if (m_viewport) {
|
||||
std::vector<SketchPlane> dplanes;
|
||||
for (const auto& dp : m_doc.resolve_datum_planes()) dplanes.push_back(dp.second);
|
||||
m_viewport->set_datum_planes(std::move(dplanes));
|
||||
}
|
||||
refresh_datum_planes();
|
||||
update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
|
||||
for (const auto& f : m_doc.features) {
|
||||
const int img = tree_icon_for(f.type);
|
||||
wxTreeItemId id = m_tree->AppendItem(root, wxString::FromUTF8(f.name), img, img);
|
||||
@@ -2903,8 +3116,15 @@ void DesignPanel::after_tree_edit(bool ok)
|
||||
sync_sketch_display(); // empty body: show any un-consumed committed sketch
|
||||
m_status->SetLabel(wxString());
|
||||
} else {
|
||||
// nde #19/20: a delete/reorder that leaves bodies behind must re-feed the per-body
|
||||
// GLVolumes — otherwise the viewport keeps showing the pre-edit solid (the deleted
|
||||
// feature's artifact lingered). feed_bodies() is idempotent for the edit/replace path.
|
||||
if (m_viewport != nullptr) feed_bodies();
|
||||
set_status_ok();
|
||||
}
|
||||
// Force a frame: under software GL (llvmpipe on the :10 test box) reload()'s scheduled
|
||||
// Refresh() is dropped, so a deleted solid stayed on screen until the next orbit.
|
||||
if (m_viewport != nullptr) m_viewport->request_repaint();
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
@@ -4341,7 +4561,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
|
||||
break;
|
||||
case CadFeatureType::Thread:
|
||||
m_thread_plane->SetSelection(index_from_plane(f.plane));
|
||||
m_thread_radius->SetValue(f.thread_radius);
|
||||
m_thread_radius->SetValue(f.thread_radius * 2.0); // field = diameter
|
||||
m_thread_pitch->SetValue(f.thread_pitch);
|
||||
m_thread_height->SetValue(f.thread_height);
|
||||
m_thread_depth->SetValue(f.thread_depth);
|
||||
@@ -4382,6 +4602,15 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
|
||||
m_plane_offset->SetValue(f.plane_offset);
|
||||
m_plane_tilt->SetValue(f.plane_angle_tilt);
|
||||
m_plane_tilt_axis->SetSelection(f.plane_axis);
|
||||
m_plane_type->SetSelection((int)f.plane_type);
|
||||
m_pl_faceA_body = f.plane_face_body; m_pl_faceA = f.plane_face;
|
||||
m_pl_faceB_body = f.plane_face2_body; m_pl_faceB = f.plane_face2;
|
||||
m_pl_edgeA_body = f.plane_edge_body; m_pl_edgeA = f.plane_edge;
|
||||
m_pl_edgeB_body = f.plane_edge2_body; m_pl_edgeB = f.plane_edge2;
|
||||
m_plane_usize->SetValue(f.plane_u_size);
|
||||
m_plane_vsize->SetValue(f.plane_v_size);
|
||||
m_plane_pick = PlanePick::None;
|
||||
refresh_plane_labels();
|
||||
break;
|
||||
case CadFeatureType::Loft:
|
||||
m_loft_refs = f.loft_profile_refs; // show_tool re-checks these in the list
|
||||
@@ -4637,7 +4866,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const
|
||||
case Tool::Thread:
|
||||
f.type = CadFeatureType::Thread;
|
||||
f.plane = thread_plane();
|
||||
f.thread_radius = m_thread_radius->GetValue();
|
||||
f.thread_radius = m_thread_radius->GetValue() * 0.5; // field = diameter -> kernel radius
|
||||
f.thread_pitch = m_thread_pitch->GetValue();
|
||||
f.thread_height = m_thread_height->GetValue();
|
||||
f.thread_depth = m_thread_depth->GetValue();
|
||||
@@ -4688,6 +4917,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const
|
||||
f.plane_offset = m_plane_offset->GetValue();
|
||||
f.plane_angle_tilt = m_plane_tilt->GetValue();
|
||||
f.plane_axis = m_plane_tilt_axis->GetSelection();
|
||||
apply_plane_refs(f); // plane_type + face/edge refs + u/v size from the card
|
||||
break;
|
||||
case Tool::Loft: {
|
||||
f.type = CadFeatureType::Loft;
|
||||
@@ -4773,7 +5003,7 @@ void DesignPanel::update_thread_gizmo()
|
||||
const SketchPlane plane = thread_plane();
|
||||
m_viewport->begin_thread_gizmo(plane,
|
||||
m_thread_x->GetValue(), m_thread_y->GetValue(),
|
||||
m_thread_radius->GetValue(), m_thread_height->GetValue());
|
||||
m_thread_radius->GetValue() * 0.5, m_thread_height->GetValue());
|
||||
}
|
||||
|
||||
// Anchor an inward thickness arrow at the picked open face's centroid (along -outward-normal).
|
||||
@@ -4939,6 +5169,97 @@ void DesignPanel::update_extrude_gizmo()
|
||||
end == ExtrudeEnd::TwoSided, m_flip->GetValue());
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_datum_planes()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
std::vector<SketchPlane> dplanes;
|
||||
for (const auto& dp : m_doc.resolve_datum_planes()) dplanes.push_back(dp.second);
|
||||
// Parallel per-plane extents, in the SAME order resolve_datum_planes emits
|
||||
// (enabled Plane features, document order), so each datum draws at its u/v size.
|
||||
std::vector<Vec2d> dsizes;
|
||||
for (const auto& f : m_doc.features)
|
||||
if (f.type == CadFeatureType::Plane && f.enabled)
|
||||
dsizes.emplace_back(f.plane_u_size, f.plane_v_size);
|
||||
m_viewport->set_datum_planes(std::move(dplanes), std::move(dsizes));
|
||||
}
|
||||
|
||||
void DesignPanel::update_datum_gizmo()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
if (m_active != Tool::Plane) { m_viewport->clear_datum_gizmo(); update_reference_planes(); return; }
|
||||
|
||||
// Resolve the candidate plane's FRAME against the doc. resolve_datum_planes() is const and
|
||||
// doesn't rebuild bodies, so transiently swap/append the candidate to read its resolved frame,
|
||||
// then restore — works for both a fresh (uncommitted) plane and an edit of a committed one.
|
||||
CadFeature f = build_candidate(Tool::Plane);
|
||||
const bool editing = (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane);
|
||||
CadFeature saved;
|
||||
int slot;
|
||||
if (editing) { saved = m_doc.features[m_edit_index]; m_doc.features[m_edit_index] = f; slot = m_edit_index; }
|
||||
else { m_doc.features.push_back(f); slot = int(m_doc.features.size()) - 1; }
|
||||
|
||||
auto datums = m_doc.resolve_datum_planes();
|
||||
// Ordinal of `slot` among enabled Plane features = its index in the resolved list.
|
||||
int ord = -1;
|
||||
for (int i = 0; i <= slot; ++i)
|
||||
if (m_doc.features[i].type == CadFeatureType::Plane && m_doc.features[i].enabled) ++ord;
|
||||
const bool ok = (ord >= 0 && ord < int(datums.size()));
|
||||
SketchPlane frame; if (ok) frame = datums[ord].second;
|
||||
|
||||
if (editing) m_doc.features[m_edit_index] = saved; else m_doc.features.pop_back();
|
||||
|
||||
if (!ok) { m_viewport->clear_datum_gizmo(); update_reference_planes(); return; }
|
||||
|
||||
// Offset arrow anchor = the base/face origin (datum origin walked back along its normal by the
|
||||
// offset). Works for both Offset-from-base (no tilt) and Offset-from-face exactly.
|
||||
const Vec3d anchor = frame.origin - frame.normal * f.plane_offset;
|
||||
const bool offset_on = (f.plane_type == PlaneType::Offset);
|
||||
m_viewport->set_datum_gizmo(frame, f.plane_u_size, f.plane_v_size,
|
||||
anchor, frame.normal, f.plane_offset, offset_on);
|
||||
update_reference_planes(); // base ghosts (origins + datums) follow the tool/model state
|
||||
}
|
||||
|
||||
// Onshape default planes: the XY/XZ/YZ reference planes are persistent, transparent, labelled, and
|
||||
// larger than the bed — shown as the FALLBACK when there is no object yet. When the Plane tool is
|
||||
// open they additionally surface existing datums so a base can be picked. Single authority for the
|
||||
// reference-plane overlay (set/clear_base_pick).
|
||||
void DesignPanel::update_reference_planes()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
// Default planes pass through the modeling origin (bed centre) — same point the kernel uses to
|
||||
// resolve XY/XZ/YZ datum bases, so the ghosts, the datums and new sketches all coincide.
|
||||
const Vec3d o = m_doc.modeling_origin;
|
||||
SketchPlane xy = SketchPlane::XY(); xy.origin += o;
|
||||
SketchPlane xz = SketchPlane::XZ(); xz.origin += o;
|
||||
SketchPlane yz = SketchPlane::YZ(); yz.origin += o;
|
||||
std::vector<SketchPlane> bp = { xy, xz, yz };
|
||||
std::vector<int> bi = { 0, 1, 2 };
|
||||
std::vector<std::string> bl = { "XY", "XZ", "YZ" };
|
||||
|
||||
if (m_active == Tool::Plane) {
|
||||
// Base picking only makes sense for the Offset method (others reference faces/edges).
|
||||
if (m_plane_type && (PlaneType)m_plane_type->GetSelection() == PlaneType::Offset) {
|
||||
auto datums = m_doc.resolve_datum_planes(); // already in world coords (origin applied)
|
||||
for (int i = 0; i < int(datums.size()); ++i) {
|
||||
bp.push_back(datums[i].second); bi.push_back(3 + i); bl.push_back(datums[i].first);
|
||||
}
|
||||
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
|
||||
} else {
|
||||
m_viewport->clear_base_pick();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Fallback (Onshape default planes): show the 3 reference planes while there is no SOLID body
|
||||
// yet — so they persist through the 2D-sketch phase and reappear after a sketch is confirmed
|
||||
// (a sketch creates no body). They no longer block selection: clicking existing geometry wins,
|
||||
// a base-plane pick only fires on a click that hit nothing else (see on_mouse fall-through).
|
||||
if (m_doc.bodies.empty())
|
||||
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
|
||||
else
|
||||
m_viewport->clear_base_pick();
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_preview()
|
||||
{
|
||||
if (m_active == Tool::None) { m_viewport->clear_preview(); return; }
|
||||
@@ -4951,6 +5272,7 @@ void DesignPanel::refresh_preview()
|
||||
m_status->SetLabel(m_active == Tool::Plane ? _L("Plane ready") : _L("Sketch ready"));
|
||||
for (wxButton* b : m_confirm_btns) if (b) b->Enable(true);
|
||||
m_status->Refresh();
|
||||
update_datum_gizmo(); // Plane card: show/refresh the in-canvas resize handles
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5027,6 +5349,8 @@ void DesignPanel::refresh_preview()
|
||||
update_revolve_gizmo();
|
||||
// Same for the Pattern spacing arrow / angle-arc (self-gates: only while the Pattern card is open).
|
||||
update_pattern_gizmo();
|
||||
// Datum-plane resize handles (self-gates: only while the Plane card is open).
|
||||
update_datum_gizmo();
|
||||
}
|
||||
|
||||
void DesignPanel::open_tool(Tool t)
|
||||
@@ -5179,6 +5503,8 @@ void DesignPanel::close_tool()
|
||||
m_viewport->clear_shell_gizmo();
|
||||
m_viewport->clear_revolve_gizmo();
|
||||
m_viewport->clear_pattern_gizmo();
|
||||
m_viewport->clear_datum_gizmo();
|
||||
update_reference_planes(); // back to no-tool: show the origin planes if there is no object yet
|
||||
m_form->Layout();
|
||||
m_form->FitInside();
|
||||
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
|
||||
|
||||
@@ -39,6 +39,9 @@ public:
|
||||
|
||||
private:
|
||||
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert };
|
||||
// Plane tool: which datum reference the next solid pick fills (declared early so the
|
||||
// method decls + card lambdas below can name it).
|
||||
enum class PlanePick { None, FaceA, FaceB, EdgeA, EdgeB };
|
||||
|
||||
// Onshape-style contextual top toolbar: only the active mode's tool group is
|
||||
// shown (Feature = sketch/extrude/dress/hole/thread; Sketch = entity tools;
|
||||
@@ -57,11 +60,16 @@ private:
|
||||
void on_add_hole();
|
||||
void on_add_thread();
|
||||
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
|
||||
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
|
||||
void on_add_revolve();
|
||||
void on_add_sweep();
|
||||
void on_add_loft();
|
||||
void on_add_pattern();
|
||||
void on_add_plane();
|
||||
void arm_plane_pick(PlanePick target); // Plane tool: next solid pick fills this reference
|
||||
void apply_plane_refs(CadFeature& f) const; // copy type + face/edge refs + sizes from the card
|
||||
void refresh_plane_labels(); // update the 4 pick labels from the captured refs
|
||||
void reset_plane_refs(); // clear captured refs (fresh Plane add)
|
||||
void on_add_shell();
|
||||
void on_add_draft();
|
||||
void on_add_boolean();
|
||||
@@ -167,6 +175,9 @@ private:
|
||||
void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card)
|
||||
void update_revolve_gizmo(); // angle-arc around the axis (Revolve card)
|
||||
void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card)
|
||||
void update_datum_gizmo(); // resize handles on the datum plane being created/edited (C3)
|
||||
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
|
||||
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
|
||||
|
||||
CadDocument m_doc;
|
||||
|
||||
@@ -302,8 +313,22 @@ private:
|
||||
// Datum plane controls (derive a selectable sketch plane: offset + tilt from a base).
|
||||
wxChoice* m_plane_base{nullptr}; // 0=XY,1=XZ,2=YZ, 3+N = Nth datum plane
|
||||
wxSpinCtrlDouble* m_plane_offset{nullptr}; // offset along base normal (mm)
|
||||
wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg)
|
||||
wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg) / Angle / Tangent angle
|
||||
wxChoice* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
|
||||
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
|
||||
wxChoice* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
|
||||
wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
|
||||
wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
|
||||
wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
|
||||
wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
|
||||
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
|
||||
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
|
||||
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
|
||||
int m_pl_faceA_body{-1}, m_pl_faceA{-1};
|
||||
int m_pl_faceB_body{-1}, m_pl_faceB{-1};
|
||||
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
|
||||
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
|
||||
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
|
||||
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
|
||||
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
|
||||
int m_sel_sketch_feat{-1};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "libslic3r/BuildVolume.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "3DScene.hpp"
|
||||
#include "GLShader.hpp"
|
||||
@@ -1165,6 +1166,17 @@ void DesignSketchTool::open_primary_autoedit()
|
||||
m_autoedit_dims.push_back({ m_live_aslot_r_label, Rc, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, v, g.param); }, span(fi) });
|
||||
m_autoedit_dims.push_back({ m_live_aslot_w_label, fw, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v*0.5)); }, span(fi) });
|
||||
}
|
||||
if (m_live_slot_fi >= 0) {
|
||||
const Feature& f = m_features[m_live_slot_fi];
|
||||
const int fi = m_live_slot_fi;
|
||||
// Slot dims, in order: (1) inter-centre distance, (2) radius (= half-width), (3) angle.
|
||||
const Vec2d d = f.c1 - f.c0;
|
||||
const double Lc = d.norm();
|
||||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||||
m_autoedit_dims.push_back({ m_live_slot_len_label, Lc, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, v, g.param); }, span(fi) });
|
||||
m_autoedit_dims.push_back({ m_live_slot_w_label, f.param, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v)); }, span(fi) });
|
||||
m_autoedit_dims.push_back({ m_live_slot_angle_label, deg, [this, fi](double v){ set_slot_angle(fi, v); }, span(fi) });
|
||||
}
|
||||
if (m_live_arc_ei >= 0) { // arc Radius is already a scalar step above; add its sweep angle
|
||||
const int ei = m_live_arc_ei;
|
||||
const SketchEntity& e = m_entities[ei];
|
||||
@@ -1734,6 +1746,70 @@ void DesignSketchTool::set_arc_slot(int fi, double Rc, double w)
|
||||
resolve_live();
|
||||
}
|
||||
|
||||
// Open the inline editor for a straight slot's dimension: which 0 = inter-centre distance,
|
||||
// 1 = radius (half-width), 2 = centreline angle. Drives set_slot / set_slot_angle geometrically.
|
||||
void DesignSketchTool::open_slot_editor(int fi, int which)
|
||||
{
|
||||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||||
const Feature& f = m_features[fi];
|
||||
const Vec2d d = f.c1 - f.c0;
|
||||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||||
const double v = (which == 0) ? d.norm() : (which == 1) ? f.param : deg;
|
||||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||||
on_inline_edit(px, v,
|
||||
[this, fi, which](double nv) {
|
||||
const Feature& g = m_features[fi];
|
||||
if (which == 0) set_slot(fi, nv, g.param);
|
||||
else if (which == 1) set_slot(fi, (g.c1 - g.c0).norm(), std::max(1e-3, nv));
|
||||
else set_slot_angle(fi, nv);
|
||||
},
|
||||
[]() {});
|
||||
}
|
||||
|
||||
// Rebuild the straight slot's 4 entities in place for a new centreline length / half-width.
|
||||
// Centre c0 and the centreline direction are kept; only c1 (length) or param (width) change.
|
||||
// Geometric; entity count preserved so constraint refs stay valid.
|
||||
void DesignSketchTool::set_slot(int fi, double length, double w)
|
||||
{
|
||||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||||
Feature& f = m_features[fi];
|
||||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return;
|
||||
Vec2d dir = f.c1 - f.c0;
|
||||
if (dir.squaredNorm() < 1e-12) return;
|
||||
dir.normalize();
|
||||
length = std::max(length, 2e-3);
|
||||
w = std::max(1e-3, w);
|
||||
const Vec2d c0 = f.c0, c1 = f.c0 + length * dir;
|
||||
std::vector<SketchEntity> rebuilt = make_slot(c0, c1, w);
|
||||
if (int(rebuilt.size()) != 4) return;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||||
m_entities[f.begin + i] = rebuilt[i];
|
||||
}
|
||||
f.c1 = c1; f.param = w;
|
||||
resolve_live();
|
||||
}
|
||||
|
||||
// Rotate a straight slot about c0 to a new centreline angle (degrees), keeping length + radius.
|
||||
void DesignSketchTool::set_slot_angle(int fi, double deg)
|
||||
{
|
||||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||||
Feature& f = m_features[fi];
|
||||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return;
|
||||
const double L = (f.c1 - f.c0).norm();
|
||||
if (L < 1e-9) return;
|
||||
const double a = deg * M_PI / 180.0;
|
||||
const Vec2d c1 = f.c0 + L * Vec2d(std::cos(a), std::sin(a));
|
||||
std::vector<SketchEntity> rebuilt = make_slot(f.c0, c1, f.param);
|
||||
if (int(rebuilt.size()) != 4) return;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||||
m_entities[f.begin + i] = rebuilt[i];
|
||||
}
|
||||
f.c1 = c1;
|
||||
resolve_live();
|
||||
}
|
||||
|
||||
// Resize an axis-aligned rectangle by dragging a corner: the diagonally-opposite corner
|
||||
// (captured at grab as m_drag_rect_anchor) stays fixed; the box becomes [anchor, cursor].
|
||||
// Geometric rebuild in place (4 lines, same order) — edges stay axis-aligned so the
|
||||
@@ -2793,7 +2869,7 @@ void DesignSketchTool::render_datum_planes()
|
||||
if (m_datum_planes.empty()) return;
|
||||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||||
using EVL = GLModel::Geometry::EVertexLayout;
|
||||
const double H = 40.0; // half-extent of the drawn rectangle (mm)
|
||||
const double H = 40.0; // default half-extent when no per-plane size is given (mm)
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const Vec3d vd = cam.get_dir_forward();
|
||||
const double hw = 1.5 / std::max(cam.get_zoom(), 1e-6); // ~1.5 px border ribbon
|
||||
@@ -2801,9 +2877,16 @@ void DesignSketchTool::render_datum_planes()
|
||||
GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 };
|
||||
GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 };
|
||||
unsigned int fb = 0, bb = 0;
|
||||
for (const SketchPlane& p : m_datum_planes) {
|
||||
const Vec3d c[4] = { p.to_world(Vec2d(-H, -H)), p.to_world(Vec2d(H, -H)),
|
||||
p.to_world(Vec2d(H, H)), p.to_world(Vec2d(-H, H)) };
|
||||
for (size_t pi = 0; pi < m_datum_planes.size(); ++pi) {
|
||||
const SketchPlane& p = m_datum_planes[pi];
|
||||
// Per-plane u/v half-extent (the GUI Size U/V + drag handles drive these); fall
|
||||
// back to the square default when no size was supplied.
|
||||
const double hu = (pi < m_datum_sizes.size() && m_datum_sizes[pi].x() > 1e-6)
|
||||
? m_datum_sizes[pi].x() * 0.5 : H;
|
||||
const double hv = (pi < m_datum_sizes.size() && m_datum_sizes[pi].y() > 1e-6)
|
||||
? m_datum_sizes[pi].y() * 0.5 : H;
|
||||
const Vec3d c[4] = { p.to_world(Vec2d(-hu, -hv)), p.to_world(Vec2d(hu, -hv)),
|
||||
p.to_world(Vec2d(hu, hv)), p.to_world(Vec2d(-hu, hv)) };
|
||||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[1].cast<float>());
|
||||
fill.add_vertex((Vec3f)c[2].cast<float>()); fill.add_triangle(fb, fb + 1, fb + 2); fb += 3;
|
||||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[2].cast<float>());
|
||||
@@ -2955,6 +3038,276 @@ void DesignSketchTool::open_extrude_editor(int which)
|
||||
[]() {});
|
||||
}
|
||||
|
||||
// ---- Datum-plane resize gizmo (C3) ----------------------------------------------------
|
||||
void DesignSketchTool::set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||||
double offset, bool offset_on)
|
||||
{
|
||||
m_dz_active = true;
|
||||
m_dz_plane = plane;
|
||||
m_dz_usize = std::max(1.0, usize);
|
||||
m_dz_vsize = std::max(1.0, vsize);
|
||||
m_dz_anchor = base_origin;
|
||||
m_dz_normal = base_normal.normalized();
|
||||
m_dz_offset = offset;
|
||||
m_dz_offset_on = offset_on;
|
||||
}
|
||||
|
||||
void DesignSketchTool::clear_datum_gizmo()
|
||||
{
|
||||
m_dz_active = false;
|
||||
m_dz_drag = -1;
|
||||
}
|
||||
|
||||
// Draw the datum rectangle outline + 4 camera-billboarded edge-midpoint handles. Self-contained
|
||||
// so it shows even for an uncommitted (not-yet-Confirmed) datum that render_datum_planes can't draw.
|
||||
void DesignSketchTool::render_datum_gizmo()
|
||||
{
|
||||
if (!m_dz_active) return;
|
||||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||||
using EVL = GLModel::Geometry::EVertexLayout;
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const Vec3d right = cam.get_dir_right().normalized();
|
||||
const Vec3d up = cam.get_dir_up().normalized();
|
||||
const Vec3d vd = cam.get_dir_forward();
|
||||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||||
const double hs = 6.0 * upp; // handle half-size (~6 px)
|
||||
const double hw = 1.5 * upp; // outline ribbon half-width
|
||||
const double hu = m_dz_usize * 0.5, hv = m_dz_vsize * 0.5;
|
||||
const SketchPlane& p = m_dz_plane;
|
||||
|
||||
// Rectangle outline (4 thin camera-facing ribbons).
|
||||
const Vec3d c[4] = { p.to_world(Vec2d(-hu, -hv)), p.to_world(Vec2d(hu, -hv)),
|
||||
p.to_world(Vec2d(hu, hv)), p.to_world(Vec2d(-hu, hv)) };
|
||||
GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 };
|
||||
unsigned int bb = 0;
|
||||
for (int s = 0; s < 4; ++s) {
|
||||
const Vec3d a = c[s], b = c[(s + 1) & 3];
|
||||
Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize();
|
||||
Vec3d off = dir.cross(vd);
|
||||
if (off.norm() < 1e-9) off = dir.cross(up);
|
||||
if (off.norm() < 1e-9) continue;
|
||||
off.normalize(); off *= hw;
|
||||
border.add_vertex((Vec3f)(a + off).cast<float>()); border.add_vertex((Vec3f)(b + off).cast<float>());
|
||||
border.add_vertex((Vec3f)(b - off).cast<float>()); border.add_vertex((Vec3f)(a - off).cast<float>());
|
||||
border.add_triangle(bb, bb + 1, bb + 2); border.add_triangle(bb, bb + 2, bb + 3); bb += 4;
|
||||
}
|
||||
|
||||
// 4 edge-midpoint handle squares.
|
||||
const Vec2d hpos[4] = { Vec2d(hu, 0), Vec2d(-hu, 0), Vec2d(0, hv), Vec2d(0, -hv) };
|
||||
GLModel::Geometry handles; handles.format = { EPT::Triangles, EVL::P3 };
|
||||
unsigned int hb = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const Vec3d ctr = p.to_world(hpos[i]);
|
||||
const Vec3d q0 = ctr - right * hs - up * hs, q1 = ctr + right * hs - up * hs,
|
||||
q2 = ctr + right * hs + up * hs, q3 = ctr - right * hs + up * hs;
|
||||
handles.add_vertex((Vec3f)q0.cast<float>()); handles.add_vertex((Vec3f)q1.cast<float>());
|
||||
handles.add_vertex((Vec3f)q2.cast<float>()); handles.add_vertex((Vec3f)q3.cast<float>());
|
||||
handles.add_triangle(hb, hb + 1, hb + 2); handles.add_triangle(hb, hb + 2, hb + 3); hb += 4;
|
||||
}
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
if (bb > 0) {
|
||||
GLModel m; m.init_from(std::move(border));
|
||||
m.set_color(ColorRGBA(1.0f, 0.62f, 0.16f, 0.9f)); // CAD amber
|
||||
m.render();
|
||||
}
|
||||
if (hb > 0) {
|
||||
GLModel m; m.init_from(std::move(handles));
|
||||
m.set_color(ColorRGBA(1.0f, 0.72f, 0.28f, 1.0f));
|
||||
m.render();
|
||||
}
|
||||
|
||||
// Offset arrow: a camera-facing ribbon from the base origin along the base normal to the
|
||||
// datum origin, with a grabbable square at the tip. Drag the tip to set the offset distance.
|
||||
if (m_dz_offset_on) {
|
||||
const Vec3d tip = m_dz_anchor + m_dz_normal * m_dz_offset;
|
||||
Vec3d off = m_dz_normal.cross(vd);
|
||||
if (off.norm() < 1e-9) off = m_dz_normal.cross(up);
|
||||
GLModel::Geometry shaft; shaft.format = { EPT::Triangles, EVL::P3 };
|
||||
if (off.norm() > 1e-9 && (tip - m_dz_anchor).norm() > 1e-9) {
|
||||
off.normalize(); off *= hw;
|
||||
shaft.add_vertex((Vec3f)(m_dz_anchor + off).cast<float>());
|
||||
shaft.add_vertex((Vec3f)(tip + off).cast<float>());
|
||||
shaft.add_vertex((Vec3f)(tip - off).cast<float>());
|
||||
shaft.add_vertex((Vec3f)(m_dz_anchor - off).cast<float>());
|
||||
shaft.add_triangle(0, 1, 2); shaft.add_triangle(0, 2, 3);
|
||||
GLModel sm; sm.init_from(std::move(shaft));
|
||||
sm.set_color(ColorRGBA(0.30f, 0.78f, 1.0f, 0.95f)); // cyan offset axis
|
||||
sm.render();
|
||||
}
|
||||
GLModel::Geometry tipsq; tipsq.format = { EPT::Triangles, EVL::P3 };
|
||||
const Vec3d t0 = tip - right * hs - up * hs, t1 = tip + right * hs - up * hs,
|
||||
t2 = tip + right * hs + up * hs, t3 = tip - right * hs + up * hs;
|
||||
tipsq.add_vertex((Vec3f)t0.cast<float>()); tipsq.add_vertex((Vec3f)t1.cast<float>());
|
||||
tipsq.add_vertex((Vec3f)t2.cast<float>()); tipsq.add_vertex((Vec3f)t3.cast<float>());
|
||||
tipsq.add_triangle(0, 1, 2); tipsq.add_triangle(0, 2, 3);
|
||||
GLModel tm; tm.init_from(std::move(tipsq));
|
||||
tm.set_color(ColorRGBA(0.45f, 0.86f, 1.0f, 1.0f));
|
||||
tm.render();
|
||||
}
|
||||
}
|
||||
|
||||
bool DesignSketchTool::hit_test_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const
|
||||
{
|
||||
if (!m_dz_active) return false;
|
||||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const double tol = 9.0 / std::max(cam.get_zoom(), 1e-6); // ~9 px in world units
|
||||
const double hu = m_dz_usize * 0.5, hv = m_dz_vsize * 0.5;
|
||||
const Vec2d hpos[4] = { Vec2d(hu, 0), Vec2d(-hu, 0), Vec2d(0, hv), Vec2d(0, -hv) };
|
||||
double best = tol; which = -1;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const Vec3d pt = m_dz_plane.to_world(hpos[i]);
|
||||
const Vec3d w = pt - ro;
|
||||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||||
const double d = (w - rd * t).norm();
|
||||
if (d < best) { best = d; which = i; }
|
||||
}
|
||||
if (m_dz_offset_on) { // offset arrow tip = handle 4
|
||||
const Vec3d pt = m_dz_anchor + m_dz_normal * m_dz_offset;
|
||||
const Vec3d w = pt - ro;
|
||||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||||
const double d = (w - rd * t).norm();
|
||||
if (d < best) { best = d; which = 4; }
|
||||
}
|
||||
return which >= 0;
|
||||
}
|
||||
|
||||
// Drag a handle: closest point of the mouse ray to the plane axis (u or v) through the origin,
|
||||
// |param| -> new half-extent, doubled to the full size.
|
||||
void DesignSketchTool::drag_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||||
{
|
||||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||||
if (which == 4) { // offset arrow: drag along base normal
|
||||
const Vec3d e = m_dz_normal; // signed offset, may go negative
|
||||
const Vec3d w0 = m_dz_anchor - ro;
|
||||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||||
const double denom = a * c - b * b;
|
||||
if (std::abs(denom) < 1e-7) return; // camera ∥ normal: leave offset as-is
|
||||
m_dz_offset = (b * ee - c * dd) / denom; // signed distance along the normal
|
||||
m_dz_plane.origin = m_dz_anchor + m_dz_normal * m_dz_offset; // rectangle follows live
|
||||
if (on_datum_offset_changed) on_datum_offset_changed(m_dz_offset);
|
||||
return;
|
||||
}
|
||||
const bool uaxis = (which == 0 || which == 1);
|
||||
const Vec3d e = (uaxis ? m_dz_plane.x_axis : m_dz_plane.y_axis).normalized();
|
||||
const Vec3d base = m_dz_plane.origin;
|
||||
const Vec3d w0 = base - ro;
|
||||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||||
const double denom = a * c - b * b;
|
||||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave size as-is
|
||||
const double s = (b * ee - c * dd) / denom; // signed coord along e of closest pt
|
||||
const double size = std::max(2.0, 2.0 * std::abs(s));
|
||||
if (uaxis) m_dz_usize = size; else m_dz_vsize = size;
|
||||
if (on_datum_size_changed) on_datum_size_changed(m_dz_usize, m_dz_vsize);
|
||||
}
|
||||
|
||||
// ---- Reference/base planes (Onshape-style default planes) -----------------------------
|
||||
void DesignSketchTool::set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||||
std::vector<std::string> labels)
|
||||
{
|
||||
m_dbp_active = !planes.empty();
|
||||
m_dbp_planes = std::move(planes);
|
||||
m_dbp_base = std::move(bases);
|
||||
m_dbp_labels = std::move(labels);
|
||||
if (m_dbp_hover >= int(m_dbp_planes.size())) m_dbp_hover = -1;
|
||||
}
|
||||
|
||||
void DesignSketchTool::clear_base_pick()
|
||||
{
|
||||
m_dbp_active = false;
|
||||
m_dbp_planes.clear();
|
||||
m_dbp_base.clear();
|
||||
m_dbp_labels.clear();
|
||||
m_dbp_hover = -1;
|
||||
}
|
||||
|
||||
// Reference planes are larger than the bed (Onshape default-plane feel). Half-extent = 0.6 * the
|
||||
// bed's larger side, so the square fully overhangs the print area. Falls back to 150mm if the bed
|
||||
// isn't queryable yet.
|
||||
double DesignSketchTool::dbp_half_extent() const
|
||||
{
|
||||
double half = 150.0;
|
||||
if (auto* pl = wxGetApp().plater()) {
|
||||
const BoundingBoxf bb = pl->build_volume().bounding_volume2d();
|
||||
const double w = bb.max.x() - bb.min.x(), d = bb.max.y() - bb.min.y();
|
||||
if (w > 1.0 && d > 1.0) half = 0.6 * std::max(w, d);
|
||||
}
|
||||
return half;
|
||||
}
|
||||
|
||||
// Draw the reference planes as large translucent labelled squares; the hovered one brightens.
|
||||
void DesignSketchTool::render_base_pick()
|
||||
{
|
||||
if (!m_dbp_active || m_dbp_planes.empty()) return;
|
||||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||||
using EVL = GLModel::Geometry::EVertexLayout;
|
||||
const double H = dbp_half_extent();
|
||||
// Onshape-ish per-plane tints: XY blue, XZ green, YZ red (keyed by base index 0/1/2; datums grey).
|
||||
auto tint = [](int base, bool hot) -> ColorRGBA {
|
||||
float a = hot ? 0.10f : 0.047f; // base planes kept faint (reduced ~2/3 from 0.30/0.14)
|
||||
if (base == 0) return ColorRGBA(0.30f, 0.55f, 0.95f, a);
|
||||
if (base == 1) return ColorRGBA(0.35f, 0.80f, 0.45f, a);
|
||||
if (base == 2) return ColorRGBA(0.92f, 0.42f, 0.42f, a);
|
||||
return ColorRGBA(0.70f, 0.72f, 0.78f, a);
|
||||
};
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
glsafe(::glDisable(GL_CULL_FACE));
|
||||
glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
const SketchPlane saved_plane = m_plane;
|
||||
for (size_t i = 0; i < m_dbp_planes.size(); ++i) {
|
||||
const SketchPlane& p = m_dbp_planes[i];
|
||||
const Vec3d q0 = p.to_world(Vec2d(-H, -H)), q1 = p.to_world(Vec2d(H, -H)),
|
||||
q2 = p.to_world(Vec2d(H, H)), q3 = p.to_world(Vec2d(-H, H));
|
||||
GLModel::Geometry quad; quad.format = { EPT::Triangles, EVL::P3 };
|
||||
quad.add_vertex((Vec3f)q0.cast<float>()); quad.add_vertex((Vec3f)q1.cast<float>());
|
||||
quad.add_vertex((Vec3f)q2.cast<float>()); quad.add_vertex((Vec3f)q3.cast<float>());
|
||||
quad.add_triangle(0, 1, 2); quad.add_triangle(0, 2, 3);
|
||||
GLModel m; m.init_from(std::move(quad));
|
||||
const bool hot = (int(i) == m_dbp_hover);
|
||||
const int base = (i < m_dbp_base.size()) ? m_dbp_base[i] : -1;
|
||||
m.set_color(tint(base, hot));
|
||||
m.render();
|
||||
|
||||
// Label near the top-left corner, drawn in the plane (draw_text lifts through m_plane).
|
||||
if (i < m_dbp_labels.size() && !m_dbp_labels[i].empty()) {
|
||||
m_plane = p;
|
||||
const double th = H * 0.10;
|
||||
const ColorRGBA lc = tint(base, true); ColorRGBA lcs(lc.r(), lc.g(), lc.b(), 1.0f);
|
||||
draw_text(m_line_model, m_dbp_labels[i], Vec2d(-H + th * 2.0, H - th * 1.6), th, lcs);
|
||||
}
|
||||
}
|
||||
m_plane = saved_plane; // draw_text renders each label immediately (draw_strokes self-renders)
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
// Ray-pick the reference planes: intersect the mouse ray with each plane, keep hits inside the
|
||||
// square, return the index of the nearest by |t|. -1 on miss.
|
||||
int DesignSketchTool::hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||||
{
|
||||
if (!m_dbp_active) return -1;
|
||||
const double H = dbp_half_extent();
|
||||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||||
int best = -1; double best_t = 1e30;
|
||||
for (size_t i = 0; i < m_dbp_planes.size(); ++i) {
|
||||
const SketchPlane& p = m_dbp_planes[i];
|
||||
const double dn = rd.dot(p.normal);
|
||||
if (std::abs(dn) < 1e-9) continue; // ray parallel to plane
|
||||
const double t = (p.origin - ro).dot(p.normal) / dn;
|
||||
if (t < 0) continue; // behind the camera
|
||||
const Vec3d hit = ro + rd * t;
|
||||
const Vec3d d = hit - p.origin;
|
||||
if (std::abs(d.dot(p.x_axis)) > H || std::abs(d.dot(p.y_axis)) > H) continue;
|
||||
if (t < best_t) { best_t = t; best = int(i); }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// ---- Move-body gizmo (M5) -------------------------------------------------------------
|
||||
void DesignSketchTool::set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform)
|
||||
{
|
||||
@@ -3365,29 +3718,6 @@ void DesignSketchTool::render_hole_gizmo()
|
||||
m_plane = saved;
|
||||
}
|
||||
|
||||
// (1b) #2 Part B: construction-line dimensions positioning the hole from the face SIDES — a
|
||||
// horizontal leg from the u-side (umin = one edge) and a vertical leg from the v-side (vmin =
|
||||
// the adjacent edge) to the hole centre, each with an editable distance label (what users ask
|
||||
// for: distance from the sides, not x/y from the centre). Falls back to the datum (0,0) when no
|
||||
// face bounds are known (dropdown-plane hole). Drawn ON the plane, construction green.
|
||||
{
|
||||
m_plane = m_hl_plane;
|
||||
const ColorRGBA con(0.55f, 0.85f, 0.55f, 1.0f);
|
||||
const double o = 16.0 * upp;
|
||||
const double ru = m_hl_has_bounds ? m_hl_umin : 0.0; // reference u-side (face edge / datum)
|
||||
const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0; // reference v-side (face edge / datum)
|
||||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||||
segs.emplace_back(Vec2d(ru, m_hl_y), Vec2d(m_hl_x, m_hl_y)); // from the u-side to the hole
|
||||
segs.emplace_back(Vec2d(m_hl_x, rv), Vec2d(m_hl_x, m_hl_y)); // from the v-side to the hole
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
draw_strokes(m_hl_stroke_model, segs, std::max(0.5 * upp, 1e-4), con);
|
||||
DimAnnot dx; dx.kind = DimType::Distance; dx.value = m_hl_x - ru; // distance from the u-side
|
||||
draw_text(m_line_model, dim_text(dx), Vec2d((ru + m_hl_x) * 0.5, m_hl_y - o), th, con);
|
||||
DimAnnot dy; dy.kind = DimType::Distance; dy.value = m_hl_y - rv; // distance from the v-side
|
||||
draw_text(m_line_model, dim_text(dy), Vec2d(m_hl_x + o, (rv + m_hl_y) * 0.5), th, con);
|
||||
m_plane = saved;
|
||||
}
|
||||
|
||||
// (2) Billboarded handles (centre marker + diameter arrow + depth arrow), screen-facing frame
|
||||
// at the centre so draw_strokes/draw_text read on top regardless of orientation.
|
||||
SketchPlane bb; bb.origin = centre; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized();
|
||||
@@ -3424,19 +3754,35 @@ void DesignSketchTool::render_hole_gizmo()
|
||||
arrow_to(centre + nrm * L, blue, da);
|
||||
}
|
||||
|
||||
// Centre marker: a small billboarded square so the reposition handle is visible + grabbable.
|
||||
{
|
||||
const double s = 7.0 * upp;
|
||||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||||
segs.emplace_back(Vec2d(-s, -s), Vec2d(s, -s));
|
||||
segs.emplace_back(Vec2d( s, -s), Vec2d(s, s));
|
||||
segs.emplace_back(Vec2d( s, s), Vec2d(-s, s));
|
||||
segs.emplace_back(Vec2d(-s, s), Vec2d(-s, -s));
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
draw_strokes(m_hl_stroke_model, segs, std::max(0.7 * upp, 1e-4), amber);
|
||||
}
|
||||
|
||||
m_plane = saved;
|
||||
|
||||
// Move handle: a small 3D CUBE at the hole centre (Orca text/SVG-on-face feel) — grab and drag
|
||||
// it to slide the hole across the face. Built from the plane axes so it sits flat on the face.
|
||||
{
|
||||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||||
using EVL = GLModel::Geometry::EVertexLayout;
|
||||
const double hs = 9.0 * upp; // cube half-size (~9 px); hit-test uses the same below
|
||||
const Vec3d U = ddir * hs, V = m_hl_plane.y_axis.normalized() * hs, Nn = nrm * hs;
|
||||
// Cube centred EXACTLY on the surface point (= the hole). Depth-test ON occludes the inner
|
||||
// half inside the solid, so the visible half-cube reads as planted at the hole — no depth-off
|
||||
// float that looked offset from the on-surface footprint. Hit-test targets the same `centre`.
|
||||
Vec3d c8[8];
|
||||
for (int i = 0; i < 8; ++i)
|
||||
c8[i] = centre + ((i & 1) ? U : -U) + ((i & 2) ? V : -V) + ((i & 4) ? Nn : -Nn);
|
||||
// 6 faces (CCW), each as 2 triangles, lightly shaded so the box reads as a cube.
|
||||
const int faces[6][4] = { {0,1,3,2},{4,6,7,5},{0,4,5,1},{2,3,7,6},{0,2,6,4},{1,5,7,3} };
|
||||
const float shade[6] = { 0.78f, 1.0f, 0.86f, 0.92f, 0.70f, 0.96f };
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
glsafe(::glDisable(GL_CULL_FACE));
|
||||
for (int f = 0; f < 6; ++f) {
|
||||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||||
for (int k = 0; k < 4; ++k) g.add_vertex((Vec3f)c8[faces[f][k]].cast<float>());
|
||||
g.add_triangle(0, 1, 2); g.add_triangle(0, 2, 3);
|
||||
GLModel m; m.init_from(std::move(g));
|
||||
m.set_color(ColorRGBA(1.0f * shade[f], 0.62f * shade[f], 0.16f * shade[f], 1.0f));
|
||||
m.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Best-matching hole handle under the cursor: centre (0) / diameter (1) / depth (2), or -1.
|
||||
@@ -3456,11 +3802,10 @@ int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEven
|
||||
const double depL = std::max(m_hl_depth, 40.0 * upp);
|
||||
const double tol = 12.0 * upp;
|
||||
|
||||
// Centre wins at the shared base: all three handles spring from the centre, so a grab within
|
||||
// tolerance of the centre point is a reposition (the arrows are only grabbable along the shaft
|
||||
// that extends outward from here). This also keeps an edge-on arrow — e.g. the depth arrow in
|
||||
// top view, which collapses onto the centre — from stealing the reposition grab.
|
||||
if (ray_segment_dist3(ro, rd, centre, centre) <= tol) return 0;
|
||||
// Centre wins at the shared base: the move cube straddles the centre, so a grab within the cube
|
||||
// half-size is a reposition. The arrows are only grabbable along the shaft extending outward,
|
||||
// which also keeps an edge-on arrow (e.g. depth in top view) from stealing the reposition grab.
|
||||
if (ray_segment_dist3(ro, rd, centre, centre) <= 9.0 * upp) return 0; // cube half-size
|
||||
|
||||
int best = -1; double bestd = tol;
|
||||
const double dD = ray_segment_dist3(ro, rd, centre, centre + ddir * rad);
|
||||
@@ -3469,20 +3814,7 @@ int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEven
|
||||
const double dZ = ray_segment_dist3(ro, rd, centre, centre + nrm * depL);
|
||||
if (dZ < bestd) { bestd = dZ; best = 2; }
|
||||
}
|
||||
if (best >= 0) return best;
|
||||
|
||||
// #2 Part B: the X/Y construction-dim labels (edit-only) — only when no arrow handle was hit.
|
||||
// Larger tolerance since they are text; positions mirror render_hole_gizmo's label offsets.
|
||||
const double o = 16.0 * upp, ltol = 16.0 * upp;
|
||||
const double ru = m_hl_has_bounds ? m_hl_umin : 0.0;
|
||||
const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0;
|
||||
const Vec3d xlbl = m_hl_plane.to_world(Vec2d((ru + m_hl_x) * 0.5, m_hl_y - o));
|
||||
const Vec3d ylbl = m_hl_plane.to_world(Vec2d(m_hl_x + o, (rv + m_hl_y) * 0.5));
|
||||
const double dXl = ray_segment_dist3(ro, rd, xlbl, xlbl);
|
||||
const double dYl = ray_segment_dist3(ro, rd, ylbl, ylbl);
|
||||
if (dXl <= ltol && dXl <= dYl) return 3;
|
||||
if (dYl <= ltol) return 4;
|
||||
return -1;
|
||||
return best;
|
||||
}
|
||||
|
||||
// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along
|
||||
@@ -4541,6 +4873,21 @@ void glyph_strokes(char c, std::vector<std::pair<Vec2d, Vec2d>>& out, double& ad
|
||||
poly(out, {Vec2d(0.30, 0.50), Vec2d(0.58, 0.0)});
|
||||
advance = 0.80;
|
||||
break;
|
||||
case 'X':
|
||||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.58, 0.0)});
|
||||
poly(out, {Vec2d(0.58, 1.0), Vec2d(0.06, 0.0)});
|
||||
advance = 0.72;
|
||||
break;
|
||||
case 'Y':
|
||||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.32, 0.52)});
|
||||
poly(out, {Vec2d(0.58, 1.0), Vec2d(0.32, 0.52)});
|
||||
poly(out, {Vec2d(0.32, 0.52), Vec2d(0.32, 0.0)});
|
||||
advance = 0.72;
|
||||
break;
|
||||
case 'Z':
|
||||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.58, 1.0), Vec2d(0.06, 0.0), Vec2d(0.58, 0.0)});
|
||||
advance = 0.72;
|
||||
break;
|
||||
case ' ':
|
||||
advance = 0.5;
|
||||
break;
|
||||
@@ -4734,6 +5081,8 @@ void DesignSketchTool::render_live_quotes(double unit_per_px)
|
||||
m_live_rrect_w_label = m_live_rrect_h_label = m_live_rrect_r_label = Vec2d(1e18, 1e18);
|
||||
m_live_aslot_fi = -1;
|
||||
m_live_aslot_r_label = m_live_aslot_w_label = Vec2d(1e18, 1e18);
|
||||
m_live_slot_fi = -1;
|
||||
m_live_slot_len_label = m_live_slot_w_label = m_live_slot_angle_label = Vec2d(1e18, 1e18);
|
||||
// Edit-op tools (Fillet/Chamfer/Offset/Mirror) put their picks in m_selection for the
|
||||
// highlight, but their own arrow/label gizmo is the value affordance — don't also draw
|
||||
// the picked entity's characteristic quotes (Length/Angle/…) or the view gets cluttered.
|
||||
@@ -4790,24 +5139,33 @@ void DesignSketchTool::render_live_quotes(double unit_per_px)
|
||||
break;
|
||||
}
|
||||
case FeatureKind::Slot: {
|
||||
// make_slot order: [top line, cap@c1, bottom line, cap@c0]. Centre-distance =
|
||||
// Distance between the two cap-arc centres; Width = cap Radius (half-width).
|
||||
const int cap_c1 = f.begin + 1, cap_c0 = f.begin + 3;
|
||||
if (cap_c0 < int(m_entities.size()) && cap_c1 < int(m_entities.size())) {
|
||||
DimAnnot dst; dst.kind = DimType::Distance;
|
||||
dst.ea = cap_c0; dst.ra = SketchPointRole::Center;
|
||||
dst.eb = cap_c1; dst.rb = SketchPointRole::Center;
|
||||
// Push the centre-distance label clear ABOVE the slot (past the cap
|
||||
// half-width) so it sits outside the fillable face — otherwise clicking
|
||||
// it would hit the interior and trigger face-select. a.side scales the
|
||||
// quote offset (draw_dim_quote: off = side * max(L*0.18, 8)).
|
||||
const double Lc = (f.c1 - f.c0).norm();
|
||||
const double unit = std::max(Lc * 0.18, 8.0);
|
||||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||||
dst.side = (f.param + th * 2.5) / unit; // clear cap + label height
|
||||
protos.push_back(dst);
|
||||
DimAnnot rad; rad.kind = DimType::Radius; rad.ea = cap_c1; // width
|
||||
protos.push_back(rad);
|
||||
// Straight slot edits GEOMETRICALLY (like the arc-slot / rounded-rect), NOT through
|
||||
// the constraint-based scalar quotes. Its dims are the centreline LENGTH (distance
|
||||
// between the two cap centres c0,c1) and the WIDTH (2*param). The old
|
||||
// Distance-between-arc-centres + Radius quotes never registered as editable, so the
|
||||
// labels did nothing on click (nde #6). Draw both labels clear of the fillable face
|
||||
// and remember the feature so open_primary_autoedit / click-to-promote drive set_slot.
|
||||
// Slot dims: (1) inter-centre distance, (2) radius (= half-width), (3) centreline angle.
|
||||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||||
Vec2d u = f.c1 - f.c0;
|
||||
const double Lc = u.norm();
|
||||
if (Lc > 1e-9) {
|
||||
u /= Lc;
|
||||
const Vec2d n(-u.y(), u.x());
|
||||
const double w = f.param;
|
||||
DimAnnot len; len.kind = DimType::Length; len.value = Lc;
|
||||
m_live_slot_len_label = 0.5 * (f.c0 + f.c1) + n * (w + th * 2.0);
|
||||
draw_text(m_line_model, dim_text(len), m_live_slot_len_label, th, dc);
|
||||
DimAnnot rd; rd.kind = DimType::Radius; rd.value = w;
|
||||
m_live_slot_w_label = f.c1 + u * (w + th * 2.0);
|
||||
draw_text(m_line_model, dim_text(rd), m_live_slot_w_label, th, dc);
|
||||
double deg = std::atan2(f.c1.y() - f.c0.y(), f.c1.x() - f.c0.x()) * 180.0 / M_PI;
|
||||
if (deg < 0.0) deg += 360.0;
|
||||
DimAnnot an; an.kind = DimType::Angle; an.value = deg;
|
||||
m_live_slot_angle_label = f.c0 - u * (w + th * 2.0);
|
||||
draw_text(m_line_model, dim_text(an), m_live_slot_angle_label, th, dc);
|
||||
m_live_slot_fi = fi;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -4938,7 +5296,7 @@ void DesignSketchTool::render_live_quotes(double unit_per_px)
|
||||
}
|
||||
|
||||
if (protos.empty() && m_live_poly_fi < 0 && m_live_rrect_fi < 0 &&
|
||||
m_live_aslot_fi < 0) { // ungrouped single entity
|
||||
m_live_aslot_fi < 0 && m_live_slot_fi < 0) { // ungrouped single entity
|
||||
switch (e.type) {
|
||||
case SketchEntity::Type::Line: {
|
||||
DimAnnot len; len.kind = DimType::Length; len.ea = ei; len.side = 1.0; protos.push_back(len);
|
||||
@@ -4962,7 +5320,7 @@ void DesignSketchTool::render_live_quotes(double unit_per_px)
|
||||
// GEOMETRIC edit (SLVS angle constraints are line-to-line). A wedge spans the arc's
|
||||
// start->end angles just OUTSIDE the radius; its label shows the included angle and is
|
||||
// clickable to type a new sweep. The radius quote is still emitted via `protos`.
|
||||
if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 && m_live_aslot_fi < 0 &&
|
||||
if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 && m_live_aslot_fi < 0 && m_live_slot_fi < 0 &&
|
||||
e.type == SketchEntity::Type::Arc && e.radius > 1e-6) {
|
||||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||||
@@ -5999,6 +6357,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
|
||||
if (!m_active) {
|
||||
render_datum_planes();
|
||||
render_solid_highlight();
|
||||
if (m_dbp_active) render_base_pick();
|
||||
if (m_dz_active) render_datum_gizmo();
|
||||
if (m_ex_active) render_extrude_gizmo();
|
||||
if (m_mv_active) render_move_gizmo();
|
||||
if (m_fl_active) render_fillet_gizmo();
|
||||
@@ -6749,6 +7109,33 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Datum-plane resize gizmo (C3): while the Plane card is open the 4 edge handles are
|
||||
// grabbable — drag changes the u/v extent live. A LeftDown that misses falls through.
|
||||
if (m_dz_active) {
|
||||
if (m_dz_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||||
drag_datum_handle(canvas, evt, m_dz_drag);
|
||||
return true;
|
||||
}
|
||||
if (evt.LeftUp() && m_dz_drag >= 0) { m_dz_drag = -1; return true; }
|
||||
if (evt.LeftDown()) {
|
||||
int which = -1;
|
||||
if (hit_test_datum_handle(canvas, evt, which)) {
|
||||
m_dz_drag = which; m_dz_press_x = evt.GetX(); m_dz_press_y = evt.GetY();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Datum base picker: HOVER highlight only here. The CLICK is handled at the very end of the
|
||||
// selection fall-through (below), so picking existing geometry (committed sketch loops,
|
||||
// solid faces/edges) always wins over a base-plane click — the planes never block selection.
|
||||
if (m_dbp_active && evt.Moving() && !evt.LeftIsDown()) {
|
||||
const int h = hit_test_base_pick(canvas, evt);
|
||||
if (h != m_dbp_hover) {
|
||||
m_dbp_hover = h;
|
||||
canvas.set_as_dirty();
|
||||
if (h >= 0) return true; // caller render()s on true -> hover repaints on software GL
|
||||
}
|
||||
}
|
||||
if (m_ex_active) {
|
||||
if (m_ex_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||||
drag_extrude_arrow(canvas, evt, m_ex_drag);
|
||||
@@ -6922,6 +7309,14 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||||
return true;
|
||||
}
|
||||
m_display_pick = -1; m_display_pick_region = -1; // clicked bare plate -> drop highlight
|
||||
// Last resort: a click that hit no geometry but landed on a reference/base plane picks it.
|
||||
if (m_dbp_active) {
|
||||
const int h = hit_test_base_pick(canvas, evt);
|
||||
if (h >= 0 && h < int(m_dbp_base.size())) {
|
||||
if (on_datum_base_picked) on_datum_base_picked(m_dbp_base[h]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // let the stock canvas orbit / deselect
|
||||
}
|
||||
|
||||
@@ -6938,7 +7333,16 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||||
if (apply_live_trim(p, tol * 3.0, m_mode == Mode::Extend)) resolve_live();
|
||||
if (apply_live_trim(p, tol * 3.0, m_mode == Mode::Extend)) {
|
||||
resolve_live();
|
||||
} else if (on_readout) {
|
||||
// nde #15: don't fail silently. The pick found nothing to cut/extend — either
|
||||
// the click missed every live segment, or the picked segment has no crossing /
|
||||
// target among the OTHER live entities (committed sketches aren't trimmed).
|
||||
on_readout(m_mode == Mode::Extend
|
||||
? std::string("Extend: click a line/arc that can reach another live entity")
|
||||
: std::string("Trim: click a segment where it crosses another live entity"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (evt.RightDown()) { request_exit(); return true; }
|
||||
@@ -7279,6 +7683,11 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||||
if ((m_live_aslot_r_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, true); return true; }
|
||||
if ((m_live_aslot_w_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, false); return true; }
|
||||
}
|
||||
if (m_live_slot_fi >= 0) {
|
||||
if ((m_live_slot_len_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 0); return true; }
|
||||
if ((m_live_slot_w_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 1); return true; }
|
||||
if ((m_live_slot_angle_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 2); return true; }
|
||||
}
|
||||
}
|
||||
|
||||
// A derived handle (the circle RadiusHandle — not an entity point, so
|
||||
|
||||
@@ -97,7 +97,8 @@ public:
|
||||
|| (m_solid_bodies != nullptr && !m_solid_bodies->empty())
|
||||
|| !m_datum_planes.empty()
|
||||
|| m_ex_active || m_mv_active || m_fl_active
|
||||
|| m_hl_active || m_th_active || m_sh_active; }
|
||||
|| m_hl_active || m_th_active || m_sh_active
|
||||
|| m_dz_active || m_dbp_active; }
|
||||
|
||||
// Solid topology selection on the committed bodies: clicking a solid cycles
|
||||
// whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With
|
||||
@@ -153,6 +154,26 @@ public:
|
||||
// (new_depth, second_side): second_side=false drives the primary depth, true the 2nd side.
|
||||
std::function<void(double depth, bool second)> on_extrude_depth_changed;
|
||||
|
||||
// Datum-plane resize gizmo (C3). The Plane tool is a DesignPanel docked card (sketch tool
|
||||
// NOT active), so the panel resolves the candidate plane's frame + current u/v extent and
|
||||
// feeds them here; the tool draws the rectangle outline + 4 edge-midpoint handles. Dragging a
|
||||
// handle changes the u (left/right) or v (top/bottom) extent live and fires on_datum_size_changed
|
||||
// back to the panel, which writes the Size spins + re-pushes the rendered datum.
|
||||
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||||
double offset, bool offset_on);
|
||||
void clear_datum_gizmo();
|
||||
std::function<void(double usize, double vsize)> on_datum_size_changed;
|
||||
std::function<void(double offset)> on_datum_offset_changed;
|
||||
|
||||
// Graphical base/origin pick: while the Plane card is open, the candidate base planes
|
||||
// (XY/XZ/YZ origin planes + existing datums) draw as translucent clickable ghosts. A click
|
||||
// on one fires on_datum_base_picked(base) with that plane's base index (0/1/2 or 3+N).
|
||||
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||||
std::vector<std::string> labels = {});
|
||||
void clear_base_pick();
|
||||
std::function<void(int base)> on_datum_base_picked;
|
||||
|
||||
// Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch
|
||||
// tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid
|
||||
// + current radius and the tool anchors a world-space radius arrow at the picked edge midpoint
|
||||
@@ -203,7 +224,9 @@ public:
|
||||
// Datum/reference planes (Plane feature) carry no solid; the panel feeds their resolved
|
||||
// SketchPlanes so they render as translucent rectangles in feature mode (otherwise a
|
||||
// Plane feature is invisible in the canvas).
|
||||
void set_datum_planes(std::vector<SketchPlane> planes) { m_datum_planes = std::move(planes); }
|
||||
void set_datum_planes(std::vector<SketchPlane> planes, std::vector<Vec2d> sizes = {}) {
|
||||
m_datum_planes = std::move(planes); m_datum_sizes = std::move(sizes);
|
||||
}
|
||||
|
||||
// Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X,
|
||||
// 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the
|
||||
@@ -575,6 +598,10 @@ private:
|
||||
// Arc-slot grouped edit: centreline-radius + width labels rebuild the 4-arc span.
|
||||
void open_arc_slot_editor(int fi, bool radius); // true=centreline R, false=width
|
||||
void set_arc_slot(int fi, double Rc, double w);
|
||||
// Straight-slot grouped edit: centreline-length + width labels rebuild the 4-entity span.
|
||||
void open_slot_editor(int fi, int which); // 0=inter-centre distance, 1=radius, 2=angle
|
||||
void set_slot(int fi, double length, double w);
|
||||
void set_slot_angle(int fi, double deg); // rotate the centreline about c0, keep len+radius
|
||||
// Grouped derived-handle drag: resize an axis-aligned rect by a corner (opposite corner
|
||||
// fixed); move a slot end by its cap centre. Both rebuild the feature span geometrically.
|
||||
void drag_rect_corner(int fi, const Vec2d& cursor);
|
||||
@@ -699,6 +726,10 @@ private:
|
||||
Vec2d m_live_aslot_r_label{0,0}; // arc-slot centreline-radius label
|
||||
Vec2d m_live_aslot_w_label{0,0}; // arc-slot width label
|
||||
int m_live_aslot_fi{-1}; // the arc-slot Feature (rebuild edits)
|
||||
Vec2d m_live_slot_len_label{0,0}; // straight-slot inter-centre distance label
|
||||
Vec2d m_live_slot_w_label{0,0}; // straight-slot radius (half-width) label
|
||||
Vec2d m_live_slot_angle_label{0,0}; // straight-slot centreline angle label
|
||||
int m_live_slot_fi{-1}; // the straight-slot Feature (rebuild edits)
|
||||
std::vector<Feature> m_features; // parametric groups over m_entities
|
||||
int m_open_feature{-1}; // index of the Feature being built, or -1
|
||||
|
||||
@@ -810,6 +841,7 @@ private:
|
||||
void render_solid_highlight();
|
||||
void render_datum_planes(); // translucent rectangles for datum/reference planes
|
||||
std::vector<SketchPlane> m_datum_planes;
|
||||
std::vector<Vec2d> m_datum_sizes; // per-plane (u,v) full extent; empty -> default
|
||||
GLModel m_solid_face_model;
|
||||
GLModel m_solid_edge_model;
|
||||
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
|
||||
@@ -830,6 +862,30 @@ private:
|
||||
void open_extrude_editor(int which);
|
||||
GLModel m_ex_arrow_model;
|
||||
|
||||
// Datum-plane resize gizmo state (C3). GUI-only; fed by the panel while the Plane card is open.
|
||||
bool m_dz_active{false};
|
||||
SketchPlane m_dz_plane; // resolved datum frame (origin + axes)
|
||||
double m_dz_usize{60.0}; // current u extent (full width)
|
||||
double m_dz_vsize{60.0}; // current v extent (full height)
|
||||
int m_dz_drag{-1}; // 0=+u,1=-u,2=+v,3=-v handle, 4=offset tip, -1 none
|
||||
int m_dz_press_x{0}, m_dz_press_y{0};
|
||||
Vec3d m_dz_anchor{Vec3d::Zero()}; // base-plane origin (offset arrow tail)
|
||||
Vec3d m_dz_normal{0.0, 0.0, 1.0}; // base normal (offset arrow direction)
|
||||
double m_dz_offset{0.0}; // current signed offset along the base normal
|
||||
bool m_dz_offset_on{false}; // draw/allow the offset arrow (Offset-from-base only)
|
||||
void render_datum_gizmo();
|
||||
bool hit_test_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
|
||||
void drag_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
|
||||
// Datum base picker (translucent clickable origin/datum planes)
|
||||
bool m_dbp_active{false};
|
||||
std::vector<SketchPlane> m_dbp_planes;
|
||||
std::vector<int> m_dbp_base;
|
||||
std::vector<std::string> m_dbp_labels;
|
||||
int m_dbp_hover{-1};
|
||||
double dbp_half_extent() const; // bed-derived: reference planes are larger than the bed
|
||||
void render_base_pick();
|
||||
int hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
|
||||
|
||||
// Move-body gizmo state: 3 world-axis translate arrows + 3 world-axis rotate rings.
|
||||
// Delta model: offset/rot are deltas about a fixed pivot, composed onto m_mv_base_xform
|
||||
// (the body's pose when Move opened) so rotation works even on an already-placed body.
|
||||
|
||||
@@ -7940,7 +7940,14 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
*/
|
||||
//bool show_texture = true;
|
||||
//BBS set axes mode
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
if (m_axes_at_bed_center) {
|
||||
// SnapOrca Design: triad at the bed centre = modeling origin (set every frame because
|
||||
// set_shape/set_axes_mode otherwise reset it to the bed corner).
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
|
||||
@@ -545,6 +545,9 @@ private:
|
||||
mutable float m_paint_toolbar_width;
|
||||
bool m_collapse_toolbar_enabled{true};
|
||||
bool m_plate_chrome_enabled{true};
|
||||
// SnapOrca Design: render the world-axis triad at the bed centre (= modeling origin) instead of
|
||||
// the bed corner. Default false preserves the main editor's corner triad.
|
||||
bool m_axes_at_bed_center{false};
|
||||
DesignSketchTool* m_design_sketch_tool{nullptr};
|
||||
|
||||
//BBS: add canvas type for assemble view usage
|
||||
@@ -885,6 +888,7 @@ public:
|
||||
void enable_separator_toolbar(bool enable);
|
||||
void enable_collapse_toolbar(bool enable);
|
||||
void enable_plate_chrome(bool enable);
|
||||
void set_axes_at_bed_center(bool b) { m_axes_at_bed_center = b; }
|
||||
void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
|
||||
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
|
||||
void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; }
|
||||
|
||||
@@ -1516,3 +1516,90 @@ TEST_CASE("re-edit: multi-type timeline replays all downstream features", "[CadD
|
||||
Vec3d sz1 = doc.display_mesh.bounding_box().size();
|
||||
REQUIRE(sz1.z() > sz0.z() + 1.0);
|
||||
}
|
||||
|
||||
TEST_CASE("datum plane construction methods", "[CadDocument][plane]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// Build a doc with a box (sketch rect 40x30 + extrude 20) so bodies[0] has faces/edges.
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 40, 30, 0, "BoxSketch");
|
||||
REQUIRE(sk >= 0);
|
||||
doc.add_extrude(sk, 20.0, false, BooleanMode::New, "Extrude1");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.bodies.size() == 1);
|
||||
const int n_faces = GeometryEngine::face_count(doc.bodies[0].shape);
|
||||
REQUIRE(n_faces == 6); // a box
|
||||
const int n_edges = GeometryEngine::edge_count(doc.bodies[0].shape);
|
||||
REQUIRE(n_edges == 12);
|
||||
|
||||
// Find top face (normal ~ +Z) and bottom face (normal ~ -Z) by scanning.
|
||||
int top_idx = -1, bot_idx = -1;
|
||||
for (int i = 0; i < n_faces; ++i) {
|
||||
TopoDS_Face fc = GeometryEngine::face_by_index(doc.bodies[0].shape, i);
|
||||
Vec3d n = GeometryEngine::face_normal_world(fc);
|
||||
if (n.z() > 0.9) top_idx = i;
|
||||
if (n.z() < -0.9) bot_idx = i;
|
||||
}
|
||||
REQUIRE(top_idx >= 0);
|
||||
REQUIRE(bot_idx >= 0);
|
||||
|
||||
// --- Offset from base XY by 10 ---
|
||||
auto planes0 = doc.resolve_datum_planes();
|
||||
size_t initial = planes0.size();
|
||||
|
||||
CadFeature f0;
|
||||
f0.type = CadFeatureType::Plane;
|
||||
f0.name = "Offset10";
|
||||
f0.plane_base = 0; // XY
|
||||
f0.plane_type = PlaneType::Offset;
|
||||
f0.plane_offset = 10;
|
||||
doc.features.push_back(f0);
|
||||
|
||||
auto planes = doc.resolve_datum_planes();
|
||||
REQUIRE(planes.size() == initial + 1);
|
||||
CHECK_THAT(planes.back().second.origin.z(), WithinAbs(10.0, 1e-6));
|
||||
CHECK_THAT(planes.back().second.normal.z(), WithinAbs(1.0, 1e-6));
|
||||
|
||||
// --- Coincident to top face ---
|
||||
CadFeature f1;
|
||||
f1.type = CadFeatureType::Plane;
|
||||
f1.name = "CoincidentTop";
|
||||
f1.plane_type = PlaneType::Coincident;
|
||||
f1.plane_face_body = 0;
|
||||
f1.plane_face = top_idx;
|
||||
doc.features.push_back(f1);
|
||||
|
||||
planes = doc.resolve_datum_planes();
|
||||
REQUIRE(planes.size() == initial + 2);
|
||||
CHECK_THAT(planes.back().second.origin.z(), WithinAbs(20.0, 1e-6)); // box height is 20
|
||||
CHECK_THAT(planes.back().second.normal.z(), WithinAbs(1.0, 1e-6));
|
||||
|
||||
// --- Midplane between top and bottom faces ---
|
||||
CadFeature f2;
|
||||
f2.type = CadFeatureType::Plane;
|
||||
f2.name = "Midplane";
|
||||
f2.plane_type = PlaneType::Midplane;
|
||||
f2.plane_face_body = 0;
|
||||
f2.plane_face = top_idx;
|
||||
f2.plane_face2_body = 0;
|
||||
f2.plane_face2 = bot_idx;
|
||||
doc.features.push_back(f2);
|
||||
|
||||
planes = doc.resolve_datum_planes();
|
||||
REQUIRE(planes.size() == initial + 3);
|
||||
CHECK_THAT(planes.back().second.origin.z(), WithinAbs(10.0, 1e-6)); // midway
|
||||
CHECK_THAT(std::abs(planes.back().second.normal.z()), WithinAbs(1.0, 1e-6));
|
||||
|
||||
// --- Orthonormality check on all resolved planes ---
|
||||
for (const auto& [name, sp] : planes) {
|
||||
INFO("Plane: " << name);
|
||||
CHECK_THAT(sp.normal.norm(), WithinAbs(1.0, 1e-6));
|
||||
CHECK_THAT(sp.x_axis.norm(), WithinAbs(1.0, 1e-6));
|
||||
CHECK_THAT(sp.y_axis.norm(), WithinAbs(1.0, 1e-6));
|
||||
CHECK_THAT(std::abs(sp.x_axis.dot(sp.y_axis)), WithinAbs(0.0, 1e-6));
|
||||
CHECK_THAT(std::abs(sp.x_axis.dot(sp.normal)), WithinAbs(0.0, 1e-6));
|
||||
CHECK_THAT(std::abs(sp.y_axis.dot(sp.normal)), WithinAbs(0.0, 1e-6));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user