diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 7049f7b08d..d5f0399d6e 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -3741,6 +3741,77 @@ std::vector> DesignSketchTool::selected_faces() const 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& 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> 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()); + } + if (m_solid_sel == SolidSel::Edge) { + for (const Vec3d& p : m_sel_edge_pts) + box.merge(p); + for (const std::vector& 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, std::vector faces) const { diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index cfcf2e8441..91adb207a0 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -2,6 +2,7 @@ #define slic3r_DesignSketchTool_hpp_ #include "libslic3r/Point.hpp" +#include "libslic3r/BoundingBox.hpp" #include "libslic3r/CAD/SketchEngine.hpp" #include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking #include @@ -24,6 +25,7 @@ class wxPoint; namespace Slic3r { class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer +class GLVolumeCollection; namespace GUI { @@ -226,6 +228,11 @@ public: void set_highlight_faces(const std::vector>& faces); // Every face drawn as selected, sorted: the committed pick's and the still-valid ones above. std::vector> 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 // (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 269770fd61..63fb7230dc 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -10144,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); if (ImGui::ImageButton3(z_normal_id, z_hover_id, btn_size)) { - 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(); +#ifdef SLIC3R_CAD + // The Design tab selects and sketches outside the canvas's selection and volumes, so + // it names what to frame. Framed along the current view, which is often square to a + // sketch plane; an empty tab falls through to the bed. + const BoundingBoxf3 design_box = m_design_sketch_tool != nullptr ? m_design_sketch_tool->fit_box(m_volumes) : BoundingBoxf3(); + if (design_box.defined) + _zoom_to_box(design_box); + 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()) { auto tooltip_str_wx = _L("Fit camera to scene or selected object."); diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 566835f495..bd9ffafdd5 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -39,6 +39,10 @@ add_executable(${_TEST_NAME}_tests ../fff_print/test_helpers.cpp ) +if (SLIC3R_CAD) + target_sources(${_TEST_NAME}_tests PRIVATE test_design_sketch_tool.cpp) +endif () + if (MSVC) target_link_libraries(${_TEST_NAME}_tests Setupapi.lib) endif () diff --git a/tests/slic3rutils/test_design_sketch_tool.cpp b/tests/slic3rutils/test_design_sketch_tool.cpp new file mode 100644 index 0000000000..7b99cde012 --- /dev/null +++ b/tests/slic3rutils/test_design_sketch_tool.cpp @@ -0,0 +1,110 @@ +// 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 + // Match the GUI precompiled header: wx/msw/wrapcctl.h needs HDITEM from CommCtrl.h. + #include +#endif + +#include +#include + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/CAD/SketchEngine.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; + +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 } }); +} + +} // 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); +}