Design: in-canvas gizmos for Draft/Cut + operand highlight for Boolean/Sweep/Loft

Closes the gizmo-parity gap (bd snaporca-4h5): the five solid features that were
card-only now give in-canvas feedback like their siblings.

Draft — angle-arc drag gizmo (clone of the Revolve gizmo): once a side face is
picked, a cyan arc anchored at the face centroid shows the taper angle; drag the
tip or type the angle, the live ghost tapers with it. Axis = world +Z (the neutral
pull direction), clamped [-89, 89].

Cut — plane offset-arrow + cutting-plane rectangle (clone of the Shell arrow, adds
a wire rectangle in the cut plane sized to the target body bbox). The arrow drags
the signed offset along the plane normal; the rectangle rides at the cut position;
the ghost splits live. (Also covers bd snaporca-1gh / snaporca-mmr.)

Boolean / Sweep / Loft — operand highlighting (new by-index highlight infra):
- Boolean tints the target body teal-green and the tool body orange (per-index
  body tint added to the DesignCanvas GLVolume colour loop + set_operand_bodies).
- Sweep tints the profile sketch cyan and the path sketch magenta.
- Loft tints every selected profile sketch green.
  Sketch tints reuse the DisplaySketch overlay via a feature-index -> colour map
  (sketch_hl_color); DisplaySketch struct unchanged. All self-gate by active tool
  and clear on close_tool.

Wiring mirrors the existing gizmo pattern 1:1 (m_*_active / render_* / set_* /
clear_* / update_* in refresh_preview / DesignCanvas passthroughs / render dispatch
+ on_mouse drag branch). Both forks; DesignSketchTool.{cpp,hpp} + DesignCanvas.hpp
+ DesignPanel.hpp byte-identical across forks. Built clean on both. Draft, Cut and
Boolean highlight live-verified on :10; Sweep/Loft sketch tint is code-complete and
build-clean (visual check pending — needs hand-drawn profile/path sketches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
Tommaso Bianchi
2026-07-01 10:19:33 +02:00
co-authored by Claude Opus 4.8
parent 8cbf5b393b
commit 5a005d4728
6 changed files with 476 additions and 4 deletions
+54
View File
@@ -216,6 +216,8 @@ void DesignCanvas::reload(bool keep_view)
// Selection tint wins; otherwise the per-body override (Color tool) or the
// auto palette via body_color().
ColorRGBA c = m_body_selected ? sel_gold : body_color(b);
if (b == m_hl_body_target) c = ColorRGBA(0.30f, 0.90f, 0.70f, 1.0f); // target = teal-green
else if (b == m_hl_body_tool) c = ColorRGBA(1.00f, 0.55f, 0.15f, 1.0f); // tool = orange
if (m_body_translucent) c.a(0.30f);
v->set_color(c);
}
@@ -610,6 +612,44 @@ void DesignCanvas::set_on_revolve_angle_changed(std::function<void(double)> cb)
m_sketch_tool.on_revolve_angle_changed = std::move(cb);
}
void DesignCanvas::set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle)
{
m_sketch_tool.set_draft_gizmo(face_centroid, face_normal, angle);
request_repaint();
}
void DesignCanvas::clear_draft_gizmo()
{
m_sketch_tool.clear_draft_gizmo();
request_repaint();
}
bool DesignCanvas::drafting() const { return m_sketch_tool.drafting(); }
void DesignCanvas::set_on_draft_angle_changed(std::function<void(double)> cb)
{
m_sketch_tool.set_on_draft_angle_changed(std::move(cb));
}
void DesignCanvas::set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent)
{
m_sketch_tool.set_cut_gizmo(plane, offset, body_center, half_extent);
request_repaint();
}
void DesignCanvas::clear_cut_gizmo()
{
m_sketch_tool.clear_cut_gizmo();
request_repaint();
}
bool DesignCanvas::cutting() const { return m_sketch_tool.cutting(); }
void DesignCanvas::set_on_cut_offset_changed(std::function<void(double)> cb)
{
m_sketch_tool.set_on_cut_offset_changed(std::move(cb));
}
void DesignCanvas::begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid,
bool circular, int count, int dir, double spacing, double angle)
{
@@ -759,6 +799,20 @@ void DesignCanvas::set_body_highlight(bool on)
reload(true); // recolours the body volume (selected = cyan tint)
}
void DesignCanvas::set_operand_bodies(int target_body, int tool_body)
{
if (m_hl_body_target == target_body && m_hl_body_tool == tool_body) return;
m_hl_body_target = target_body;
m_hl_body_tool = tool_body;
reload(true); // recolours the body volumes (same idiom set_body_highlight uses)
}
void DesignCanvas::set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl)
{
m_sketch_tool.set_highlight_sketches(std::move(hl));
request_repaint();
}
void DesignCanvas::set_body_translucent(bool on)
{
if (m_body_translucent == on) return;
+17 -1
View File
@@ -130,6 +130,18 @@ public:
void clear_revolve_gizmo();
bool revolving() const;
void set_on_revolve_angle_changed(std::function<void(double)> cb);
// Visual Draft angle-arc gizmo: the panel feeds the face centroid + face normal + angle while
// its Draft card is open; drag/edit fire the angle callback.
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
void clear_draft_gizmo();
bool drafting() const;
void set_on_draft_angle_changed(std::function<void(double)> cb);
// Visual Cut gizmo: plane-rectangle preview + draggable normal offset arrow while
// the Cut card is open; drag fires the offset callback.
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
void clear_cut_gizmo();
bool cutting() const;
void set_on_cut_offset_changed(std::function<void(double)> cb);
// Visual Pattern gizmo: the panel feeds the (world XY) plane + target body centroid + mode +
// count/dir/spacing/angle while its Pattern card is open; drag/edit fire the value callback.
void begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular,
@@ -157,9 +169,11 @@ public:
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_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
void set_datum_planes(std::vector<SketchPlane> planes,
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
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_operand_bodies(int target_body, int tool_body); // -1,-1 clears
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
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
@@ -229,6 +243,8 @@ private:
Model m_model;
bool m_first_frame{true};
bool m_body_selected{false}; // tree selected a body feature → tint the solid
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
bool m_body_hidden{false}; // preview-only mode → hide base bodies, ghost = the result
std::vector<bool> m_body_visible; // per-body visibility (empty => all visible)
+74
View File
@@ -1907,6 +1907,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
refresh_preview();
});
m_viewport->set_on_draft_angle_changed([this](double angle) {
if (m_draft_angle) m_draft_angle->SetValue(angle);
refresh_preview();
});
m_viewport->set_on_cut_offset_changed([this](double v) {
if (m_cut_offset) m_cut_offset->SetValue(v);
refresh_preview();
});
m_viewport->set_on_pattern_changed([this](double value) {
// Linear drag feeds spacing; circular drag feeds angle. The card knows which is live.
if (m_pattern_type && m_pattern_type->GetSelection() == 1) {
@@ -5171,6 +5181,60 @@ void DesignPanel::update_revolve_gizmo()
m_revolve_angle->GetValue(), m_revolve_flip->GetValue());
}
void DesignPanel::update_draft_gizmo()
{
if (!m_viewport) return;
if (m_active != Tool::Draft || m_sel_solid_face < 0 || m_sel_solid_body < 0
|| m_sel_solid_body >= int(m_doc.bodies.size())) {
m_viewport->clear_draft_gizmo();
return;
}
const TopoDS_Face f = GeometryEngine::face_by_index(m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face);
const Vec3d c = GeometryEngine::face_centroid_world(f);
const Vec3d n = GeometryEngine::face_normal_world(f);
m_viewport->set_draft_gizmo(c, n, m_draft_angle->GetValue());
}
void DesignPanel::update_cut_gizmo()
{
if (!m_viewport) return;
if (m_active != Tool::Cut) { m_viewport->clear_cut_gizmo(); return; }
const int bi = m_cut_target ? m_cut_target->GetSelection() : -1;
if (bi < 0 || bi >= int(m_doc.bodies.size()) || bi >= int(m_doc.display_body_meshes.size())) {
m_viewport->clear_cut_gizmo();
return;
}
const SketchPlane plane = plane_from_choice(m_cut_plane->GetSelection());
const BoundingBoxf3 bb = m_doc.display_body_meshes[bi].bounding_box();
const Vec3d center = bb.center();
const double half = std::max(0.5 * (bb.max - bb.min).norm(), 10.0);
m_viewport->set_cut_gizmo(plane, m_cut_offset->GetValue(), center, half);
}
void DesignPanel::update_operand_highlight()
{
if (!m_viewport) return;
// default: nothing highlighted
int bt = -1, bl = -1;
std::vector<std::pair<int, ColorRGBA>> sk;
if (m_active == Tool::Boolean) {
if (m_bool_target) bt = m_bool_target->GetSelection();
if (m_bool_tool) bl = m_bool_tool->GetSelection();
} else if (m_active == Tool::Sweep) {
if (m_sweep_profile_ref >= 0) sk.emplace_back(m_sweep_profile_ref, ColorRGBA(0.30f, 0.85f, 1.0f, 1.0f)); // profile = cyan
if (m_sweep_path_ref >= 0) sk.emplace_back(m_sweep_path_ref, ColorRGBA(1.00f, 0.40f, 0.90f, 1.0f)); // path = magenta
} else if (m_active == Tool::Loft) {
// checked rows of m_loft_list map to feature indices via m_loft_sketch_idx
// (exactly as build_candidate(Tool::Loft) reads them).
if (m_loft_list)
for (unsigned i = 0; i < m_loft_list->GetCount(); ++i)
if (m_loft_list->IsChecked(i) && i < m_loft_sketch_idx.size())
sk.emplace_back(m_loft_sketch_idx[i], ColorRGBA(0.40f, 0.90f, 0.50f, 1.0f)); // profiles = green
}
m_viewport->set_operand_bodies(bt, bl);
m_viewport->set_highlight_sketches(std::move(sk));
}
void DesignPanel::update_pattern_gizmo()
{
if (!m_viewport) return;
@@ -5449,6 +5513,12 @@ void DesignPanel::refresh_preview()
update_shell_gizmo();
// Same for the Revolve angle-arc around the axis (self-gates: only while the Revolve card is open).
update_revolve_gizmo();
// Same for the Draft angle-arc around the face centroid (self-gates: only while the Draft card is open).
update_draft_gizmo();
// Same for the Cut plane offset arrow + rectangle (self-gates: only while the Cut card is open).
update_cut_gizmo();
// Operand highlight for Boolean (body tints) / Sweep / Loft (sketch tints); self-gates by tool.
update_operand_highlight();
// 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).
@@ -5604,7 +5674,11 @@ void DesignPanel::close_tool()
m_viewport->clear_thread_gizmo();
m_viewport->clear_shell_gizmo();
m_viewport->clear_revolve_gizmo();
m_viewport->clear_draft_gizmo();
m_viewport->clear_cut_gizmo();
m_viewport->clear_pattern_gizmo();
m_viewport->set_operand_bodies(-1, -1);
m_viewport->set_highlight_sketches({});
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();
+3
View File
@@ -184,6 +184,9 @@ private:
void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card)
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_draft_gizmo(); // angle-arc around the face centroid (Draft card)
void update_cut_gizmo(); // plane-rectangle + offset arrow (Cut card)
void update_operand_highlight(); // Boolean/Sweep/Loft operand tinting on the canvas
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
+278 -2
View File
@@ -4211,6 +4211,238 @@ static Vec3d rv_yaxis(const Vec3d& axis, const Vec3d& ref, bool flip)
return flip ? -y : y;
}
// Arc in the draft plane (perpendicular to the world +Z axis through the face centroid).
// The arc sweeps from angle 0 to m_dr_angle, representing the taper amount.
static Vec3d dr_yaxis(const Vec3d& axis, const Vec3d& ref)
{
Vec3d y = axis.cross(ref);
if (y.norm() < 1e-9) return ref;
y.normalize();
return y;
}
void DesignSketchTool::set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle)
{
m_dr_center = face_centroid;
m_dr_angle = std::min(89.0, std::max(-89.0, angle));
m_dr_axis = Vec3d::UnitZ(); // pull direction is world +Z
Vec3d ref = face_normal - face_normal.dot(m_dr_axis) * m_dr_axis; // horizontal component
if (ref.norm() < 1e-6) ref = Vec3d::UnitX(); // fallback: face normal is parallel to Z
m_dr_ref = ref / ref.norm();
m_dr_radius = 12.0; // fixed world-size manipulator
if (!m_dr_active) m_dr_drag = false;
m_dr_active = true;
}
void DesignSketchTool::clear_draft_gizmo()
{
m_dr_active = false;
m_dr_drag = false;
}
void DesignSketchTool::render_draft_gizmo()
{
if (!m_dr_active) return;
const Camera& cam = wxGetApp().plater()->get_camera();
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double th = std::max(15.0 * upp, 1e-4);
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
const SketchPlane saved = m_plane;
SketchPlane rp; rp.origin = m_dr_center; rp.x_axis = m_dr_ref; rp.y_axis = yax; rp.normal = m_dr_axis;
m_plane = rp;
const ColorRGBA arcc(0.15f, 0.92f, 1.0f, 1.0f);
const double r = m_dr_radius;
// Draft angle can be negative: sweep counter-clockwise (positive) or clockwise (negative)
const double a = m_dr_angle * M_PI / 180.0;
const int N = std::max(8, int(std::abs(a) / (M_PI / 32.0))); // ~ every 5.6°
std::vector<std::pair<Vec2d, Vec2d>> segs;
Vec2d prev(r, 0.0);
for (int i = 1; i <= N; ++i) {
const double t = a * double(i) / double(N);
const Vec2d cur(r * std::cos(t), r * std::sin(t));
segs.emplace_back(prev, cur);
prev = cur;
}
const Vec2d tip(r * std::cos(a), r * std::sin(a));
segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // spoke at angle 0
segs.emplace_back(Vec2d(0, 0), tip); // spoke at the swept angle (the handle)
const Vec2d tang(-std::sin(a), std::cos(a));
const Vec2d radial = tip.normalized();
const double as = std::max(r * 0.14, th);
segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5));
segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5));
const double hs = std::max(th * 0.8, r * 0.05);
const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs;
segs.emplace_back(tip + du, tip + dv);
segs.emplace_back(tip + dv, tip - du);
segs.emplace_back(tip - du, tip - dv);
segs.emplace_back(tip - dv, tip + du);
glsafe(::glDisable(GL_DEPTH_TEST));
draw_strokes(m_dr_stroke_model, segs, std::max(0.8 * upp, 1e-4), arcc);
DimAnnot da; da.kind = DimType::Angle; da.value = m_dr_angle;
draw_text(m_line_model, dim_text(da), tip * 1.14, th, arcc);
m_plane = saved;
}
bool DesignSketchTool::hit_test_draft_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
{
if (!m_dr_active) return false;
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
const Vec3d ro = ray.a, rd = ray.b - ray.a;
const Camera& cam = wxGetApp().plater()->get_camera();
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
const double a = m_dr_angle * M_PI / 180.0;
const double tol = 16.0 * upp;
auto ray_pt = [&](const Vec3d& p) {
const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12);
return (p - (ro + t * rd)).norm();
};
const Vec3d tip = m_dr_center + m_dr_radius * (std::cos(a) * m_dr_ref + std::sin(a) * yax);
const Vec3d ref = m_dr_center + m_dr_radius * m_dr_ref; // angle-0 spoke end
const int N = 48;
for (int i = 0; i <= N; ++i) {
const double f = double(i) / double(N);
const double th = a * f;
const Vec3d arc = m_dr_center + m_dr_radius * (std::cos(th) * m_dr_ref + std::sin(th) * yax);
if (ray_pt(arc) <= tol) return true;
if (ray_pt(m_dr_center + f * (tip - m_dr_center)) <= tol) return true;
if (ray_pt(m_dr_center + f * (ref - m_dr_center)) <= tol) return true;
}
return false;
}
void DesignSketchTool::drag_draft_arc(GLCanvas3D& canvas, const wxMouseEvent& evt)
{
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
const Vec3d ro = ray.a, rd = ray.b - ray.a;
const double denom = rd.dot(m_dr_axis);
if (std::abs(denom) < 1e-9) return;
const double t = (m_dr_center - ro).dot(m_dr_axis) / denom;
const Vec3d p = ro + t * rd;
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
const double u = (p - m_dr_center).dot(m_dr_ref);
const double v = (p - m_dr_center).dot(yax);
double deg = std::atan2(v, u) * 180.0 / M_PI;
deg = std::min(89.0, std::max(-89.0, deg));
m_dr_angle = deg;
if (m_on_draft_angle_changed) m_on_draft_angle_changed(deg);
}
// ---- Cut gizmo (plane normal arrow + wire rectangle preview) --------------------------------
// Arrow: shaft + arrowhead billboarded along the cut-plane normal from the projected body
// centre, with a signed Distance label = offset. Drag is RELATIVE via hole_axis_proj.
// Rectangle: 4 segments in the cut plane at the current offset, sized to the target body.
void DesignSketchTool::set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent)
{
m_ct_n = plane.normal.normalized();
m_ct_u = plane.x_axis.normalized();
m_ct_v = plane.y_axis.normalized();
Vec3d rel = body_center - plane.origin;
m_ct_base = plane.origin + (rel - rel.dot(m_ct_n) * m_ct_n); // body center projected into the cut plane
m_ct_offset = offset;
m_ct_half = std::max(half_extent, 10.0);
if (!m_ct_active) m_ct_drag = false;
m_ct_active = true;
}
void DesignSketchTool::clear_cut_gizmo()
{
m_ct_active = false;
m_ct_drag = false;
}
void DesignSketchTool::render_cut_gizmo()
{
if (!m_ct_active) return;
const Camera& cam = wxGetApp().plater()->get_camera();
const Vec3d right = cam.get_dir_right().normalized();
const Vec3d up = cam.get_dir_up().normalized();
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double th = std::max(15.0 * upp, 1e-4);
const ColorRGBA teal(0.20f, 0.80f, 0.75f, 1.0f);
// ---- Wire rectangle in the cut plane ----
{
const Vec3d cutpos = m_ct_base + m_ct_offset * m_ct_n;
const SketchPlane saved = m_plane;
SketchPlane rp; rp.origin = cutpos; rp.x_axis = m_ct_u; rp.y_axis = m_ct_v; rp.normal = m_ct_n;
m_plane = rp;
const double h = m_ct_half;
std::vector<std::pair<Vec2d, Vec2d>> segs;
segs.emplace_back(Vec2d(-h, -h), Vec2d( h, -h));
segs.emplace_back(Vec2d( h, -h), Vec2d( h, h));
segs.emplace_back(Vec2d( h, h), Vec2d(-h, h));
segs.emplace_back(Vec2d(-h, h), Vec2d(-h, -h));
glsafe(::glDisable(GL_DEPTH_TEST));
draw_strokes(m_ct_rect_model, segs, std::max(0.7 * upp, 1e-4), teal);
m_plane = saved;
}
// ---- Offset arrow (billboarded, clone of render_shell_gizmo) ----
{
const double L_sign = (std::abs(m_ct_offset) < 40.0 * upp)
? std::copysign(40.0 * upp, (m_ct_offset == 0.0 ? 1.0 : m_ct_offset))
: m_ct_offset;
const Vec3d tipw = m_ct_base + m_ct_n * L_sign;
const SketchPlane saved = m_plane;
SketchPlane bb; bb.origin = m_ct_base; bb.x_axis = right; bb.y_axis = up;
bb.normal = cam.get_dir_forward().normalized();
m_plane = bb;
const Vec2d tip2((tipw - m_ct_base).dot(right), (tipw - m_ct_base).dot(up));
if (tip2.norm() > 1e-6) {
const Vec2d u = tip2.normalized();
const Vec2d nrm(-u.y(), u.x());
std::vector<std::pair<Vec2d, Vec2d>> segs;
segs.emplace_back(Vec2d(0, 0), tip2);
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
const Vec2d back = tip2 - u * as;
segs.emplace_back(tip2, back + nrm * (as * 0.5));
segs.emplace_back(tip2, back - nrm * (as * 0.5));
glsafe(::glDisable(GL_DEPTH_TEST));
draw_strokes(m_ct_stroke_model, segs, std::max(0.7 * upp, 1e-4), teal);
DimAnnot da; da.kind = DimType::Distance; da.value = m_ct_offset;
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, teal);
}
m_plane = saved;
}
}
bool DesignSketchTool::hit_test_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const
{
if (!m_ct_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 upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
const double L_sign = (std::abs(m_ct_offset) < 40.0 * upp)
? std::copysign(40.0 * upp, (m_ct_offset == 0.0 ? 1.0 : m_ct_offset))
: m_ct_offset;
return ray_segment_dist3(ro, rd, m_ct_base, m_ct_base + m_ct_n * L_sign) <= 12.0 * upp;
}
void DesignSketchTool::start_cut_drag(GLCanvas3D& canvas, const wxMouseEvent& evt)
{
m_ct_drag = true;
m_ct_grab_val = m_ct_offset;
const double p = hole_axis_proj(canvas, evt, m_ct_base, m_ct_n);
m_ct_grab_proj = std::isnan(p) ? 0.0 : p;
}
void DesignSketchTool::drag_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt)
{
const double proj = hole_axis_proj(canvas, evt, m_ct_base, m_ct_n);
if (std::isnan(proj)) return;
m_ct_offset = m_ct_grab_val + (proj - m_ct_grab_proj);
if (m_on_cut_offset_changed) m_on_cut_offset_changed(m_ct_offset);
}
void DesignSketchTool::set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip)
{
@@ -6282,6 +6514,12 @@ void DesignSketchTool::confirm_transform()
if (on_selection_changed) on_selection_changed(0);
}
const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const
{
for (const auto& h : m_hl_sketches) if (h.first == feature) return &h.second;
return nullptr;
}
void DesignSketchTool::render(GLCanvas3D& canvas)
{
m_dim_label_seq = 0;
@@ -6338,7 +6576,10 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
const std::vector<RegionLoop> loops = region_loops(ds.entities);
for (int r = 0; r < int(loops.size()); ++r) {
const bool sel = (ds.feature == m_display_pick && r == m_display_pick_region);
draw_fill(m_fill_model, loops[r].poly, sel ? sface : dface);
const ColorRGBA* hlc = sketch_hl_color(ds.feature);
ColorRGBA fc = sel ? sface : dface;
if (hlc && !sel) { fc = *hlc; fc.a(0.20f); }
draw_fill(m_fill_model, loops[r].poly, fc);
}
}
glsafe(::glDisable(GL_BLEND));
@@ -6357,7 +6598,9 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
if (e.type == SketchEntity::Type::Point) continue;
bool closed = false;
std::vector<Vec2d> poly = entity_polyline(e, closed);
draw_quad_strip(m_line_model, poly, closed, sel_ent[i] ? swire : dwire);
const ColorRGBA* hlc = sketch_hl_color(ds.feature);
ColorRGBA wc = sel_ent[i] ? swire : (hlc ? *hlc : dwire);
draw_quad_strip(m_line_model, poly, closed, wc);
}
}
m_plane = saved_plane;
@@ -6377,6 +6620,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
if (m_th_active) render_thread_gizmo();
if (m_sh_active) render_shell_gizmo();
if (m_rv_active) render_revolve_gizmo();
if (m_dr_active) render_draft_gizmo();
if (m_ct_active) render_cut_gizmo();
if (m_pt_active) render_pattern_gizmo();
shader->stop_using();
glsafe(::glEnable(GL_CULL_FACE));
@@ -7186,6 +7431,37 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
return true;
}
}
// Draft angle-arc gizmo: drag the arc tip to sweep the draft angle; a stationary click
// edits it. A LeftDown that misses falls through to normal picking.
if (m_dr_active) {
if (m_dr_drag && evt.Dragging() && evt.LeftIsDown()) {
drag_draft_arc(canvas, evt);
return true;
}
if (evt.LeftUp() && m_dr_drag) {
m_dr_drag = false;
return true;
}
if (evt.LeftDown() && hit_test_draft_handle(canvas, evt)) {
m_dr_drag = true; m_dr_press_x = evt.GetX(); m_dr_press_y = evt.GetY();
return true;
}
}
// Cut gizmo: drag the offset arrow along the cut-plane normal; no positive clamp (offset is signed).
if (m_ct_active) {
if (m_ct_drag && evt.Dragging() && evt.LeftIsDown()) {
drag_cut_arrow(canvas, evt);
return true;
}
if (evt.LeftUp() && m_ct_drag) {
m_ct_drag = false;
return true;
}
if (evt.LeftDown() && hit_test_cut_arrow(canvas, evt)) {
start_cut_drag(canvas, evt);
return true;
}
}
// Pattern gizmo: drag the diamond/arc handle to set the spacing (linear) or angle (circular);
// a stationary click opens the inline editor. A LeftDown that misses falls through to picking.
if (m_pt_active) {
+50 -1
View File
@@ -93,12 +93,13 @@ public:
// own plane. render() draws these as translucent faces + outlines.
struct DisplaySketch { std::vector<SketchEntity> entities; SketchPlane plane; int feature{-1}; };
void set_display_sketches(std::vector<DisplaySketch> ds) { m_display_sketches = std::move(ds); }
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl) { m_hl_sketches = std::move(hl); }
bool has_display() const { return m_active || !m_display_sketches.empty()
|| (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_dz_active || m_dbp_active; }
|| m_dr_active || m_ct_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
@@ -238,6 +239,18 @@ public:
bool revolving() const { return m_rv_active; }
std::function<void(double angle)> on_revolve_angle_changed;
// Visual Draft angle-arc gizmo (taper a picked face; axis = world +Z, arc in XY).
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
void clear_draft_gizmo();
bool drafting() const { return m_dr_active; }
void set_on_draft_angle_changed(std::function<void(double)> cb) { m_on_draft_angle_changed = std::move(cb); }
// Visual Cut gizmo (plane normal arrow + plane rectangle preview).
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
void clear_cut_gizmo();
bool cutting() const { return m_ct_active; }
void set_on_cut_offset_changed(std::function<void(double)> cb) { m_on_cut_offset_changed = std::move(cb); }
// Visual Pattern gizmo. Linear: a 3D arrow along the world axis (plane X/Y per `dir`) of length
// spacing*(count-1) with a tick at each copy; dragging the end sets the spacing. Circular: a
// revolve-style angle-arc about the plane normal through the plane origin sweeping `angle`.
@@ -660,6 +673,7 @@ private:
void draw_vertices(GLModel& model, const std::vector<Vec2d>& pts, const ColorRGBA& color,
double half_size = 1.3);
void draw_fill(GLModel& model, const std::vector<Vec2d>& poly, const ColorRGBA& color);
const ColorRGBA* sketch_hl_color(int feature) const;
bool m_active{false};
SketchPlane m_plane;
@@ -822,6 +836,7 @@ private:
float m_render_scale{1.0f}; // canvas scale for Measure-style dim labels
GLModel m_fill_model; // translucent face fill for closed regions
std::vector<DisplaySketch> m_display_sketches; // committed sketches drawn persistently
std::vector<std::pair<int, ColorRGBA>> m_hl_sketches; // feature index -> outline colour (Sweep/Loft operands)
int m_display_pick{-1}; // FEATURE index of the click-selected display sketch (-1 none)
// Solid (whole/face/edge) selection on the committed bodies. Pointers are non-owning,
@@ -1011,6 +1026,40 @@ private:
void open_revolve_editor();
GLModel m_rv_stroke_model;
// Draft gizmo state (arc center = picked face centroid; axis = world +Z, taper pull direction).
bool m_dr_active{false};
Vec3d m_dr_center{Vec3d::Zero()}; // arc center = face centroid (world)
Vec3d m_dr_axis{Vec3d::UnitZ()}; // draft axis = world +Z (pull direction)
Vec3d m_dr_ref{Vec3d::UnitX()}; // angle-0 reference dir (perp to axis)
double m_dr_radius{10.0}; // arc radius (world)
double m_dr_angle{5.0}; // current sweep magnitude (deg, [-89, 89])
bool m_dr_drag{false};
int m_dr_press_x{0}, m_dr_press_y{0};
void render_draft_gizmo();
bool hit_test_draft_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void drag_draft_arc(GLCanvas3D& canvas, const wxMouseEvent& evt);
GLModel m_dr_stroke_model;
std::function<void(double)> m_on_draft_angle_changed;
// Cut gizmo state (plane normal arrow + wire rectangle at the current offset).
bool m_ct_active{false};
Vec3d m_ct_base{Vec3d::Zero()}; // body centre projected into the cut plane
Vec3d m_ct_n{Vec3d::UnitZ()}; // cut plane normal (unit)
Vec3d m_ct_u{Vec3d::UnitX()}; // cut plane U axis (unit)
Vec3d m_ct_v{Vec3d::UnitY()}; // cut plane V axis (unit)
double m_ct_offset{0.0};
double m_ct_half{10.0};
bool m_ct_drag{false};
double m_ct_grab_val{0.0};
double m_ct_grab_proj{0.0};
void render_cut_gizmo();
bool hit_test_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void start_cut_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
void drag_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
GLModel m_ct_stroke_model;
GLModel m_ct_rect_model;
std::function<void(double)> m_on_cut_offset_changed;
// Pattern gizmo state. Linear arrow along m_pt_dirw from m_pt_base; circular arc like Revolve
// but axis = m_pt_normal through m_pt_origin (the world XY plane by default).
bool m_pt_active{false};