Compare commits

...
Author SHA1 Message Date
SoftFever 0c2e26fa3d Stop the Design tab's move arrows from jumping the body on the first drag
Grabbing an arrow anywhere along its length snapped the body's centre to that point as soon
as the mouse moved. The body now moves by how far the cursor travels from where the arrow
was grabbed, including when it is grabbed while looking straight down the axis.
2026-10-06 14:48:48 +08:00
SoftFever e328b60992 Make the Design tab's reference planes readable where they cross
The XY/XZ/YZ planes are cut along each other and drawn back to front, with
lines along every crossing, and their fills are strong enough for the order
to show. Before, they blended into one grey smear and were too pale to work with.
2026-10-06 14:19:46 +08:00
SoftFever afa9252b59 Make the Fit camera button frame the Design tab's selection and sketches
In the Design tab the button always swung the camera to the plate view and framed the whole
bed, whatever was selected: the tab's picks and sketches are neither the canvas's selection
nor its volumes. It now frames the selected faces, body, edges, vertex, sketch region or
sketch entities, and with nothing selected everything on show — the visible bodies, the
feature preview and the sketches — keeping the current view direction. An empty tab still
frames the bed as before.
2026-10-06 12:28:52 +08:00
SoftFever 56ebde968a Keep the Design tab's status line clear of the view buttons at any display scale
The line started at a fixed inset scaled like the ImGui style, while the navigator and the
round canvas buttons scale with the monitor's DPI on Windows, so at 150% the buttons covered
its first words. It now starts past the edge the canvas reports for that corner.
2026-10-06 11:55:41 +08:00
SoftFever 840d44b611 Keep the Design tab's edge lines in step with the bodies after undo, hide and delete 2026-10-06 11:21:18 +08:00
SoftFever 34696f5651 Name the Design canvas flag after its role, not the axes it once moved 2026-10-06 03:44:15 +08:00
SoftFever c300fa1e8d Keep the Design tab's bed, grid and reference planes aligned on every plate 2026-10-06 03:41:33 +08:00
SoftFever cd30b196b2 Let the camera pan and orbit freely while sketching in the Design tab
Panning or orbiting with a right-drag no longer ends the polyline chain or drops
the point already placed. Only a right-click that doesn't move does. In the
Touchpad camera style, Alt+move and Shift+move now orbit and pan even while a
draw tool is armed.
2026-10-05 23:24:50 +08:00
SoftFever b67269ccc4 Fix z-fighting between feature previews and the bodies in the Design tab
The bodies now always draw over the preview on every face a feature leaves unchanged, for
all features. Since a hole's cut sits inside the body, the Hole preview now hides the
bodies and shows the result alone, as Fillet/Chamfer and Draft do.
2026-10-05 19:38:46 +08:00
13 changed files with 969 additions and 175 deletions
@@ -262,9 +262,9 @@ and a band is not a "gesture" in the `CadLevel::Gesture` sense because nothing h
|---|---|---|---|---|---| |---|---|---|---|---|---|
| `Move` | — | `update_hover` / `update_solid_hover` (**non-consuming**, returns false) | — | passthrough | hover only asks for a repaint, `:10022-10025` | | `Move` | — | `update_hover` / `update_solid_hover` (**non-consuming**, returns false) | — | passthrough | hover only asks for a repaint, `:10022-10025` |
| `LeftDown` | orbit may begin | latch press, **return false** | — | consume-or-orbit is the canvas's call | `:10058-10063`; consuming here killed orbit once already | | `LeftDown` | orbit may begin | latch press, **return false** | — | consume-or-orbit is the canvas's call | `:10058-10063`; consuming here killed orbit once already |
| `LeftDrag` > 8 px | — | start + drive band, **consume** | — | no longer orbits in this canvas | middle-drag orbits, right-drag pans (`:10013-10016`) | | `LeftDrag` > 8 px | — | start + drive band, **consume**, when Shift is held or the left button has no camera action | — | otherwise the left button's drag action (Preferences > Control) | the band is Prepare's Shift+left-drag rectangle selection |
| `LeftUp` | — | commit pick **or** resolve band | — | — | `:10035-10041` | | `LeftUp` | — | commit pick **or** resolve band | — | — | `:10035-10041` |
| `MiddleDrag` / `RightDrag` | — | must not see it | — | orbit / pan | camera gestures never reach the FSM | | `MiddleDrag` / `RightDrag` | — | must not see it | — | the button's drag action (Preferences > Control) | camera gestures never reach the FSM; a right press reaches the tool only once its release shows it was a click |
| `RightClick` | — | — | offer menu | — | `snaporca-xmh6` open: a right-click that only clears the sketch selection eats the offer | | `RightClick` | — | — | offer menu | — | `snaporca-xmh6` open: a right-click that only clears the sketch selection eats the offer |
| `Esc` | — | — | `escape()` ladder | — | one route whatever holds focus (`:4228-4231`) | | `Esc` | — | — | `escape()` ladder | — | one route whatever holds focus (`:4228-4231`) |
| `Del` / `Backspace` | — | `delete_selected_or_last_sketch_entity()` | char hook | — | `:4260` | | `Del` / `Backspace` | — | `delete_selected_or_last_sketch_entity()` | char hook | — | `:4260` |
+28 -7
View File
@@ -154,15 +154,18 @@ rather than to its end points: a press that wandered past the budget at any mome
navigation, even if it comes back to where it started, which is what stops a slow, careful navigation, even if it comes back to where it started, which is what stops a slow, careful
orbit from ending in a menu. There is no time budget — a gesture that means something different orbit from ending in a menu. There is no time budget — a gesture that means something different
when it is slow is exactly what the interaction charter rules out. The raycast uses the press when it is slow is exactly what the interaction charter rules out. The raycast uses the press
position, not the release. An armed sketch tool that already consumed the right position, not the release. The sketch tool sees a right press only once the release has shown it
button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag. was a click: the press itself goes to the camera, which may pan or orbit with that button, and the
Past either budget the event is navigation, and navigation does not transition the state canvas replays it to the tool on a stationary release. A tool that uses the click (to terminate a
machine. chain, say) keeps the menu closed. Past either budget the event is navigation, and navigation
does not transition the state machine.
Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control
for each button. The left button is shared with picking, so a whole body is swept with a for each button, and in the Touchpad camera style a move with Alt held orbits and one with Shift
rectangle on plain left-drag only while no camera action is assigned to it, and with held pans, whatever tool is armed. The left button is shared with picking and drawing, so a tool
Shift+left-drag otherwise — Prepare's own rectangle selection. handle or a press that draws takes it first, as a gizmo does in Prepare; a whole body is swept
with a rectangle on plain left-drag only while no camera action is assigned to the left button,
and with Shift+left-drag otherwise — Prepare's own rectangle selection.
Entering a sketch changes three things at once so the mode is legible: a banner above the Entering a sketch changes three things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
@@ -189,6 +192,15 @@ degenerate edges are left out (`GeometryEngine::display_edges`), and the polylin
once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the
common case. common case.
While a feature card is open, its preview ghost is the whole model the candidate would produce,
drawn translucent over the bodies, so every face the feature leaves alone is in both at the same
depth. The ghost is drawn with a depth bias that pushes it back (`GLVolume::depth_bias`), so on a shared face
the body always wins instead of the two copies z-fighting, and the ghost shows only where the
result reaches past the bodies. Material a feature removes lies inside the old solid and would not
show at all, so the tools whose result mostly coincides with the body — Fillet/Chamfer, Draft,
Hole and the Mate hover — hide the bodies once the preview is valid and draw the result alone,
opaque.
## Showing what is selected ## Showing what is selected
A selection is drawn on the faces it names, never as a tint over the body: a translucent A selection is drawn on the faces it names, never as a tint over the body: a translucent
@@ -234,6 +246,15 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
`DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit, `DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit,
Show/hide and Delete on a feature, Move, Show/hide and Delete on a body — and the eye shows Show/hide and Delete on a feature, Move, Show/hide and Delete on a body — and the eye shows
whether that row is hidden. whether that row is hidden.
- **Plates.** This is the one thing the tab does not follow. The canvas has a bed of its own at
the printer bed's home position, whichever plate Prepare has current, and a new document's
modeling origin is that bed's centre. A bed that followed the current plate would slide out
from under a design: the origin is fixed once per document, baked into every sketch plane and
saved in the recipe, while the current plate can change between visits. Commit to Plate does
not need it either, since the committed object is placed on an empty spot of the current
plate. What the canvas does read from the plate is moved onto its bed: the exclude areas, the
plate box the camera orbits about when nothing is picked (`GLCanvas3D::_current_plate_box`),
and the first view, which starts as a copy of Prepare's camera.
- **Viewport text.** The status line and the active tool's values are drawn by the canvas in - **Viewport text.** The status line and the active tool's values are drawn by the canvas in
its ImGui pass, so they go with the canvas: a top-level window over GL does not follow its its ImGui pass, so they go with the canvas: a top-level window over GL does not follow its
frame and was left floating over other applications. frame and was left floating over other applications.
+7
View File
@@ -310,6 +310,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
, force_native_color(false) , force_native_color(false)
, force_neutral_color(false) , force_neutral_color(false)
, force_sinking_contours(false) , force_sinking_contours(false)
, depth_bias(false)
, picking(false) , picking(false)
, tverts_range(0, size_t(-1)) , tverts_range(0, size_t(-1))
{ {
@@ -1332,11 +1333,17 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
shader->set_uniform("projection_matrix", projection_matrix); shader->set_uniform("projection_matrix", projection_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix); shader->set_uniform("view_normal_matrix", view_normal_matrix);
if (volume.first->depth_bias) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(1.0f, 1.0f));
}
//BBS: add outline related logic //BBS: add outline related logic
if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline()) if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline())
volume.first->render_with_outline(cnv_size); volume.first->render_with_outline(cnv_size);
else else
volume.first->render(); volume.first->render();
if (volume.first->depth_bias)
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
#if ENABLE_ENVIRONMENT_MAP #if ENABLE_ENVIRONMENT_MAP
if (use_environment_texture) if (use_environment_texture)
+4
View File
@@ -232,6 +232,10 @@ public:
bool force_neutral_color : 1; bool force_neutral_color : 1;
// Whether or not to force rendering of sinking contours // Whether or not to force rendering of sinking contours
bool force_sinking_contours : 1; bool force_sinking_contours : 1;
// Orca: draw this volume with a positive depth bias (glPolygonOffset, pushed away from the
// camera), so on a surface it shares with another volume the other volume wins the depth
// test instead of z-fighting it
bool depth_bias : 1;
// Is render for picking // Is render for picking
bool picking : 1; bool picking : 1;
// slice error // slice error
+36 -20
View File
@@ -8,6 +8,7 @@
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal #include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/PartPlate.hpp" // the current plate's origin: the first view's offset
#include "slic3r/GUI/ImGuiWrapper.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLToolbar.hpp" #include "slic3r/GUI/GLToolbar.hpp"
#include "slic3r/GUI/Event.hpp" #include "slic3r/GUI/Event.hpp"
@@ -91,7 +92,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->enable_plate_chrome(false); m_canvas->enable_plate_chrome(false);
m_canvas->enable_labels(false); m_canvas->enable_labels(false);
m_canvas->enable_sinking_contours(false); // they would be sliced from the plater's meshes m_canvas->enable_sinking_contours(false); // they would be sliced from the plater's meshes
m_canvas->set_axes_at_bed_center(true); // triad at bed centre = modeling origin m_canvas->set_design_canvas(true); // home-position bed, triad and CAD grid at the modeling origin
m_canvas->set_design_sketch_tool(&m_sketch_tool); m_canvas->set_design_sketch_tool(&m_sketch_tool);
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) { m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
@@ -190,8 +191,11 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
refresh_bed(); refresh_bed();
// The view this canvas opens on. Built lazily, on the way into the Design tab, so this // The view this canvas opens on. Built lazily, on the way into the Design tab, so this
// is the view the user is looking at right now. // is the view the user is looking at right now — moved off the current plate onto the
// Design bed, which stays at the printer bed's home whichever plate is current.
m_parked_camera = wxGetApp().plater()->get_camera(); m_parked_camera = wxGetApp().plater()->get_camera();
if (PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate())
m_parked_camera.translate_world(-plate->get_origin());
// Before any of this class's own Binds below: wx calls dynamically bound handlers in // Before any of this class's own Binds below: wx calls dynamically bound handlers in
// reverse order of binding, and GLCanvas3D swallows several events without skipping them — // reverse order of binding, and GLCanvas3D swallows several events without skipping them —
@@ -378,10 +382,14 @@ void DesignCanvas::reload(bool keep_view)
v->set_color(c); v->set_color(c);
} }
} else if (obj_idx == 1) { } else if (obj_idx == 1) {
// The ghost is normally a faint blue overlay on the visible body. In preview-only // The ghost is the whole resulting model, normally drawn as a faint blue overlay on
// mode it IS the result (base bodies hidden), so render it opaque so it reads as a // the visible bodies. Every face the feature leaves alone is in both, at the same
// finished solid rather than a see-through hint. // depth, so the ghost is drawn with a depth bias: the bodies win on those faces instead
// of the two copies z-fighting, and the ghost shows only where the result reaches past
// the bodies. In preview-only mode it IS the result (base bodies hidden), so render it
// opaque so it reads as a finished solid rather than a see-through hint.
v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost); v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost);
v->depth_bias = true;
} }
} }
@@ -406,6 +414,7 @@ void DesignCanvas::set_bodies(const std::vector<TriangleMesh>* body_meshes,
if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; } if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; }
m_body_meshes = body_meshes; m_body_meshes = body_meshes;
m_sketch_tool.refresh_body_edges(); // of the bodies set_solid_pick() pointed the tool at
m_lit_faces = m_sketch_tool.selected_faces(); m_lit_faces = m_sketch_tool.selected_faces();
rebuild_bodies(); rebuild_bodies();
reload(!m_first_frame); reload(!m_first_frame);
@@ -415,6 +424,8 @@ void DesignCanvas::clear_mesh()
{ {
m_body_meshes = nullptr; m_body_meshes = nullptr;
m_volumes.clear(); m_volumes.clear();
// No solid, so no edge lines, pick or hover either, as after a rebuild that leaves no body.
m_sketch_tool.set_solid_pick(nullptr, nullptr, nullptr, nullptr);
if (!m_model.objects.empty()) { if (!m_model.objects.empty()) {
m_model.delete_object((size_t)0); m_model.delete_object((size_t)0);
reload(true); reload(true);
@@ -550,6 +561,7 @@ void DesignCanvas::refresh_bed()
double printable_height = 100.0; double printable_height = 100.0;
const auto* ph_opt = config->opt<ConfigOptionFloat>("printable_height"); const auto* ph_opt = config->opt<ConfigOptionFloat>("printable_height");
if (ph_opt) printable_height = ph_opt->value; if (ph_opt) printable_height = ph_opt->value;
// No position: the Design bed stays at the printer bed's home, whichever plate is current.
m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params
} }
@@ -1047,15 +1059,18 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
return; return;
m_ctx_bound = true; m_ctx_bound = true;
// Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event. // Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event.
// It only consumes when it actually opens the offer; every other right-click still falls // It only consumes when it actually opens the offer.
// through to the polyline-chain end and the move gizmo, which were there first. // Right-drag may pan or orbit (Preferences > Control). Without remembering where the press
// Right-drag pans. Without remembering where the press landed, every pan ended by popping // landed, every such drag ended by popping the offer over wherever the camera stopped — the
// the offer over wherever the camera stopped — the menu appearing as the reward for moving // menu appearing as the reward for moving the view. A right-click is the release of a
// the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx). // STATIONARY press (kCadRightClickDriftPx); only that reaches the sketch tool or the offer.
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) { m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition(); m_ctx_press = e.GetPosition();
m_ctx_travelled = false; m_ctx_travelled = false;
e.Skip(); // the canvas still needs the press to seed the orbit // Drop any press the tool still keeps (only a click's release takes it, so a pan's stays)
// before the canvas offers it this one, which ImGui may take instead.
m_sketch_tool.drop_right_click();
e.Skip(); // the canvas still needs the press to seed a pan or an orbit
}); });
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) { m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) { if (e.RightIsDown()) {
@@ -1066,12 +1081,13 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
}); });
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) { m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press; const wxPoint d = e.GetPosition() - m_ctx_press;
// Always read-and-clear, even when another guard already rules the offer out, or a // Click, or navigation? A press that travelled panned or orbited; one that did not, did not.
// terminator recorded under one condition would still be pending under the next.
const bool terminated = m_sketch_tool.take_right_consumed();
// Click, or navigation? A press that travelled orbited; one that did not, did not.
const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx; const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx;
if (m_on_context_menu && !terminated && !inline_busy() && is_click) { // Ending a chain or abandoning an anchor uses the click up.
const bool terminated = is_click && m_canvas && m_sketch_tool.take_right_click(*m_canvas);
if (terminated)
m_canvas->set_as_dirty(); // drawn by the canvas's own RightUp (e.Skip below) or at idle
else if (m_on_context_menu && !inline_busy() && is_click) {
// The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not // The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not
// at the release, so the raycast uses the press position. Within a 3 px budget the // at the release, so the raycast uses the press position. Within a 3 px budget the
// two are the same pixel in practice; using the press is what makes that a // two are the same pixel in practice; using the press is what makes that a
@@ -1167,9 +1183,6 @@ void DesignCanvas::set_readout(const std::string& text)
if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one) if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one)
} }
// Clear of the view cube and the two round view buttons, which own the bottom-left corner.
static constexpr float kStatusHudLeftInset = 190.f;
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour) void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{ {
if (text == m_status_hud_last && colour == m_status_hud_colour) return; if (text == m_status_hud_last && colour == m_status_hud_colour) return;
@@ -1204,8 +1217,11 @@ void DesignCanvas::render_hud()
ImGuiWrapper::pop_common_window_style(); ImGuiWrapper::pop_common_window_style();
}; };
if (!m_status_hud_last.IsEmpty()) { if (!m_status_hud_last.IsEmpty()) {
// Past the view cube and the round view buttons, which own the bottom-left corner. Asked
// of the canvas, which lays them out: they follow the monitor's DPI on Windows, where `em`
// does not, so a fixed inset in `em` let them cover the start of the line at 150%.
// A sentence can be a sentence: it wraps to the room left of the readout chip. // A sentence can be a sentence: it wraps to the room left of the readout chip.
const float left = kStatusHudLeftInset * em; const float left = m_canvas->get_canvas_toolbar_right() + margin;
const ImVec4 col = m_status_hud_colour.IsOk() const ImVec4 col = m_status_hud_colour.IsOk()
? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f, ? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f,
m_status_hud_colour.Blue() / 255.f, 1.f) m_status_hud_colour.Blue() / 255.f, 1.f)
+6 -2
View File
@@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include <utility> #include <utility>
#include <wx/colour.h> #include <wx/colour.h>
@@ -48,10 +49,11 @@ public:
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour). // Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept // `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept
// by address (a stable panel member) and read again whenever the selection changes. // by address (a stable panel member) and read again whenever the selection changes. The
// bodies' edge lines are refreshed with them (DesignSketchTool::refresh_body_edges).
void set_bodies(const std::vector<TriangleMesh>* body_meshes, void set_bodies(const std::vector<TriangleMesh>* body_meshes,
const std::vector<bool>& visible = {}); const std::vector<bool>& visible = {});
void clear_mesh(); void clear_mesh(); // no bodies: drops their volumes and the tool's edge lines, pick and selection
void set_preview_mesh(const TriangleMesh& mesh); void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview(); void clear_preview();
@@ -83,6 +85,8 @@ public:
void finish_sketch(); void finish_sketch();
bool is_sketching() const; bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation) void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
// Centre of the Design bed: the printer bed at its home position, whichever plate is current.
Vec2d bed_center() const { return m_bed.build_volume().bed_center(); }
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no // The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs // per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the // show. Exactly one of the two views is live at a time, so entering and leaving are the
+23 -9
View File
@@ -104,7 +104,6 @@
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Plater.hpp"
#include "libslic3r/BuildVolume.hpp"
#include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Shortcuts.hpp" #include "slic3r/GUI/Shortcuts.hpp"
@@ -7345,8 +7344,12 @@ void DesignPanel::hydrate_from_model()
// that has none yet. A design in progress keeps its origin: its sketches have it baked into // that has none yet. A design in progress keeps its origin: its sketches have it baked into
// their planes, so moving it under them (a printer change between visits) would slide the // their planes, so moving it under them (a printer change between visits) would slide the
// datum planes off the geometry. A loaded project brings its own (load_recipe). // datum planes off the geometry. A loaded project brings its own (load_recipe).
// The centre of the Design bed, not the plater's: the plater moves its bed onto the current
// plate, so on plate 2+ the origin would land one plate stride away from the bed drawn here.
// Re-synced first because the control socket calls this without on_tab_shown().
if (!m_doc.origin_from_recipe) { if (!m_doc.origin_from_recipe) {
const Vec2d bc = plater->build_volume().bed_center(); m_viewport->refresh_bed();
const Vec2d bc = m_viewport->bed_center();
m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0); m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0);
// A document started here takes the weld rule from the preference; a loaded one // A document started here takes the weld rule from the preference; a loaded one
// brings its own (load_recipe). // brings its own (load_recipe).
@@ -7893,8 +7896,7 @@ void DesignPanel::set_tree_selection(int row)
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and // The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
// features by index, so replacing or renumbering the feature list (undo/redo, New Design, load, // features by index, so replacing or renumbering the feature list (undo/redo, New Design, load,
// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild // delete, reorder) drops it, the viewport's highlights with it. The callers repaint.
// that leaves no body never reaches set_solid_pick. The callers repaint.
void DesignPanel::drop_selection() void DesignPanel::drop_selection()
{ {
if (m_viewport != nullptr) if (m_viewport != nullptr)
@@ -8090,8 +8092,18 @@ void DesignPanel::on_toggle_visibility()
refresh_tree(); // greys the row refresh_tree(); // greys the row
set_tree_selection(sel); // keep the toggled feature selected set_tree_selection(sel); // keep the toggled feature selected
if (m_viewport != nullptr) { if (m_viewport != nullptr) {
if (m_doc.display_mesh.its.indices.empty()) m_viewport->clear_mesh(); if (m_doc.display_mesh.its.indices.empty()) {
else feed_bodies(); m_viewport->clear_mesh();
} else {
// Not set_status_ok(), which would take the gizmo from a Move still open.
feed_bodies();
// Hiding or showing a feature rebuilds the bodies and can renumber them, so the pick is
// re-pointed and its selection dropped. That also brings picking back for a body shown
// after everything was hidden (clear_mesh() dropped it).
m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh,
&m_doc.display_tri_face, &m_doc.display_tri_body,
&m_body_visible, &m_body_xform);
}
} }
sync_sketch_display(); // skips the hidden sketch + direct-renders sync_sketch_display(); // skips the hidden sketch + direct-renders
set_status(StatusKind::Info, shown ? _L("Feature shown") : _L("Feature hidden")); set_status(StatusKind::Info, shown ? _L("Feature shown") : _L("Feature hidden"));
@@ -11185,10 +11197,12 @@ void DesignPanel::refresh_preview()
// Confirm so the user sees the gate before clicking; the red status says why. // Confirm so the user sees the gate before clicking; the red status says why.
m_candidate_ok = ok; m_candidate_ok = ok;
update_confirm_button(); update_confirm_button();
// Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the // Fillet/Chamfer/Draft/Hole: once the target edge/face yields a valid result, show ONLY the
// preview (hide the base bodies) so the user sees the finished shape, not the old solid // preview (hide the base bodies) so the user sees the finished shape, not the old solid
// doubled with the ghost. Before a valid pick the body stays visible so it can be picked. // doubled with the ghost. A hole's cut lies inside the old solid, so without this the body
m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft) && ok); // would hide it entirely. Before a valid pick the body stays visible so it can be picked.
m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft ||
m_active == Tool::Hole) && ok);
m_status->Refresh(); m_status->Refresh();
// Refresh the in-canvas Extrude depth arrow (self-gates: only while the Extrude card is open). // Refresh the in-canvas Extrude depth arrow (self-gates: only while the Extrude card is open).
+374 -91
View File
@@ -53,6 +53,7 @@
#include <cstdio> #include <cstdio>
#include <cstdarg> #include <cstdarg>
#include <cstdlib> #include <cstdlib>
#include <iterator>
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp"
@@ -77,6 +78,7 @@ static bool point_in_poly(const Vec2d& q, const std::vector<Vec2d>& poly);
static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, const Vec3d& v0, const Vec3d& v1, static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, const Vec3d& v0, const Vec3d& v1,
const Vec3d& v2, double& t); const Vec3d& v2, double& t);
static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b); static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b);
static double ray_axis_proj(const Linef3& ray, const Vec3d& anchor, const Vec3d& dir);
// Project a world-space point to canvas screen pixels (device px, GL viewport units; // Project a world-space point to canvas screen pixels (device px, GL viewport units;
// origin top-left after the GL y-flip). Mirrors GLCanvas3D's world->screen pattern: // origin top-left after the GL y-flip). Mirrors GLCanvas3D's world->screen pattern:
@@ -3473,18 +3475,19 @@ static double display_edge_tol(const TopoDS_Shape& shape)
void DesignSketchTool::refresh_body_edges() void DesignSketchTool::refresh_body_edges()
{ {
const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0; const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0;
bool rebuilt = n < m_body_edges_shape.size(); // a body went away
m_body_edges.resize(n); m_body_edges.resize(n);
m_body_edges_key.resize(n, nullptr); m_body_edges_shape.resize(n);
m_body_edges_tol.resize(n, 0.0); m_body_edges_tol.resize(n, 0.0);
for (size_t b = 0; b < n; ++b) { for (size_t b = 0; b < n; ++b) {
const void* key = body_key(int(b)); if (is_current_shape(int(b), m_body_edges_shape[b]))
if (key == m_body_edges_key[b] && key != nullptr)
continue;
m_body_edges_key[b] = key;
m_body_edges[b].clear();
if (key == nullptr)
continue; continue;
rebuilt = rebuilt || !m_body_edges_shape[b].IsNull(); // a body sampled before was rebuilt
const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape; const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape;
m_body_edges_shape[b] = shape;
m_body_edges[b].clear();
if (shape.IsNull())
continue;
m_body_edges_tol[b] = display_edge_tol(shape); m_body_edges_tol[b] = display_edge_tol(shape);
try { try {
m_body_edges[b] = GeometryEngine::display_edges(shape, m_body_edges_tol[b]); m_body_edges[b] = GeometryEngine::display_edges(shape, m_body_edges_tol[b]);
@@ -3492,15 +3495,36 @@ void DesignSketchTool::refresh_body_edges()
m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else
} }
} }
// A pick expires once the body it was taken on is rebuilt, which renumbers its faces and
// edges. Judged by that body's shape, not by these lines: a recompute that refreshed no lines
// (adding a datum, say) may come before a pick on the bodies it built, and that pick holds.
// The hover is resolved again on the next mouse move, so any rebuild just drops it.
if (m_solid_sel != SolidSel::None && !is_current_shape(m_sel_body, m_sel_shape))
clear_solid_selection();
else if (rebuilt)
m_pre = SolidPick{};
// The face outlines of a rebuilt body let go of its shape, which may be a large import.
const auto stale = [this](const FaceHighlight& h) { return !is_current_shape(h.body, h.shape); };
if (stale(m_sel_hl)) m_sel_hl = FaceHighlight{};
if (stale(m_pre_hl)) m_pre_hl = FaceHighlight{};
m_hl_faces.erase(std::remove_if(m_hl_faces.begin(), m_hl_faces.end(), stale), m_hl_faces.end());
} }
// The identity of a body's current shape. Face ids and sampled edges hold while it does. const TopoDS_Shape* DesignSketchTool::body_shape(int body) const
const void* DesignSketchTool::body_key(int body) const
{ {
if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size())) if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size()))
return nullptr; return nullptr;
const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape; const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape;
return shape.IsNull() ? nullptr : shape.TShape().get(); return shape.IsNull() ? nullptr : &shape;
}
// Whether `sampled` is still the body's shape. The caches hold the shape itself rather than its
// address, so the sampled TShape stays alive and no rebuilt body can be allocated in its place.
// IsSame also compares the Location, which the sampled lines are in.
bool DesignSketchTool::is_current_shape(int body, const TopoDS_Shape& sampled) const
{
const TopoDS_Shape* shape = body_shape(body);
return shape != nullptr && sampled.IsSame(*shape);
} }
// View-facing ribbons along a body's polylines (shape coordinates), `hw` either side of the line // View-facing ribbons along a body's polylines (shape coordinates), `hw` either side of the line
@@ -3603,6 +3627,7 @@ void DesignSketchTool::clear_solid_selection()
{ {
m_solid_sel = SolidSel::None; m_solid_sel = SolidSel::None;
m_sel_body = m_sel_face = m_sel_edge = -1; m_sel_body = m_sel_face = m_sel_edge = -1;
m_sel_shape = TopoDS_Shape();
m_sel_edge_pts.clear(); m_sel_edge_pts.clear();
m_sel_edges_more.clear(); m_sel_edges_more.clear();
m_sel_edges_more_pts.clear(); m_sel_edges_more_pts.clear();
@@ -3630,6 +3655,7 @@ void DesignSketchTool::select_body(int body)
return; return;
} }
m_sel_body = body; m_sel_body = body;
m_sel_shape = (*m_solid_bodies)[body].shape;
m_sel_face = m_sel_edge = -1; m_sel_face = m_sel_edge = -1;
m_sel_edge_pts.clear(); m_sel_edge_pts.clear();
m_sel_edges_more.clear(); m_sel_edges_more.clear();
@@ -3641,7 +3667,7 @@ void DesignSketchTool::set_highlight_faces(const std::vector<std::pair<int, int>
{ {
std::map<int, std::vector<int>> by_body; std::map<int, std::vector<int>> by_body;
for (const auto& [b, f] : faces) for (const auto& [b, f] : faces)
if (body_key(b) != nullptr) if (body_shape(b) != nullptr)
by_body[b].push_back(f); by_body[b].push_back(f);
// Reuse each body's entry, so re-sending the same faces resamples nothing. // Reuse each body's entry, so re-sending the same faces resamples nothing.
std::vector<FaceHighlight> next; std::vector<FaceHighlight> next;
@@ -3663,13 +3689,16 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body,
std::sort(faces.begin(), faces.end()); std::sort(faces.begin(), faces.end());
faces.erase(std::unique(faces.begin(), faces.end()), faces.end()); faces.erase(std::unique(faces.begin(), faces.end()), faces.end());
h.faces = std::move(faces); h.faces = std::move(faces);
h.key = body_key(body); const TopoDS_Shape* shape = body_shape(body);
if (h.key == nullptr || h.faces.empty()) if (shape == nullptr)
return h;
h.shape = *shape;
if (h.faces.empty())
return h; return h;
// Sampled at the body edges' tolerance; a whole body reuses its body edges. // Sampled at the body edges' tolerance; a whole body reuses its body edges.
const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape; const std::vector<TopoDS_Face> all = GeometryEngine::faces_of(*shape);
const std::vector<TopoDS_Face> all = GeometryEngine::faces_of(shape); const bool cached = body < int(m_body_edges_shape.size())
const bool cached = body < int(m_body_edges_key.size()) && m_body_edges_key[body] == h.key; && is_current_shape(body, m_body_edges_shape[body]);
if (cached && h.faces.size() == all.size() && h.faces.front() == 0 && h.faces.back() == int(all.size()) - 1) { if (cached && h.faces.size() == all.size() && h.faces.front() == 0 && h.faces.back() == int(all.size()) - 1) {
h.edges = m_body_edges[body]; h.edges = m_body_edges[body];
return h; return h;
@@ -3681,7 +3710,7 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body,
if (f >= 0 && f < int(all.size())) if (f >= 0 && f < int(all.size()))
builder.Add(picked, all[f]); builder.Add(picked, all[f]);
try { try {
h.edges = GeometryEngine::display_edges(picked, cached ? m_body_edges_tol[body] : display_edge_tol(shape)); h.edges = GeometryEngine::display_edges(picked, cached ? m_body_edges_tol[body] : display_edge_tol(*shape));
} catch (const Standard_Failure&) { } catch (const Standard_Failure&) {
h.edges.clear(); // an edge that cannot be sampled costs the outline, never the fill h.edges.clear(); // an edge that cannot be sampled costs the outline, never the fill
} }
@@ -3691,12 +3720,13 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body,
std::vector<std::pair<int, int>> DesignSketchTool::picked_faces() const std::vector<std::pair<int, int>> DesignSketchTool::picked_faces() const
{ {
std::vector<std::pair<int, int>> out; std::vector<std::pair<int, int>> out;
if (body_key(m_sel_body) == nullptr) const TopoDS_Shape* shape = body_shape(m_sel_body);
if (shape == nullptr)
return out; return out;
if (m_solid_sel == SolidSel::Face && m_sel_face >= 0) if (m_solid_sel == SolidSel::Face && m_sel_face >= 0)
out.emplace_back(m_sel_body, m_sel_face); out.emplace_back(m_sel_body, m_sel_face);
else if (m_solid_sel == SolidSel::Whole) else if (m_solid_sel == SolidSel::Whole)
for (int f = 0, n = GeometryEngine::face_count((*m_solid_bodies)[m_sel_body].shape); f < n; ++f) for (int f = 0, n = GeometryEngine::face_count(*shape); f < n; ++f)
out.emplace_back(m_sel_body, f); out.emplace_back(m_sel_body, f);
return out; return out;
} }
@@ -3705,7 +3735,7 @@ std::vector<std::pair<int, int>> DesignSketchTool::selected_faces() const
{ {
std::vector<std::pair<int, int>> out = picked_faces(); std::vector<std::pair<int, int>> out = picked_faces();
for (const FaceHighlight& h : m_hl_faces) for (const FaceHighlight& h : m_hl_faces)
if (h.key != nullptr && h.key == body_key(h.body)) if (is_current_shape(h.body, h.shape))
for (int f : h.faces) for (int f : h.faces)
out.emplace_back(h.body, f); out.emplace_back(h.body, f);
std::sort(out.begin(), out.end()); std::sort(out.begin(), out.end());
@@ -3713,12 +3743,83 @@ std::vector<std::pair<int, int>> DesignSketchTool::selected_faces() const
return out; return out;
} }
BoundingBoxf3 DesignSketchTool::fit_box(const GLVolumeCollection& volumes) const
{
BoundingBoxf3 box;
const auto add_entity = [&box, this](const SketchEntity& e, const SketchPlane& plane) {
bool closed = false;
for (const Vec2d& p : entity_polyline(e, closed))
box.merge(plane.to_world(p));
};
const auto add_entities = [&add_entity](const std::vector<SketchEntity>& ents, const SketchPlane& plane) {
for (const SketchEntity& e : ents)
add_entity(e, plane);
};
// The selected faces, a whole body's included, from the pick mesh: it is in world
// coordinates already, moved bodies and all.
const std::vector<std::pair<int, int>> faces = selected_faces();
if (!faces.empty() && m_solid_mesh != nullptr && m_solid_tri_face != nullptr && m_solid_tri_body != nullptr) {
const indexed_triangle_set& its = m_solid_mesh->its;
const size_t n = std::min({ its.indices.size(), m_solid_tri_face->size(), m_solid_tri_body->size() });
for (size_t t = 0; t < n; ++t)
if (std::binary_search(faces.begin(), faces.end(), std::make_pair((*m_solid_tri_body)[t], (*m_solid_tri_face)[t])))
for (int i = 0; i < 3; ++i)
box.merge(its.vertices[its.indices[t][i]].cast<double>());
}
if (m_solid_sel == SolidSel::Edge) {
for (const Vec3d& p : m_sel_edge_pts)
box.merge(p);
for (const std::vector<Vec3d>& pts : m_sel_edges_more_pts)
for (const Vec3d& p : pts)
box.merge(p);
} else if (m_solid_sel == SolidSel::Vertex)
box.merge(m_sel_vertex_pt);
// A committed sketch's picked region with its holes; a stroke outside any region, the sketch.
if (m_display_pick >= 0)
for (const DisplaySketch& d : m_display_sketches)
if (d.feature == m_display_pick)
add_entities(m_display_pick_region >= 0 ? selected_loop_entities() : d.entities, d.plane);
if (m_active) {
for (int i : m_selection)
if (i >= 0 && i < int(m_entities.size()))
add_entity(m_entities[i], m_plane);
for (const auto& [i, role] : m_point_sel)
if (i >= 0 && i < int(m_entities.size())) {
const SketchEntity& e = m_entities[i];
box.merge(m_plane.to_world(role == SketchPointRole::Center ? e.center :
role == SketchPointRole::P1 ? e.p1 : e.p0));
}
}
// Nothing selected: everything on show. A hidden body's volume is inactive.
if (!box.defined) {
for (const GLVolume* v : volumes.volumes)
if (v->is_active)
box.merge(v->transformed_bounding_box());
for (const DisplaySketch& d : m_display_sketches)
add_entities(d.entities, d.plane);
if (m_active)
add_entities(m_entities, m_plane);
}
// A vertex has no size, nor has a sketch along its normal, and the camera cannot frame an
// extent it does not see. Every axis gets at least a small neighbourhood.
if (box.defined) {
constexpr double min_extent = 10.; // mm
const Vec3d grow = (Vec3d::Constant(min_extent) - box.size()).cwiseMax(0.) * 0.5;
box.min -= grow;
box.max += grow;
}
return box;
}
const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(FaceHighlight& cache, int body, const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(FaceHighlight& cache, int body,
std::vector<int> faces) const std::vector<int> faces) const
{ {
std::sort(faces.begin(), faces.end()); std::sort(faces.begin(), faces.end());
faces.erase(std::unique(faces.begin(), faces.end()), faces.end()); faces.erase(std::unique(faces.begin(), faces.end()), faces.end());
if (cache.body != body || cache.faces != faces || cache.key != body_key(body)) if (cache.body != body || cache.faces != faces || !is_current_shape(body, cache.shape))
cache = make_face_highlight(body, std::move(faces)); cache = make_face_highlight(body, std::move(faces));
return cache; return cache;
} }
@@ -3729,7 +3830,7 @@ const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(F
// version: the line alone, thinner and fainter. // version: the line alone, thinner and fainter.
void DesignSketchTool::render_face_outline(const FaceHighlight& h, bool quiet) void DesignSketchTool::render_face_outline(const FaceHighlight& h, bool quiet)
{ {
if (m_body_edges_hidden || h.key == nullptr || h.key != body_key(h.body) || !body_pickable(h.body)) if (m_body_edges_hidden || !is_current_shape(h.body, h.shape) || !body_pickable(h.body))
return; // hidden, or the body was rebuilt and these face ids are stale return; // hidden, or the body was rebuilt and these face ids are stale
const Camera& cam = wxGetApp().plater()->get_camera(); const Camera& cam = wxGetApp().plater()->get_camera();
@@ -4006,6 +4107,7 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent
m_sel_edges_more_pts.clear(); m_sel_edges_more_pts.clear();
m_sel_body = p.body; m_sel_body = p.body;
m_sel_shape = (*m_solid_bodies)[p.body].shape; // resolve_solid_pick checked p.body
m_sel_face = p.face; m_sel_face = p.face;
m_sel_edge = p.edge; m_sel_edge = p.edge;
m_sel_edge_pts = std::move(p.edge_pts); m_sel_edge_pts = std::move(p.edge_pts);
@@ -5284,6 +5386,144 @@ void DesignSketchTool::drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& e
} }
// ---- Reference/base planes (Onshape-style default planes) ----------------------------- // ---- Reference/base planes (Onshape-style default planes) -----------------------------
namespace {
// One level of a BSP tree whose splitters are the planes themselves, in order: the painter's
// algorithm made exact for polygons that cross. Pieces of plane k (and of any plane lying in it)
// are in the splitter; every other piece is wholly on one side or is cut in two. Far side, then the
// splitter's own pieces, then the near side is back to front. A piece is only cut when it has
// corners strictly on both sides, so neither half can be degenerate.
void paint_back_to_front(std::vector<PlanePiece>&& in, size_t k, const std::vector<SketchPlane>& planes, double eps,
const Vec3d& eye, const Vec3d& forward, bool perspective, std::vector<PlanePiece>& out)
{
if (in.empty())
return;
if (k == planes.size()) { // not reached: every piece is in the splitter at its own plane's level
std::move(in.begin(), in.end(), std::back_inserter(out));
return;
}
// The square's own normal: SketchPlane::normal is reversed on XZ, and the side tests only need
// one consistent choice.
const Vec3d n = planes[k].x_axis.cross(planes[k].y_axis).normalized();
const double d = n.dot(planes[k].origin);
std::vector<PlanePiece> front, back, on;
for (PlanePiece& piece : in) {
if (piece.plane == int(k)) {
on.push_back(std::move(piece));
continue;
}
std::vector<double> s;
int sides = 0;
for (const Vec3d& c : piece.corners) {
s.push_back(n.dot(c) - d);
sides |= s.back() > eps ? 1 : s.back() < -eps ? 2 : 0;
}
if (sides == 0)
on.push_back(std::move(piece));
else if (sides == 1)
front.push_back(std::move(piece));
else if (sides == 2)
back.push_back(std::move(piece));
else {
PlanePiece f{ piece.plane, {} }, b{ piece.plane, {} };
const size_t m = piece.corners.size();
for (size_t i = 0; i < m; ++i) {
const size_t j = (i + 1) % m;
const Vec3d& a = piece.corners[i];
if (s[i] >= -eps) f.corners.push_back(a);
if (s[i] <= eps) b.corners.push_back(a);
if ((s[i] > eps && s[j] < -eps) || (s[i] < -eps && s[j] > eps)) {
const Vec3d x = a + (piece.corners[j] - a) * (s[i] / (s[i] - s[j]));
f.corners.push_back(x);
b.corners.push_back(x);
}
}
front.push_back(std::move(f));
back.push_back(std::move(b));
}
}
// An orthographic eye is at infinity behind the view direction. Camera::get_position() is a
// finite point there and can sit on the wrong side of a plane, so only the direction counts.
const bool eye_in_front = perspective ? n.dot(eye) - d > 0. : n.dot(forward) < 0.;
paint_back_to_front(std::move(eye_in_front ? back : front), k + 1, planes, eps, eye, forward, perspective, out);
std::move(on.begin(), on.end(), std::back_inserter(out));
paint_back_to_front(std::move(eye_in_front ? front : back), k + 1, planes, eps, eye, forward, perspective, out);
}
Vec2d in_frame(const SketchPlane& p, const Vec3d& x) { return Vec2d((x - p.origin).dot(p.x_axis), (x - p.origin).dot(p.y_axis)); }
double plane_distance(const SketchPlane& p, const Vec3d& x) { return p.x_axis.cross(p.y_axis).normalized().dot(x - p.origin); }
// Shrink the segment ab, which lies in p, to its part inside p's square (Liang-Barsky). False when
// nothing is left.
bool clip_to_square(Vec3d& a, Vec3d& b, const SketchPlane& p, double half, double eps)
{
const Vec2d s = in_frame(p, a), d = in_frame(p, b) - s;
double t0 = 0., t1 = 1.;
for (int axis = 0; axis < 2; ++axis)
for (double sign : { -1., 1. }) { // keep sign * (s + t * d)[axis] <= half
const double room = half + eps - sign * s[axis], rate = sign * d[axis];
if (rate > 0.)
t1 = std::min(t1, room / rate);
else if (rate < 0.)
t0 = std::max(t0, room / rate);
else if (room < 0.)
return false;
}
if (t1 - t0 < 1e-9)
return false;
const Vec3d ab = b - a;
b = a + ab * t1;
a = a + ab * t0;
return true;
}
} // namespace
std::vector<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& planes, double half,
const Vec3d& eye, const Vec3d& forward, bool perspective)
{
std::vector<PlanePiece> squares;
for (int i = 0; i < int(planes.size()); ++i) {
const SketchPlane& p = planes[i];
squares.push_back({ i, { p.to_world(Vec2d(-half, -half)), p.to_world(Vec2d(half, -half)),
p.to_world(Vec2d(half, half)), p.to_world(Vec2d(-half, half)) } });
}
const double eps = 1e-6 * std::max(half, 1.);
std::vector<PlanePiece> out;
paint_back_to_front(std::move(squares), 0, planes, eps, eye, forward, perspective, out);
// What to outline: the piece's share of its square's border, and of where it crosses another
// square. Every other edge is a cut lying in the plane that made it, but the cut ran along that
// whole infinite plane, so it is clipped to that plane's square: a datum that never reaches a base
// plane gets no line across it. A piece is convex, so an edge with both ends on one side line of
// its square lies along that side.
for (PlanePiece& piece : out) {
const SketchPlane& own = planes[piece.plane];
for (size_t i = 0; i < piece.corners.size(); ++i) {
const Vec3d& a = piece.corners[i];
const Vec3d& b = piece.corners[(i + 1) % piece.corners.size()];
const Vec2d fa = in_frame(own, a), fb = in_frame(own, b);
bool border = false;
for (int axis = 0; axis < 2; ++axis)
for (double side : { -half, half })
border = border || (std::abs(fa[axis] - side) <= eps && std::abs(fb[axis] - side) <= eps);
if (border) {
piece.lines.emplace_back(a, b);
continue;
}
for (int k = 0; k < int(planes.size()); ++k) {
auto in_k = [&](const Vec3d& x) { return std::abs(plane_distance(planes[k], x)) <= 2. * eps; };
// Not a plane the piece itself lies in: clipped to its own square, a cut is kept whole.
if (k == piece.plane || !in_k(a) || !in_k(b) || std::all_of(piece.corners.begin(), piece.corners.end(), in_k))
continue;
Vec3d ca = a, cb = b;
if (clip_to_square(ca, cb, planes[k], half, eps))
piece.lines.emplace_back(ca, cb);
}
}
}
return out;
}
void DesignSketchTool::set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases, void DesignSketchTool::set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
std::vector<std::string> labels) std::vector<std::string> labels)
{ {
@@ -5319,49 +5559,72 @@ double DesignSketchTool::dbp_half_extent() const
return half; return half;
} }
// Draw the reference planes as large translucent labelled squares; the hovered one brightens. // Draw the reference planes as labelled translucent squares outlined in their own hue; the hovered
// one brightens. Depth testing is off (they overlay the bed and any bodies), so draw order is the
// blend order — and the planes cross, so they go down piece by piece, back to front.
void DesignSketchTool::render_base_pick() void DesignSketchTool::render_base_pick()
{ {
if (!m_dbp_active || m_dbp_planes.empty()) return; if (!m_dbp_active || m_dbp_planes.empty()) return;
using EPT = GLModel::Geometry::EPrimitiveType; using EPT = GLModel::Geometry::EPrimitiveType;
using EVL = GLModel::Geometry::EVertexLayout; using EVL = GLModel::Geometry::EVertexLayout;
const double H = dbp_half_extent(); 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). // Two layers of alpha a blended in either order differ by only a^2 of their colour difference,
auto tint = [](int base, bool hot) -> ColorRGBA { // so a faint fill hides which plane is in front however well the pieces are sorted: at 0.16 that
float a = hot ? 0.10f : 0.047f; // base planes kept faint (reduced ~2/3 from 0.30/0.14) // is under 3%, and the crossing planes read as one grey smear. At 0.35 it is ~12%, and the bed
if (base == 0) return ColorRGBA(0.30f, 0.55f, 0.95f, a); // grid still reads through all three.
if (base == 1) return ColorRGBA(0.35f, 0.80f, 0.45f, a); constexpr float kFillAlpha = 0.35f, kFillAlphaHot = 0.50f;
if (base == 2) return ColorRGBA(0.92f, 0.42f, 0.42f, a); constexpr float kLineAlpha = 0.90f, kLineAlphaHot = 1.00f;
return ColorRGBA(0.70f, 0.72f, 0.78f, a); // Onshape-ish per-plane hues: XY blue, XZ green, YZ red (keyed by base index 0/1/2; datums grey).
auto hue = [](int base) -> ColorRGBA {
if (base == 0) return ColorRGBA(0.30f, 0.55f, 0.95f, 1.0f);
if (base == 1) return ColorRGBA(0.35f, 0.80f, 0.45f, 1.0f);
if (base == 2) return ColorRGBA(0.92f, 0.42f, 0.42f, 1.0f);
return ColorRGBA(0.70f, 0.72f, 0.78f, 1.0f);
}; };
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); // outline ribbon, as on datum planes
glsafe(::glDisable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_DEPTH_TEST));
glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glDisable(GL_CULL_FACE));
glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
const SketchPlane saved_plane = m_plane; for (const PlanePiece& piece : planes_back_to_front(m_dbp_planes, H, cam.get_position(), vd,
for (size_t i = 0; i < m_dbp_planes.size(); ++i) { cam.get_type() == Camera::EType::Perspective)) {
const SketchPlane& p = m_dbp_planes[i]; const bool hot = piece.plane == m_dbp_hover;
const Vec3d q0 = p.to_world(Vec2d(-H, -H)), q1 = p.to_world(Vec2d(H, -H)), ColorRGBA col = hue(piece.plane < int(m_dbp_base.size()) ? m_dbp_base[piece.plane] : -1);
q2 = p.to_world(Vec2d(H, H)), q3 = p.to_world(Vec2d(-H, H)); GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 };
GLModel::Geometry quad; quad.format = { EPT::Triangles, EVL::P3 }; for (const Vec3d& q : piece.corners) fill.add_vertex((Vec3f)q.cast<float>());
quad.add_vertex((Vec3f)q0.cast<float>()); quad.add_vertex((Vec3f)q1.cast<float>()); for (unsigned int i = 1; i + 1 < piece.corners.size(); ++i) fill.add_triangle(0, i, i + 1); // convex: a fan
quad.add_vertex((Vec3f)q2.cast<float>()); quad.add_vertex((Vec3f)q3.cast<float>()); GLModel fm; fm.init_from(std::move(fill));
quad.add_triangle(0, 1, 2); quad.add_triangle(0, 2, 3); col.a(hot ? kFillAlphaHot : kFillAlpha);
GLModel m; m.init_from(std::move(quad)); fm.set_color(col);
const bool hot = (int(i) == m_dbp_hover); fm.render();
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). // Its outline and crossing lines go down with it, so a line behind another plane is tinted
if (i < m_dbp_labels.size() && !m_dbp_labels[i].empty()) { // by it exactly like the plane it lies on.
m_plane = p; std::vector<std::vector<Vec3d>> strokes;
const double th = H * 0.10; for (const auto& [a, b] : piece.lines) strokes.push_back({ a, b });
const ColorRGBA lc = tint(base, true); ColorRGBA lcs(lc.r(), lc.g(), lc.b(), 1.0f); GLModel::Geometry outline;
draw_text(m_line_model, m_dbp_labels[i], Vec2d(-H + th * 2.0, H - th * 1.6), th, lcs); append_ribbons(outline, -1, strokes, vd, Vec3d::Zero(), hw); // body -1: already world coordinates
if (!outline.is_empty()) {
GLModel om; om.init_from(std::move(outline));
col.a(hot ? kLineAlphaHot : kLineAlpha);
om.set_color(col);
om.render();
} }
} }
m_plane = saved_plane; // draw_text renders each label immediately (draw_strokes self-renders)
// Label near the top-left corner, drawn in the plane (draw_text lifts through m_plane). Labels
// are ImGui chips, on top of the planes whatever the order here.
const SketchPlane saved_plane = m_plane;
const double th = H * 0.10;
for (size_t i = 0; i < m_dbp_planes.size() && i < m_dbp_labels.size(); ++i) {
if (m_dbp_labels[i].empty()) continue;
m_plane = m_dbp_planes[i];
draw_text(m_line_model, m_dbp_labels[i], Vec2d(-H + th * 2.0, H - th * 1.6), th,
hue(i < m_dbp_base.size() ? m_dbp_base[i] : -1));
}
m_plane = saved_plane;
glsafe(::glDisable(GL_BLEND)); glsafe(::glDisable(GL_BLEND));
} }
@@ -5374,7 +5637,7 @@ int DesignSketchTool::hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent&
// THE LABEL WINS, and it has to. Each plane's name is a screen-space chip centred on its // THE LABEL WINS, and it has to. Each plane's name is a screen-space chip centred on its
// own in-plane anchor, and it is the one part of a base plane a user aims at deliberately — // own in-plane anchor, and it is the one part of a base plane a user aims at deliberately —
// the quads are near-transparent and overlap everywhere. Ray-casting the quads alone made // the quads are translucent and overlap everywhere. Ray-casting the quads alone made
// the labels pure decoration: on a fresh document at 1920x1060, clicking "XY" reported // the labels pure decoration: on a fresh document at 1920x1060, clicking "XY" reported
// "XZ plane selected", because the XZ quad happens to sit in front at that pixel. Nothing // "XZ plane selected", because the XZ quad happens to sit in front at that pixel. Nothing
// about the click was ambiguous to the user; they clicked the word XY. // about the click was ambiguous to the user; they clicked the word XY.
@@ -5557,19 +5820,22 @@ bool DesignSketchTool::hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEven
axis = best; return true; axis = best; return true;
} }
// Skew-line closest point of the mouse ray to the axis line through the ORIGINAL centroid // Record how far along the arrow it was grabbed (NaN while the camera looks down the axis; the
// -> signed offset along that axis (no clamp; a body can move either way). // first drag sample that projects stands in), so the body's centre does not snap to the grab.
void DesignSketchTool::drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis) void DesignSketchTool::grab_move_arrow(int axis, const Linef3& ray)
{ {
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); m_mv_drag = axis;
const Vec3d ro = r.a, rd = r.b - r.a; m_mv_grab_along = ray_axis_proj(ray, m_mv_base + m_mv_offset, Vec3d::Unit(axis));
const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; }
const Vec3d e = axes[axis];
const Vec3d w0 = m_mv_base - ro; // Slide the body along the grabbed axis by the cursor's travel (no clamp; it can move either way).
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); void DesignSketchTool::drag_move_arrow(const Linef3& ray)
const double denom = a * c - b * b; {
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave offset as-is if (m_mv_drag < 0 || m_mv_drag > 2) return;
m_mv_offset[axis] = (b * ee - c * dd) / denom; const double proj = ray_axis_proj(ray, m_mv_base + m_mv_offset, Vec3d::Unit(m_mv_drag));
if (std::isnan(proj)) return; // camera ∥ axis: leave offset as-is
if (std::isnan(m_mv_grab_along)) { m_mv_grab_along = proj; return; }
m_mv_offset[m_mv_drag] += proj - m_mv_grab_along;
if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform()); if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform());
} }
@@ -5939,21 +6205,10 @@ int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEven
return best; return best;
} }
// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along
// dir. NaN when the camera is ~parallel to the axis (no meaningful projection).
double DesignSketchTool::hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt, double DesignSketchTool::hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt,
const Vec3d& anchor, const Vec3d& dir) const const Vec3d& anchor, const Vec3d& dir) const
{ {
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); return ray_axis_proj(canvas.mouse_ray(Point(evt.GetX(), evt.GetY())), anchor, dir);
const Vec3d ro = r.a, rd = r.b - r.a;
const Vec3d e = dir;
const Vec3d w0 = 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;
// Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in
// top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through.
if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan("");
return (b * ee - c * dd) / denom;
} }
void DesignSketchTool::start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) void DesignSketchTool::start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
@@ -9917,6 +10172,21 @@ static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a
return (pr - ps).norm(); return (pr - ps).norm();
} }
// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along
// dir. NaN when the camera is ~parallel to the axis (no meaningful projection).
static double ray_axis_proj(const Linef3& ray, const Vec3d& anchor, const Vec3d& dir)
{
const Vec3d ro = ray.a, rd = ray.vector();
const Vec3d e = dir;
const Vec3d w0 = 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;
// Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in
// top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through.
if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan("");
return (b * ee - c * dd) / denom;
}
// Screen-plane distance from p to a sketch entity, for click picking in Constrain // Screen-plane distance from p to a sketch entity, for click picking in Constrain
// mode. Circles/arcs measure distance to the ring; points to their position. // mode. Circles/arcs measure distance to the ring; points to their position.
static double entity_pick_dist(const Vec2d& p, const SketchEntity& e) static double entity_pick_dist(const Vec2d& p, const SketchEntity& e)
@@ -10315,14 +10585,14 @@ std::vector<int> DesignSketchTool::connected_loop(int seed) const
// the offer was excluded in sketch mode wholesale so a right-click could end a polyline chain, // the offer was excluded in sketch mode wholesale so a right-click could end a polyline chain,
// abandon an anchor or exit a tool. That made every sketch row in the atlas unreachable. // abandon an anchor or exit a tool. That made every sketch row in the atlas unreachable.
// The honest test is not "which mode are we in" but "did the tool actually USE this right-click", // The honest test is not "which mode are we in" but "did the tool actually USE this right-click",
// and only the tool knows. Wrapping on_mouse records that once, for every terminator, instead of // and only the tool knows: take_right_click returns it for every terminator, from the
// threading a flag through the twenty-odd sites that consume a RightDown. // twenty-odd sites that consume a RightDown.
// Right-click abandons the anchor a draw tool has down. With NOTHING down there is nothing to // Right-click abandons the anchor a draw tool has down. With NOTHING down there is nothing to
// abandon — and consuming the click anyway made the offer unreachable from every armed draw tool: // abandon — and consuming the click anyway made the offer unreachable from every armed draw tool:
// on_mouse records the consumption in m_right_consumed and DesignCanvas's RIGHT_UP handler // DesignCanvas's RIGHT_UP handler suppresses the menu whenever the tool used the click, so
// suppresses the menu whenever it is set, so right-click became a no-op that also hid the one door // right-click became a no-op that also hid the one door to half the vocabulary (47 of 86 verbs
// to half the vocabulary (47 of 86 verbs have no shortcut). Measured on the rig: with Line armed, // have no shortcut). Measured on the rig: with Line armed, two right-clicks in a row produced no
// two right-clicks in a row produced no menu and no tool change; only Escape freed it. // menu and no tool change; only Escape freed it.
// Same rule as xmh6, which said it for the selection: clearing nothing is not a gesture // Same rule as xmh6, which said it for the selection: clearing nothing is not a gesture
// terminator. ghcz. // terminator. ghcz.
bool DesignSketchTool::right_abandon() bool DesignSketchTool::right_abandon()
@@ -10334,12 +10604,24 @@ bool DesignSketchTool::right_abandon()
return true; return true;
} }
// The camera follows Preferences > Control here as in Prepare, so no tool may take a gesture the
// camera owns. A right press may start whatever drag action the right button is given, and
// whether it did is known only at the release: ending a chain on the press made every pan or
// orbit started there end the chain too. The press goes to the camera and is kept;
// DesignCanvas's RIGHT_UP handler replays it through take_right_click when it was a click.
bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas) bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
{ {
const bool consumed = on_mouse_impl(evt, canvas); if (evt.RightDown()) {
if (evt.RightDown()) m_right_press = evt;
m_right_consumed = consumed; return false;
return consumed; }
return on_mouse_impl(evt, canvas);
}
bool DesignSketchTool::take_right_click(GLCanvas3D& canvas)
{
auto press = std::exchange(m_right_press, {});
return press && on_mouse_impl(*press, canvas);
} }
bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
@@ -10413,7 +10695,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
// exits move mode. A LeftDown that misses the arrows falls through to solid re-pick. // exits move mode. A LeftDown that misses the arrows falls through to solid re-pick.
if (m_mv_active) { if (m_mv_active) {
if (m_mv_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { if (m_mv_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
if (m_mv_drag < 3) drag_move_arrow(canvas, evt, m_mv_drag); if (m_mv_drag < 3) drag_move_arrow(canvas.mouse_ray(Point(evt.GetX(), evt.GetY())));
else drag_move_arc(canvas, evt, m_mv_drag - 3); else drag_move_arc(canvas, evt, m_mv_drag - 3);
return true; return true;
} }
@@ -10431,7 +10713,8 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
if (evt.LeftDown()) { if (evt.LeftDown()) {
int axis = -1; int axis = -1;
if (hit_test_move_arrow(canvas, evt, axis)) { // translate arrows win over rings if (hit_test_move_arrow(canvas, evt, axis)) { // translate arrows win over rings
m_mv_drag = axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY(); grab_move_arrow(axis, canvas.mouse_ray(Point(evt.GetX(), evt.GetY())));
m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY();
return true; return true;
} }
if (hit_test_move_arc(canvas, evt, axis)) { if (hit_test_move_arc(canvas, evt, axis)) {
@@ -11288,9 +11571,9 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
return true; return true;
} }
if (evt.RightDown()) { if (evt.RightDown()) {
// Hand the click back (return false) so the offer opens: the m_right_consumed flag // Hand the click back (return false) so the offer opens: take_right_click returns
// this return value feeds means "the tool USED this right-click", and a plain // this value as "the tool USED this right-click", and a plain right-click in Select
// right-click in Select mode is not a gesture terminator. // mode is not a gesture terminator.
// //
// But do NOT drop the selection on the way out. The offer menu describes WHAT IS // But do NOT drop the selection on the way out. The offer menu describes WHAT IS
// SELECTED, so clearing first guaranteed it could only ever describe nothing: select // SELECTED, so clearing first guaranteed it could only ever describe nothing: select
+46 -13
View File
@@ -2,26 +2,31 @@
#define slic3r_DesignSketchTool_hpp_ #define slic3r_DesignSketchTool_hpp_
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Line.hpp"
#include "libslic3r/CAD/SketchEngine.hpp" #include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking #include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking
#include <TopoDS_Shape.hxx>
#include "libslic3r/CAD/SketchInference.hpp" #include "libslic3r/CAD/SketchInference.hpp"
#include "slic3r/GUI/GLModel.hpp" #include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies #include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
#include <Eigen/Core> #include <Eigen/Core>
#include <cstddef> #include <cstddef>
#include <wx/event.h>
#include <functional> #include <functional>
#include <optional>
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include <math.h> #include <math.h>
#include <vector> #include <vector>
#include <string> #include <string>
#include <utility> #include <utility>
class wxMouseEvent;
class wxPoint; class wxPoint;
namespace Slic3r { namespace Slic3r {
class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer
class GLVolumeCollection;
namespace GUI { namespace GUI {
@@ -50,6 +55,22 @@ inline ColorRGBA design_idle_face_color()
{ {
return ColorRGBA(0.72f, 0.76f, 0.80f, 0.14f); return ColorRGBA(0.72f, 0.76f, 0.80f, 0.14f);
} }
// A convex piece of the square drawn on planes[plane], and the segments to outline with it: its share
// of the square's border and of the lines where it crosses the other squares.
struct PlanePiece
{
int plane;
std::vector<Vec3d> corners;
std::vector<std::pair<Vec3d, Vec3d>> lines;
};
// The squares of half-extent `half` on `planes`, cut where they cross one another and ordered back
// to front for an eye at `eye` (perspective) or looking along `forward` (orthographic). Translucent
// planes that cross cannot be drawn in any per-plane order: each is partly in front of and partly
// behind the others. Drawn piece by piece in this order, each one tints only what is behind it.
std::vector<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& planes, double half,
const Vec3d& eye, const Vec3d& forward, bool perspective);
class DesignSketchTool { class DesignSketchTool {
public: public:
enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect, enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect,
@@ -149,10 +170,10 @@ public:
// Right-click on a draw tool: true when an in-progress anchor was abandoned, false when // Right-click on a draw tool: true when an in-progress anchor was abandoned, false when
// there was nothing to abandon — and false is what lets the offer menu open. ghcz. // there was nothing to abandon — and false is what lets the offer menu open. ghcz.
bool right_abandon(); bool right_abandon();
// True if the LAST right-press was consumed as a gesture terminator (end a polyline chain, // Replays and clears the right press on_mouse kept for the camera: true if the tool used it as
// abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to // a gesture terminator (end a polyline chain, abandon an anchor), so no offer opens.
// decide whether that right-click was the user's, in which case it opens the offer. bool take_right_click(GLCanvas3D& canvas);
bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; } void drop_right_click() { m_right_press.reset(); }
void render(GLCanvas3D& canvas); void render(GLCanvas3D& canvas);
// The in-canvas value field, drawn by render() before any early return. Owned by // The in-canvas value field, drawn by render() before any early return. Owned by
// DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp. // DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp.
@@ -207,6 +228,9 @@ public:
// their edges do not float over the preview. // their edges do not float over the preview.
void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; } void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; }
void clear_solid_selection(); void clear_solid_selection();
// Resample the edges of each body whose shape changed and drop those of bodies that are gone.
// Also expires a pick or hover on a rebuilt body. The canvas calls it whenever it gets the bodies.
void refresh_body_edges();
bool has_solid_selection() const { return m_solid_sel != SolidSel::None; } bool has_solid_selection() const { return m_solid_sel != SolidSel::None; }
// Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes // Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes
// edges of the same body): the earlier picks first, the last-clicked edge at the end. // edges of the same body): the earlier picks first, the last-clicked edge at the end.
@@ -221,6 +245,11 @@ public:
void set_highlight_faces(const std::vector<std::pair<int, int>>& faces); void set_highlight_faces(const std::vector<std::pair<int, int>>& faces);
// Every face drawn as selected, sorted: the committed pick's and the still-valid ones above. // Every face drawn as selected, sorted: the committed pick's and the still-valid ones above.
std::vector<std::pair<int, int>> selected_faces() const; std::vector<std::pair<int, int>> selected_faces() const;
// What the canvas's Fit button frames: the selection — faces (the Feature tree's included), a
// body, edges, a vertex, a sketch region, the live sketch's picked entities — else everything
// on show: the visible bodies, the preview of the feature being edited and every sketch.
// Undefined when the tab shows none of it. `volumes` are the canvas's: bodies and preview.
BoundingBoxf3 fit_box(const GLVolumeCollection& volumes) const;
// Move-body gizmo (M5): translate a whole body with three world-axis drag arrows // Move-body gizmo (M5): translate a whole body with three world-axis drag arrows
// (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host // (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host
@@ -232,6 +261,9 @@ public:
void clear_move_gizmo(); void clear_move_gizmo();
bool moving_body() const { return m_mv_active; } bool moving_body() const { return m_mv_active; }
int move_body_index() const { return m_mv_body; } int move_body_index() const { return m_mv_body; }
// Press, then drag, translate arrow `axis` (0..2 = X/Y/Z) with the mouse ray under the cursor.
void grab_move_arrow(int axis, const Linef3& ray);
void drag_move_arrow(const Linef3& ray);
std::function<void(int body, const Transform3d& xform)> on_body_move_changed; std::function<void(int body, const Transform3d& xform)> on_body_move_changed;
// Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id). // Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id).
std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed; std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed;
@@ -1225,19 +1257,20 @@ private:
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
// The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read // The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read
// apart. One polyline set per body in its own shape coordinates, resampled only for a body // apart. One polyline set per body in its own shape coordinates, resampled only for a body
// whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute. // whose shape changed (see is_current_shape).
std::vector<std::vector<std::vector<Vec3d>>> m_body_edges; std::vector<std::vector<std::vector<Vec3d>>> m_body_edges;
std::vector<const void*> m_body_edges_key; std::vector<TopoDS_Shape> m_body_edges_shape; // the shape each set was sampled from
std::vector<double> m_body_edges_tol; // the chord tolerance they were sampled at std::vector<double> m_body_edges_tol; // the chord tolerance they were sampled at
bool m_body_edges_hidden{false}; bool m_body_edges_hidden{false};
void refresh_body_edges();
void render_body_edges(); void render_body_edges();
const void* body_key(int body) const; // the body's TShape, nullptr when there is none const TopoDS_Shape* body_shape(int body) const; // nullptr when there is no such body or it has no shape
bool is_current_shape(int body, const TopoDS_Shape& sampled) const; // `sampled` is still the body's shape
void append_ribbons(GLModel::Geometry& g, int body, const std::vector<std::vector<Vec3d>>& polylines, void append_ribbons(GLModel::Geometry& g, int body, const std::vector<std::vector<Vec3d>>& polylines,
const Vec3d& vd, const Vec3d& pull, double hw) const; const Vec3d& vd, const Vec3d& pull, double hw) const;
bool body_pickable(int b) const; // false when the body is explicitly hidden bool body_pickable(int b) const; // false when the body is explicitly hidden
SolidSel m_solid_sel{SolidSel::None}; SolidSel m_solid_sel{SolidSel::None};
int m_sel_body{-1}; // which body the face/edge selection is on int m_sel_body{-1}; // which body the face/edge selection is on
TopoDS_Shape m_sel_shape; // that body's shape when picked: the ids index into it
int m_sel_face{-1}; int m_sel_face{-1};
int m_sel_edge{-1}; int m_sel_edge{-1};
std::vector<Vec3d> m_sel_edge_pts; std::vector<Vec3d> m_sel_edge_pts;
@@ -1274,7 +1307,7 @@ private:
void hit_display_sketch(const DisplaySketch& d, const Vec2d& p, double tol, void hit_display_sketch(const DisplaySketch& d, const Vec2d& p, double tol,
int& edge_feat, int& edge_reg, int& edge_ent, int& edge_feat, int& edge_reg, int& edge_ent,
double& edge_d, int& face_feat, int& face_reg) const; double& edge_d, int& face_feat, int& face_reg) const;
bool m_right_consumed{false}; // last RightDown was a gesture terminator, not a menu std::optional<wxMouseEvent> m_right_press; // right press not yet known to be a click
bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body
void render_solid_highlight(); void render_solid_highlight();
// The above's edge and vertex highlight, from explicit arguments, so the committed selection // The above's edge and vertex highlight, from explicit arguments, so the committed selection
@@ -1282,11 +1315,11 @@ private:
void render_solid_sel(SolidSel kind, const std::vector<Vec3d>& edge_pts, const Vec3d& vertex_pt, void render_solid_sel(SolidSel kind, const std::vector<Vec3d>& edge_pts, const Vec3d& vertex_pt,
const ColorRGBA& rgb); const ColorRGBA& rgb);
// A set of selected faces of one body, with their edges sampled once, keyed by the body's // A set of selected faces of one body, with their edges sampled once, keyed by the body's
// TShape so a recompute that rebuilt the body retires it. // shape so a recompute that rebuilt the body retires it.
struct FaceHighlight { struct FaceHighlight {
int body{-1}; int body{-1};
std::vector<int> faces; // sorted std::vector<int> faces; // sorted
const void* key{nullptr}; TopoDS_Shape shape;
std::vector<std::vector<Vec3d>> edges; // in the body's shape coordinates std::vector<std::vector<Vec3d>> edges; // in the body's shape coordinates
}; };
FaceHighlight make_face_highlight(int body, std::vector<int> faces) const; FaceHighlight make_face_highlight(int body, std::vector<int> faces) const;
@@ -1403,6 +1436,7 @@ private:
int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none
double m_mv_radius{0.0}; // body bounding-sphere radius (mm); 0 = unknown double m_mv_radius{0.0}; // body bounding-sphere radius (mm); 0 = unknown
int m_mv_press_x{0}, m_mv_press_y{0}; int m_mv_press_x{0}, m_mv_press_y{0};
double m_mv_grab_along{0.0}; // arrow grab point's distance from the anchor; NaN = unknown
Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform
void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis
void render_move_gizmo(); void render_move_gizmo();
@@ -1410,7 +1444,6 @@ private:
double move_gizmo_arm(const Camera& cam) const; double move_gizmo_arm(const Camera& cam) const;
bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis); void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const; bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const;
void open_move_editor(int axis); void open_move_editor(int axis);
+80 -25
View File
@@ -1875,7 +1875,7 @@ BoundingBoxf3 GLCanvas3D::volumes_bounding_box(bool current_plate_only) const
bool is_limit = m_canvas_type != ECanvasType::CanvasAssembleView; bool is_limit = m_canvas_type != ECanvasType::CanvasAssembleView;
if (is_limit) { if (is_limit) {
if (current_plate_only) { if (current_plate_only) {
expand_part_plate_list_box = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_bounding_box(); expand_part_plate_list_box = _current_plate_box();
} else { } else {
auto plate_list_box = wxGetApp().plater()->get_partplate_list().get_bounding_box(); auto plate_list_box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
auto horizontal_radius = 0.5 * sqrt(std::pow(plate_list_box.min[0] - plate_list_box.max[0], 2) + std::pow(plate_list_box.min[1] - plate_list_box.max[1], 2)); auto horizontal_radius = 0.5 * sqrt(std::pow(plate_list_box.min[0] - plate_list_box.max[0], 2) + std::pow(plate_list_box.min[1] - plate_list_box.max[1], 2));
@@ -2088,6 +2088,25 @@ float GLCanvas3D::get_collapse_toolbar_height() const
return collapse_side() != CollapseSide::None ? collapse_toolbar().get_height() : 0; return collapse_side() != CollapseSide::None ? collapse_toolbar().get_height() : 0;
} }
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
static constexpr float NAVIGATOR_SIZE = 128.f;
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
float GLCanvas3D::get_canvas_toolbar_right() const
{
// The toolbar scale, which on Windows follows the monitor's DPI where the ImGui style does not.
float sc = get_scale();
#ifdef WIN32
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
#endif // WIN32
const float left = wxGetApp().show_3d_navigator() ? NAVIGATOR_SIZE : CANVAS_TOOLBAR_MARGIN;
return (left + 2.f * CANVAS_TOOLBAR_PADDING + CANVAS_TOOLBAR_BUTTON) * sc;
}
GLToolbar& GLCanvas3D::collapse_toolbar() const GLToolbar& GLCanvas3D::collapse_toolbar() const
{ {
return m_collapse_toolbar != nullptr ? *m_collapse_toolbar : wxGetApp().plater()->get_collapse_toolbar(); return m_collapse_toolbar != nullptr ? *m_collapse_toolbar : wxGetApp().plater()->get_collapse_toolbar();
@@ -2395,7 +2414,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
} }
else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward()) else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward())
show_grid = false; show_grid = false;
if (m_axes_at_bed_center) if (m_design_canvas)
// Design tab: the plate grid is generated from the plate's front-left corner, so it // Design tab: the plate grid is generated from the plate's front-left corner, so it
// floats mid-cell under the modeling-origin triad. Suppress it here; a CAD grid centred // floats mid-cell under the modeling-origin triad. Suppress it here; a CAD grid centred
// on the origin is rendered in its place (see _render_cad_grid). // on the origin is rendered in its place (see _render_cad_grid).
@@ -2411,7 +2430,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
m_frame_profiler.mark("bed"); m_frame_profiler.mark("bed");
if (show_bed) //BBS: add outline logic if (show_bed) //BBS: add outline logic
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid); _render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
if (m_axes_at_bed_center && show_bed) if (m_design_canvas && show_bed)
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid. // Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
_render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix()); _render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix());
m_frame_profiler.mark("plates"); m_frame_profiler.mark("plates");
@@ -4311,7 +4330,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
} }
if (evt.LeftDown() && m_canvas != nullptr) if (evt.LeftDown() && m_canvas != nullptr)
m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas
if (m_design_sketch_tool->on_mouse(evt, *this)) { // A Touchpad-style orbit or pan (a plain move with Alt or Shift held) is the camera's,
// whatever the tool would make of the move. Only a drag consults the button mappings.
const bool camera_move = evt.Moving() && (is_camera_rotate(evt, {}) || is_camera_pan(evt, {}));
if (!camera_move && m_design_sketch_tool->on_mouse(evt, *this)) {
m_dirty = true; m_dirty = true;
render(); // force an immediate redraw so the sketch overlay updates live render(); // force an immediate redraw so the sketch overlay updates live
return; return;
@@ -6355,7 +6377,7 @@ void GLCanvas3D::_render_3d_navigator()
} }
} }
const float size = 128 * sc; const float size = NAVIGATOR_SIZE * sc;
m_canvas_toolbar_pos[0] = size; m_canvas_toolbar_pos[0] = size;
const auto result = ImGuizmo::ViewManipulate(cameraView, cameraProjection, ImGuizmo::OPERATION::ROTATE, ImGuizmo::MODE::WORLD, nullptr, const auto result = ImGuizmo::ViewManipulate(cameraView, cameraProjection, ImGuizmo::OPERATION::ROTATE, ImGuizmo::MODE::WORLD, nullptr,
camDistance, ImVec2(viewManipulateLeft, viewManipulateTop - size), ImVec2(size, size), camDistance, ImVec2(viewManipulateLeft, viewManipulateTop - size), ImVec2(size, size),
@@ -8251,7 +8273,7 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
*/ */
//bool show_texture = true; //bool show_texture = true;
//BBS set axes mode //BBS set axes mode
if (m_axes_at_bed_center) { if (m_design_canvas) {
// Design tab: triad at the bed centre = modeling origin (set every frame because // Design tab: triad at the bed centre = modeling origin (set every frame because
// set_shape/set_axes_mode otherwise reset it to the bed corner). // set_shape/set_axes_mode otherwise reset it to the bed corner).
const Vec2d bc = m_bed.build_volume().bed_center(); const Vec2d bc = m_bed.build_volume().bed_center();
@@ -8264,7 +8286,30 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid) void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
{ {
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled); PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
// Design tab: its bed stays at the printer bed's home whichever plate is current, so the
// current plate's exclude areas are moved from that plate onto it.
PartPlate* curr_plate = plate_list.get_curr_plate();
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
}
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
{
// Design tab: its own bed stands in for the current plate (see _render_platelist).
const BuildVolume& build_volume = m_bed.build_volume();
if (m_design_canvas && build_volume.valid()) {
// Flat at z = 0 like the plate's own box. Merged, as PartPlate builds it: the min/max
// constructor leaves a flat box undefined.
const BoundingBoxf bb = build_volume.bounding_volume2d();
BoundingBoxf3 box;
box.merge(Vec3d(bb.min.x(), bb.min.y(), 0.));
box.merge(Vec3d(bb.max.x(), bb.max.y(), 0.));
return box;
}
PartPlate* curr_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
return curr_plate != nullptr ? curr_plate->get_bounding_box() : BoundingBoxf3();
} }
// Design tab: CAD grid on the bed plane, drawn in place of the plate's corner-origin grid. // Design tab: CAD grid on the bed plane, drawn in place of the plate's corner-origin grid.
@@ -10038,10 +10083,10 @@ void GLCanvas3D::_render_canvas_toolbar()
sc *= (float) dpi / (float) DPI_DEFAULT; sc *= (float) dpi / (float) DPI_DEFAULT;
#endif // WIN32 #endif // WIN32
ImVec2 btn_size = ImVec2(36.f, 36.f) * sc; ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (10.f * sc), 10.f * sc); ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
ImVec2 spacing = ImVec2(6.f, 6.f) * sc; ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
ImVec2 padding = ImVec2(2.f, 2.f) * sc; ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
Vec2i32 pos = { Vec2i32 pos = {
m_canvas_toolbar_pos[0] + margin.x, m_canvas_toolbar_pos[0] + margin.x,
get_canvas_size().get_height() - margin.y get_canvas_size().get_height() - margin.y
@@ -10099,14 +10144,25 @@ void GLCanvas3D::_render_canvas_toolbar()
ImTextureID z_hover_id = m_gizmos.get_icon_texture_id(m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_HOVER); ImTextureID z_hover_id = m_gizmos.get_icon_texture_id(m_is_dark ? GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_DARK_HOVER : GLGizmosManager::MENU_ICON_NAME::IC_CANVAS_ZOOM_HOVER);
if (ImGui::ImageButton3(z_normal_id, z_hover_id, btn_size)) { if (ImGui::ImageButton3(z_normal_id, z_hover_id, btn_size)) {
select_view("plate"); #ifdef SLIC3R_CAD
if (m_selection.is_empty()) { // The Design tab selects and sketches outside the canvas's selection and volumes, so
if (m_canvas_type == ECanvasType::CanvasAssembleView) // it names what to frame. Framed along the current view, which is often square to a
zoom_to_volumes(); // sketch plane; an empty tab falls through to the bed.
else const BoundingBoxf3 design_box = m_design_sketch_tool != nullptr ? m_design_sketch_tool->fit_box(m_volumes) : BoundingBoxf3();
zoom_to_bed(); if (design_box.defined)
} else { _zoom_to_box(design_box);
zoom_to_selection(); else
#endif
{
select_view("plate");
if (m_selection.is_empty()) {
if (m_canvas_type == ECanvasType::CanvasAssembleView)
zoom_to_volumes();
else
zoom_to_bed();
} else {
zoom_to_selection();
}
} }
} else if (ImGui::IsItemHovered()) { } else if (ImGui::IsItemHovered()) {
auto tooltip_str_wx = _L("Fit camera to scene or selected object."); auto tooltip_str_wx = _L("Fit camera to scene or selected object.");
@@ -11058,10 +11114,9 @@ std::optional<Vec3d> GLCanvas3D::get_camera_orbit_target(ECameraNavigationType n
{ {
// Orca: Centralize the pre-existing pivot rules so orbiting and pan fallback cannot // Orca: Centralize the pre-existing pivot rules so orbiting and pan fallback cannot
// choose different reference depths for the same canvas and active tool. // choose different reference depths for the same canvas and active tool.
PartPlate* current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); const BoundingBoxf3 plate_box = _current_plate_box();
if (navigation_type == ECameraNavigationType::Gesture) if (navigation_type == ECameraNavigationType::Gesture)
return current_plate == nullptr ? std::nullopt : return plate_box.defined ? std::make_optional(plate_box.center()) : std::nullopt;
std::make_optional(current_plate->get_bounding_box().center());
const GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type(); const GLGizmosManager::EType gizmo_type = m_gizmos.get_current_type();
const bool use_scene_target = m_canvas_type == ECanvasType::CanvasAssembleView || const bool use_scene_target = m_canvas_type == ECanvasType::CanvasAssembleView ||
@@ -11081,15 +11136,15 @@ std::optional<Vec3d> GLCanvas3D::get_camera_orbit_target(ECameraNavigationType n
Vec3d target = Vec3d::Zero(); Vec3d target = Vec3d::Zero();
if (m_canvas_type == ECanvasType::CanvasPreview) { if (m_canvas_type == ECanvasType::CanvasPreview) {
if (current_plate != nullptr) if (plate_box.defined)
target = current_plate->get_bounding_box().center(); target = plate_box.center();
} else if (!m_selection.is_empty()) { } else if (!m_selection.is_empty()) {
target = m_selection.get_bounding_box().center(); target = m_selection.get_bounding_box().center();
} else { } else {
// Orca: Match regular mouse orbit: objects on the active plate, then the plate itself. // Orca: Match regular mouse orbit: objects on the active plate, then the plate itself.
BoundingBoxf3 bbox = volumes_bounding_box(true); BoundingBoxf3 bbox = volumes_bounding_box(true);
if (!bbox.defined && current_plate != nullptr) if (!bbox.defined)
bbox = current_plate->get_bounding_box(); bbox = plate_box;
if (bbox.defined) if (bbox.defined)
target = bbox.center(); target = bbox.center();
} }
+13 -6
View File
@@ -604,9 +604,11 @@ private:
GLToolbar* m_collapse_toolbar{nullptr}; GLToolbar* m_collapse_toolbar{nullptr};
std::function<CollapseSide()> m_collapse_side; std::function<CollapseSide()> m_collapse_side;
bool m_plate_chrome_enabled{true}; bool m_plate_chrome_enabled{true};
// Design tab: render the world-axis triad at the bed centre (= modeling origin) instead of // This canvas is the Design tab's. Its bed stays at the printer bed's home whichever plate is
// the bed corner. Default false preserves the main editor's corner triad. // current, with the world-axis triad and a CAD grid at the bed centre (= modeling origin) in
bool m_axes_at_bed_center{false}; // place of the corner triad and the plate grid, and the plate data it reads (exclude areas,
// the current plate's box) moved onto that bed. Default false leaves the editor tabs untouched.
bool m_design_canvas{false};
// Design tab: draw the printer bed and its plate grid at all. Default true, so the // Design tab: draw the printer bed and its plate grid at all. Default true, so the
// main editor is untouched; the Design tab lets the user hide it to model without a bed. // main editor is untouched; the Design tab lets the user hide it to model without a bed.
bool m_show_bed{true}; bool m_show_bed{true};
@@ -1025,7 +1027,7 @@ public:
// initialized, which loads the toolbar's background. // initialized, which loads the toolbar's background.
void set_collapse_toolbar(GLToolbar* toolbar, std::function<CollapseSide()> side); void set_collapse_toolbar(GLToolbar* toolbar, std::function<CollapseSide()> side);
void enable_plate_chrome(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_canvas(bool b) { m_design_canvas = b; }
void set_show_bed(bool b) { m_show_bed = b; } void set_show_bed(bool b) { m_show_bed = b; }
bool get_show_bed() const { return m_show_bed; } bool get_show_bed() const { return m_show_bed; }
void enable_sinking_contours(bool enable) { m_sinking_contours_enabled = enable; } void enable_sinking_contours(bool enable) { m_sinking_contours_enabled = enable; }
@@ -1059,6 +1061,9 @@ public:
bool is_collapse_toolbar_on_left() const; bool is_collapse_toolbar_on_left() const;
float get_collapse_toolbar_width() const; float get_collapse_toolbar_width() const;
float get_collapse_toolbar_height() const; float get_collapse_toolbar_height() const;
// Right edge, in canvas pixels, of the bottom-left corner the 3D navigator and the round
// canvas buttons own. An overlay along the bottom edge starts past it.
float get_canvas_toolbar_right() const;
void update_volumes_colors_by_extruder(); void update_volumes_colors_by_extruder();
@@ -1441,9 +1446,11 @@ private:
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix); void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic //BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true); void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
BoundingBoxf3 _current_plate_box() const;
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's // Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
// corner-origin grid when the axes sit at the bed centre (modeling origin). Rebuilds its // corner-origin grid, centred on the modeling origin. Rebuilds its GLModels lazily, only
// GLModels lazily, only when the bed shape changed. // when the bed shape changed.
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix); void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add outline drawing logic //BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true); void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
+4
View File
@@ -39,6 +39,10 @@ add_executable(${_TEST_NAME}_tests
../fff_print/test_helpers.cpp ../fff_print/test_helpers.cpp
) )
if (SLIC3R_CAD)
target_sources(${_TEST_NAME}_tests PRIVATE test_design_sketch_tool.cpp)
endif ()
if (MSVC) if (MSVC)
target_link_libraries(${_TEST_NAME}_tests Setupapi.lib) target_link_libraries(${_TEST_NAME}_tests Setupapi.lib)
endif () endif ()
@@ -0,0 +1,346 @@
// Orca: This suite links libslic3r_gui; the Design tab's sketch tool needs no wx application or GL
// context until it renders.
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
// Match the GUI precompiled header: wx/msw/wrapcctl.h needs HDITEM from CommCtrl.h.
#include <CommCtrl.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <cmath>
#include <initializer_list>
#include <iterator>
#include <limits>
#include <optional>
#include <random>
#include <vector>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/Line.hpp"
#include "libslic3r/Point.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
using namespace Slic3r;
using namespace Slic3r::GUI;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
namespace {
SketchEntity circle(const Vec2d& c, double r)
{
SketchEntity e;
e.type = SketchEntity::Type::Circle;
e.center = e.p0 = c;
e.radius = r;
return e;
}
// Two committed sketches on XY: feature 1 a 10 mm circle at (50, 20), feature 2 a 5 mm one at (-60, 0).
void show_two_sketches(DesignSketchTool& tool)
{
tool.set_display_sketches({ { { circle({ 50., 20. }, 10.) }, SketchPlane::XY(), 1 },
{ { circle({ -60., 0. }, 5.) }, SketchPlane::XY(), 2 } });
}
// The Design tab's base planes, all through one origin, as DesignPanel shows them.
std::vector<SketchPlane> base_planes() { return { SketchPlane::XY(), SketchPlane::XZ(), SketchPlane::YZ() }; }
struct View
{
Vec3d eye;
Vec3d forward;
bool perspective;
};
// Eyes in four octants, off every plane, plus two orthographic directions.
const View kViews[] = {
{ Vec3d(300., -400., 250.), Vec3d(-300., 400., -250.).normalized(), true },
{ Vec3d(-350., -200., 300.), Vec3d(350., 200., -300.).normalized(), true },
{ Vec3d(250., 300., -200.), Vec3d(-250., -300., 200.).normalized(), true },
{ Vec3d(-300., 350., -250.), Vec3d(300., -350., 250.).normalized(), true },
{ Vec3d::Zero(), Vec3d(-0.5, 0.7, -0.5).normalized(), false },
{ Vec3d::Zero(), Vec3d(0.3, 0.4, 0.85).normalized(), false },
};
double area(const PlanePiece& piece, const SketchPlane& plane)
{
Vec3d sum = Vec3d::Zero();
for (size_t i = 0; i < piece.corners.size(); ++i)
sum += piece.corners[i].cross(piece.corners[(i + 1) % piece.corners.size()]);
return 0.5 * std::abs(sum.dot(plane.x_axis.cross(plane.y_axis)));
}
// How far along the ray (from, unit dir) it crosses `piece`, or nothing if it misses.
std::optional<double> hit_distance(const PlanePiece& piece, const SketchPlane& plane, const Vec3d& from, const Vec3d& dir)
{
const Vec3d n = plane.x_axis.cross(plane.y_axis);
const double dn = n.dot(dir);
if (std::abs(dn) < 1e-9)
return std::nullopt;
const double t = n.dot(piece.corners.front() - from) / dn;
if (t <= 0.)
return std::nullopt;
const Vec3d x = from + dir * t;
double lo = 0., hi = 0.;
for (size_t i = 0; i < piece.corners.size(); ++i) {
const Vec3d& a = piece.corners[i];
const Vec3d& b = piece.corners[(i + 1) % piece.corners.size()];
const double s = (b - a).cross(x - a).dot(n);
lo = std::min(lo, s);
hi = std::max(hi, s);
}
if (lo < 0. && hi > 0.)
return std::nullopt; // outside one of the edges
return t;
}
// Whether x lies on one of the segments the pieces are outlined with.
bool outlined(const std::vector<PlanePiece>& pieces, const Vec3d& x)
{
for (const PlanePiece& piece : pieces)
for (const auto& [a, b] : piece.lines) {
const Vec3d ab = b - a;
const double t = std::clamp((x - a).dot(ab) / ab.squaredNorm(), 0., 1.);
if ((a + ab * t - x).norm() < 1e-6)
return true;
}
return false;
}
std::vector<PlanePiece> pieces_of(const std::vector<SketchPlane>& planes)
{
const View& view = kViews[0];
return planes_back_to_front(planes, 75., view.eye, view.forward, view.perspective);
}
struct SightLines
{
int overlapping = 0; // sight lines through two or more pieces, where draw order matters
int out_of_order = 0; // ...of which meet a nearer piece before a farther one
};
// Translucent pieces blend correctly only if, along every line of sight, each piece is drawn after
// every piece behind it.
SightLines sight_lines(const std::vector<SketchPlane>& planes, double half, const View& view)
{
const std::vector<PlanePiece> pieces = planes_back_to_front(planes, half, view.eye, view.forward, view.perspective);
std::mt19937 rng(7);
std::uniform_real_distribution<double> coord(-0.95 * half, 0.95 * half);
SightLines seen;
for (int r = 0; r < 500; ++r) {
const Vec3d target(coord(rng), coord(rng), coord(rng));
const Vec3d from = view.perspective ? view.eye : Vec3d(target - view.forward * (10. * half));
const Vec3d dir = (target - from).normalized();
double last = std::numeric_limits<double>::max();
int hits = 0;
bool ok = true;
for (const PlanePiece& piece : pieces)
if (const std::optional<double> t = hit_distance(piece, planes[piece.plane], from, dir)) {
ok = ok && *t <= last + 1e-6 * half;
last = *t;
++hits;
}
if (hits >= 2) {
++seen.overlapping;
seen.out_of_order += ok ? 0 : 1;
}
}
return seen;
}
} // namespace
TEST_CASE("Fit frames the picked sketch region, not the other sketches", "[DesignSketchTool]")
{
DesignSketchTool tool;
GLVolumeCollection no_bodies;
show_two_sketches(tool);
tool.set_display_pick(1, 0);
const BoundingBoxf3 box = tool.fit_box(no_bodies);
REQUIRE(box.defined);
CHECK_THAT(box.min.x(), WithinAbs(40., 0.5));
CHECK_THAT(box.max.x(), WithinAbs(60., 0.5));
CHECK_THAT(box.min.y(), WithinAbs(10., 0.5));
CHECK_THAT(box.max.y(), WithinAbs(30., 0.5));
}
TEST_CASE("Fit frames every sketch when nothing is picked", "[DesignSketchTool]")
{
DesignSketchTool tool;
GLVolumeCollection no_bodies;
show_two_sketches(tool);
const BoundingBoxf3 box = tool.fit_box(no_bodies);
REQUIRE(box.defined);
CHECK_THAT(box.min.x(), WithinAbs(-65., 0.5));
CHECK_THAT(box.max.x(), WithinAbs(60., 0.5));
}
TEST_CASE("Fit frames the sketch being drawn along with the committed ones", "[DesignSketchTool]")
{
DesignSketchTool tool;
GLVolumeCollection no_bodies;
show_two_sketches(tool);
// A 100 mm line up the XZ plane, whose y axis is world Z.
SketchEntity line;
line.p0 = { 0., 0. };
line.p1 = { 0., 100. };
tool.begin_edit({ line }, {}, SketchPlane::XZ());
const BoundingBoxf3 box = tool.fit_box(no_bodies);
REQUIRE(box.defined);
CHECK_THAT(box.max.z(), WithinAbs(100., 1e-6));
CHECK_THAT(box.min.x(), WithinAbs(-65., 0.5));
}
TEST_CASE("Fit gives a flat sketch depth, so it can be framed edge-on", "[DesignSketchTool]")
{
DesignSketchTool tool;
GLVolumeCollection no_bodies;
show_two_sketches(tool);
tool.set_display_pick(2, 0);
const BoundingBoxf3 box = tool.fit_box(no_bodies);
REQUIRE(box.defined);
CHECK(box.size().z() > 0.);
CHECK_THAT(box.center().z(), WithinAbs(0., 1e-9));
}
TEST_CASE("Fit frames nothing when the Design tab shows nothing", "[DesignSketchTool]")
{
DesignSketchTool tool;
GLVolumeCollection no_bodies;
CHECK_FALSE(tool.fit_box(no_bodies).defined);
}
TEST_CASE("Crossing base planes are drawn back to front from any viewpoint", "[DesignSketchTool]")
{
const View& view = kViews[GENERATE(range(0, int(std::size(kViews))))];
const SightLines seen = sight_lines(base_planes(), 75., view);
CHECK(seen.overlapping >= 200); // of the 500: most lines of sight into the planes cross two
CHECK(seen.out_of_order == 0);
}
TEST_CASE("Datum planes are drawn back to front among the base planes", "[DesignSketchTool]")
{
// A datum parallel to XY 30 mm up, and one tilted 30 degrees about X through (0, 0, 10).
std::vector<SketchPlane> planes = base_planes();
SketchPlane raised = SketchPlane::XY();
raised.origin = Vec3d(0., 0., 30.);
SketchPlane tilted;
tilted.origin = Vec3d(0., 0., 10.);
tilted.y_axis = Vec3d(0., std::cos(M_PI / 6.), std::sin(M_PI / 6.));
tilted.normal = tilted.x_axis.cross(tilted.y_axis);
planes.push_back(raised);
planes.push_back(tilted);
const View& view = kViews[GENERATE(range(0, int(std::size(kViews))))];
const SightLines seen = sight_lines(planes, 75., view);
CHECK(seen.overlapping >= 200);
CHECK(seen.out_of_order == 0);
}
TEST_CASE("Base planes are outlined along every line where they cross", "[DesignSketchTool]")
{
// XY, XZ and YZ cross along the three axes, all through the middle of each 75 mm half-square.
const std::vector<PlanePiece> pieces = pieces_of(base_planes());
int missed = 0;
for (double t = -70.; t <= 70.; t += 10.)
for (const Vec3d& axis : { Vec3d(1., 0., 0.), Vec3d(0., 1., 0.), Vec3d(0., 0., 1.) })
missed += outlined(pieces, axis * t) ? 0 : 1;
CHECK(missed == 0);
}
TEST_CASE("A datum clear of the base planes gets no lines across it", "[DesignSketchTool]")
{
// Parallel to YZ at x = 100, past the 75 mm half-squares of XY and XZ: the infinite XY and XZ
// planes still cut it, along z = 0 and y = 0, but the squares never meet.
std::vector<SketchPlane> planes = base_planes();
SketchPlane beyond = SketchPlane::YZ();
beyond.origin = Vec3d(100., 0., 0.);
planes.push_back(beyond);
const std::vector<PlanePiece> pieces = pieces_of(planes);
CHECK_FALSE(outlined(pieces, Vec3d(100., 30., 0.)));
CHECK_FALSE(outlined(pieces, Vec3d(100., 0., 30.)));
}
TEST_CASE("A line where two squares cross stops where either square ends", "[DesignSketchTool]")
{
// Parallel to XY 30 mm up and moved 60 mm along X, so it spans x = -15..135. It meets XZ along
// y = 0, z = 30, but XZ's square only reaches x = 75.
std::vector<SketchPlane> planes = base_planes();
SketchPlane raised = SketchPlane::XY();
raised.origin = Vec3d(60., 0., 30.);
planes.push_back(raised);
const std::vector<PlanePiece> pieces = pieces_of(planes);
CHECK(outlined(pieces, Vec3d(0., 0., 30.)));
CHECK(outlined(pieces, Vec3d(70., 0., 30.)));
CHECK_FALSE(outlined(pieces, Vec3d(100., 0., 30.)));
}
TEST_CASE("Cutting the base planes along each other keeps every plane whole", "[DesignSketchTool]")
{
const std::vector<SketchPlane> planes = base_planes();
const double half = 75.;
const View& view = kViews[0];
std::vector<double> covered(planes.size(), 0.);
for (const PlanePiece& piece : planes_back_to_front(planes, half, view.eye, view.forward, view.perspective))
covered[piece.plane] += area(piece, planes[piece.plane]);
for (double a : covered)
CHECK_THAT(a, WithinRel(4. * half * half, 1e-9));
}
TEST_CASE("A move arrow drag moves the body by the cursor's travel, wherever the arrow is grabbed", "[DesignSketchTool]")
{
DesignSketchTool tool;
Transform3d moved = Transform3d::Identity();
tool.on_body_move_changed = [&moved](int, const Transform3d& xform) { moved = xform; };
tool.set_move_gizmo(0, Vec3d::Zero(), Transform3d::Identity(), 10.);
// Looking straight down onto the X arrow, the cursor over x = `x` on it.
const auto ray_at = [](double x) { return Linef3(Vec3d(x, 0., 100.), Vec3d(x, 0., 0.)); };
// Grabbed 12 mm out from the body centre: a 1 mm move moves the body 1 mm, not 13.
tool.grab_move_arrow(0, ray_at(12.));
tool.drag_move_arrow(ray_at(13.));
CHECK_THAT(moved.translation().x(), WithinAbs(1., 1e-9));
tool.drag_move_arrow(ray_at(17.));
CHECK_THAT(moved.translation().x(), WithinAbs(5., 1e-9));
// The next drag carries on from where the body was left: grabbed 3 mm past it, moved 2 mm.
tool.grab_move_arrow(0, ray_at(8.));
tool.drag_move_arrow(ray_at(10.));
CHECK_THAT(moved.translation().x(), WithinAbs(7., 1e-9));
}
TEST_CASE("A move arrow pressed while looking down its axis does not jump on the first move", "[DesignSketchTool]")
{
DesignSketchTool tool;
Transform3d moved = Transform3d::Identity();
tool.on_body_move_changed = [&moved](int, const Transform3d& xform) { moved = xform; };
tool.set_move_gizmo(0, Vec3d::Zero(), Transform3d::Identity(), 10.);
const auto ray_at = [](double x) { return Linef3(Vec3d(x, 0., 100.), Vec3d(x, 0., 0.)); };
// The press projects nowhere on the X axis, so the first move only finds where it was grabbed.
tool.grab_move_arrow(0, Linef3(Vec3d(100., 0., 0.), Vec3d::Zero()));
tool.drag_move_arrow(ray_at(13.));
CHECK_THAT(moved.translation().x(), WithinAbs(0., 1e-9));
tool.drag_move_arrow(ray_at(15.));
CHECK_THAT(moved.translation().x(), WithinAbs(2., 1e-9));
}