mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 07:11:10 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c2e26fa3d | ||
|
|
e328b60992 | ||
|
|
afa9252b59 | ||
|
|
56ebde968a | ||
|
|
840d44b611 | ||
|
|
34696f5651 | ||
|
|
c300fa1e8d | ||
|
|
cd30b196b2 | ||
|
|
b67269ccc4 |
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user