From e6a14b39c95c0e920daf978146aa71e3ca648018 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Wed, 12 Aug 2026 21:34:59 +0200 Subject: [PATCH] Rib: the thickness gets its handle, so the whole tool is draggable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rib's depth already reused the Extrude arrow. Its thickness could not: the arrow points along the plane normal, and thickness is an offset either side of the rib line, IN the plane. Different direction, different handle. Two square handles at mid ± perp·half, plus the slab's actual footprint drawn as a thin closed rectangle — the footprint matters more than the dots, because what a rib thickness means is how wide that slab lands on the body, and until now there was no way to see it before committing. A drag on either handle sets the FULL thickness, twice the perpendicular distance from the line, because the slab is centred on the line and the handle sits at half. Both handles behave identically for the same reason, so they share one colour rather than pretending to be two different actions. A zero-length line has no direction to grow a slab perpendicular to, so the shared rib_frame() helper returns false and render and drag both draw nothing rather than dividing by zero. Non-Line entities clear the gizmo instead of guessing: the kernel is line-only and a gizmo that guesses would be lying about what Confirm will build. Unlike the helix callback this one goes through refresh_preview(), because Rib builds a real solid ghost that has to rebuild. The helix has none and skips it deliberately. Both gizmos coexist and resolve the sketch and entity the same way, so the depth arrow and the thickness handles can never disagree about which line they are on. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-plew. --- src/slic3r/GUI/DesignCanvas.cpp | 18 ++++ src/slic3r/GUI/DesignCanvas.hpp | 3 + src/slic3r/GUI/DesignPanel.cpp | 27 +++++ src/slic3r/GUI/DesignPanel.hpp | 1 + src/slic3r/GUI/DesignSketchTool.cpp | 149 ++++++++++++++++++++++++++++ src/slic3r/GUI/DesignSketchTool.hpp | 24 ++++- 6 files changed, 221 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/DesignCanvas.cpp b/src/slic3r/GUI/DesignCanvas.cpp index 166b5ecfb9..2d83ea026e 100644 --- a/src/slic3r/GUI/DesignCanvas.cpp +++ b/src/slic3r/GUI/DesignCanvas.cpp @@ -861,6 +861,24 @@ void DesignCanvas::set_on_helix_changed(std::function cb) +{ + m_sketch_tool.on_rib_thickness_changed = std::move(cb); +} + void DesignCanvas::set_base_pick(std::vector planes, std::vector bases, std::vector labels) { diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp index 2d4c7b31be..d0203f5863 100644 --- a/src/slic3r/GUI/DesignCanvas.hpp +++ b/src/slic3r/GUI/DesignCanvas.hpp @@ -174,6 +174,9 @@ public: double taper, bool left_handed); // helix curve + 3 drag handles void clear_helix_gizmo(); void set_on_helix_changed(std::function cb); + void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness); // rib slab footprint + 2 thickness handles + void clear_rib_gizmo(); + void set_on_rib_thickness_changed(std::function cb); void set_base_pick(std::vector planes, std::vector bases, std::vector labels = {}); // clickable labelled reference planes void clear_base_pick(); diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index a83e2b8811..b04da78a77 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -3544,6 +3544,12 @@ DesignPanel::DesignPanel(wxWindow* parent) m_viewport->request_repaint(); }); + // Rib thickness handle: writes the spin and re-feeds the ghost, same as the depth arrow. + m_viewport->set_on_rib_thickness_changed([this](double thickness) { + if (m_rib_thickness) m_rib_thickness->SetValue(thickness); + refresh_preview(); // Rib HAS a solid ghost, so the full preview path is correct here + }); + // 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) { @@ -9122,6 +9128,25 @@ void DesignPanel::update_helix_gizmo() m_helix_left_handed && m_helix_left_handed->GetValue()); } +void DesignPanel::update_rib_gizmo() +{ + if (!m_viewport) return; + if (m_active != Tool::Rib) { m_viewport->clear_rib_gizmo(); return; } + // Same resolution the Tool::Rib branch of update_extrude_gizmo() uses — the depth arrow and + // this share one sketch and one entity, and must never disagree about which line that is. + const int ssel = m_rib_sketch ? m_rib_sketch->GetSelection() : wxNOT_FOUND; + const int ref = (ssel != wxNOT_FOUND) + ? int(reinterpret_cast(m_rib_sketch->GetClientData(ssel))) : -1; + if (ref < 0 || ref >= int(m_doc.features.size())) { m_viewport->clear_rib_gizmo(); return; } + const CadFeature& sk = m_doc.features[ref]; + const int ei = m_rib_entity ? m_rib_entity->GetValue() : 0; + if (ei < 0 || ei >= int(sk.entities.size())) { m_viewport->clear_rib_gizmo(); return; } + const SketchEntity& e = sk.entities[ei]; + if (e.type != SketchEntity::Type::Line) { m_viewport->clear_rib_gizmo(); return; } // rib is line-only + m_viewport->set_rib_gizmo(sk.plane, e.p0, e.p1, + m_rib_thickness ? m_rib_thickness->GetValue() : 0.0); +} + // 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 @@ -9297,6 +9322,8 @@ void DesignPanel::refresh_preview() update_datum_gizmo(); // Helix curve + handles (self-gates: only while the Helix card is open). update_helix_gizmo(); + // Rib slab footprint + thickness handles (self-gates: only while the Rib card is open). + update_rib_gizmo(); update_operand_highlight(); } diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index a66d12efae..2397e5e2d5 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -273,6 +273,7 @@ private: 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 update_helix_gizmo(); // live helix curve + radius/height/pitch handles (Helix card) + void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card) void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport void refresh_mate_connectors(); // push connector frames so verse + polarity are visible void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object) diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index 81a755515c..13f693caab 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -3914,6 +3914,139 @@ void DesignSketchTool::drag_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& if (on_helix_changed) on_helix_changed(m_hx_radius, m_hx_pitch, m_hx_height); } +// ---- Rib thickness gizmo (in-plane slab footprint + 2 symmetric handles) ----------------- +void DesignSketchTool::set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, + double thickness) +{ + m_rb_active = true; + m_rb_plane = plane; + m_rb_p0 = p0; + m_rb_p1 = p1; + m_rb_thickness = thickness; +} + +void DesignSketchTool::clear_rib_gizmo() +{ + m_rb_active = false; + m_rb_drag = -1; +} + +// Resolve the rib line's in-plane frame: unit direction d, perpendicular perp, midpoint, and +// half-thickness. A degenerate line (zero length) has no direction to grow a slab perpendicular +// to — return false so the caller draws nothing and never divides by zero. +static bool rib_frame(const Vec2d& p0, const Vec2d& p1, double thickness, + Vec2d& d, Vec2d& perp, Vec2d& mid, double& half) +{ + const Vec2d seg = p1 - p0; + const double len = seg.norm(); + if (len < 1e-9) return false; + d = seg / len; + perp = Vec2d(-d.y(), d.x()); + mid = 0.5 * (p0 + p1); + half = thickness * 0.5; + return true; +} + +// Draw the rib slab's actual footprint (the rectangle p0±perp·half, p1±perp·half) as a thin +// closed ribbon plus two square handles at mid ± perp·half. Both handles sit at half-thickness, +// so a drag on either expresses the full thickness symmetrically. +void DesignSketchTool::render_rib_gizmo() +{ + if (!m_rb_active) return; + Vec2d d, perp, mid; double half; + if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) 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; // ribbon half-width + + // Slab footprint outline (4 thin camera-facing ribbons). + const Vec3d c[4] = { m_rb_plane.to_world(m_rb_p0 + perp * half), + m_rb_plane.to_world(m_rb_p1 + perp * half), + m_rb_plane.to_world(m_rb_p1 - perp * half), + m_rb_plane.to_world(m_rb_p0 - perp * half) }; + 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()); border.add_vertex((Vec3f)(b + off).cast()); + border.add_vertex((Vec3f)(b - off).cast()); border.add_vertex((Vec3f)(a - off).cast()); + border.add_triangle(bb, bb + 1, bb + 2); border.add_triangle(bb, bb + 2, bb + 3); bb += 4; + } + + // Two handles: 0 = +perp side, 1 = -perp side (both at half-thickness). + const Vec3d hpts[2] = { m_rb_plane.to_world(mid + perp * half), + m_rb_plane.to_world(mid - perp * half) }; + const ColorRGBA hot(1.0f, 0.85f, 0.2f, 1.0f); + + 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 slab footprint + m.render(); + } + for (int i = 0; i < 2; ++i) { + const Vec3d ctr = hpts[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; + GLModel::Geometry sq; sq.format = { EPT::Triangles, EVL::P3 }; + sq.add_vertex((Vec3f)q0.cast()); sq.add_vertex((Vec3f)q1.cast()); + sq.add_vertex((Vec3f)q2.cast()); sq.add_vertex((Vec3f)q3.cast()); + sq.add_triangle(0, 1, 2); sq.add_triangle(0, 2, 3); + GLModel m; m.init_from(std::move(sq)); + m.set_color(m_rb_drag == i ? hot : ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f)); // green + m.render(); + } +} + +bool DesignSketchTool::hit_test_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const +{ + if (!m_rb_active) return false; + Vec2d d, perp, mid; double half; + if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) 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 Vec3d hpts[2] = { m_rb_plane.to_world(mid + perp * half), + m_rb_plane.to_world(mid - perp * half) }; + double best = tol; which = -1; + for (int i = 0; i < 2; ++i) { + const Vec3d w = hpts[i] - ro; + const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12); + const double dd = (w - rd * t).norm(); + if (dd < best) { best = dd; which = i; } + } + return which >= 0; +} + +// Drag a handle: project the cursor ray onto the rib plane (the same call the helix radius drag +// uses), take the perpendicular distance from the rib LINE to that point, and set the thickness +// to twice that distance — the slab is centred on the line and the handle sits at half-thickness. +void DesignSketchTool::drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) +{ + (void)which; // both handles behave identically: a drag on either sets the full thickness + const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); + const Vec3d ro = r.a, rd = r.b - r.a; + Vec2d d, perp, mid; double half; + if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) return; + const Vec2d lp = m_rb_plane.project(ro, rd); + const Vec2d w = lp - m_rb_p0; + const double dist = std::abs(w.x() * d.y() - w.y() * d.x()); // |cross(w, d)| = perp distance + m_rb_thickness = std::max(0.01, 2.0 * dist); // ×2: centred slab, handle at half + if (on_rib_thickness_changed) on_rib_thickness_changed(m_rb_thickness); +} + // ---- Reference/base planes (Onshape-style default planes) ----------------------------- void DesignSketchTool::set_base_pick(std::vector planes, std::vector bases, std::vector labels) @@ -7336,6 +7469,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) if (m_dbp_active) render_base_pick(); if (m_dz_active) render_datum_gizmo(); if (m_hx_active) render_helix_gizmo(); + if (m_rb_active) render_rib_gizmo(); if (m_ex_active) render_extrude_gizmo(); if (m_mv_active) render_move_gizmo(); if (m_fl_active) render_fillet_gizmo(); @@ -8158,6 +8292,21 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) } } } + // Rib thickness gizmo: two in-plane handles on the slab footprint, while the Rib card is open. + if (m_rb_active) { + if (m_rb_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { + drag_rib_handle(canvas, evt, m_rb_drag); + return true; + } + if (evt.LeftUp() && m_rb_drag >= 0) { m_rb_drag = -1; return true; } + if (evt.LeftDown()) { + int which = -1; + if (hit_test_rib_handle(canvas, evt, which)) { + m_rb_drag = which; + 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. diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index 33879da896..dc9aeef21c 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -119,7 +119,7 @@ public: || m_show_planes || m_show_axes || m_ex_active || m_mv_active || m_fl_active || m_hl_active || m_th_active || m_sh_active - || m_dr_active || m_ct_active || m_dz_active || m_dbp_active || m_hx_active; } + || m_dr_active || m_ct_active || m_dz_active || m_dbp_active || m_hx_active || m_rb_active; } // View helpers: the 3 world origin planes (XY/XZ/YZ) and the world axis triad, each // shown/hidden by a toggle (keys P / A). Off by default so the idle scene stays clean. @@ -214,6 +214,15 @@ public: void clear_helix_gizmo(); std::function on_helix_changed; + // Visual Rib thickness gizmo. The rib is a thin slab grown either side of an open sketch + // line, so its thickness is an IN-PLANE offset perpendicular to that line — the depth arrow + // (which points along the plane normal) cannot express it. Two handles, one per side, + // dragged symmetrically: the slab is centred on the line, so a drag on either side sets the + // full thickness rather than one half. + void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness); + void clear_rib_gizmo(); + std::function on_rib_thickness_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). @@ -1039,6 +1048,19 @@ private: void render_helix_gizmo(); bool hit_test_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const; void drag_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which); + // Rib thickness gizmo state (plane-anchored slab footprint + 2 drag handles). Fed by the + // panel while the Rib card is open (sketch tool NOT active); the tool draws the rib's + // footprint outline and a handle on each side of the line at half-thickness. Dragging either + // handle sets the full thickness (the slab is centred on the line). + bool m_rb_active{false}; + SketchPlane m_rb_plane; + Vec2d m_rb_p0{Vec2d::Zero()}; // rib line endpoints, in plane coords + Vec2d m_rb_p1{Vec2d::Zero()}; + double m_rb_thickness{2.0}; + int m_rb_drag{-1}; // 0 = +perp handle, 1 = -perp handle, -1 none + void render_rib_gizmo(); + bool hit_test_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const; + void drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which); // Datum base picker (translucent clickable origin/datum planes) bool m_dbp_active{false}; std::vector m_dbp_planes;