From 250fed88086bc3846a59fa6d337dc22f33fa9bda Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 6 Oct 2026 16:30:29 +0800 Subject: [PATCH] Show the Design tab's reference planes as separate labelled squares around the origin The XY, XZ and YZ planes no longer cross through the bed. Each is a small square in its axis colour, set off from the axes into the corner that faces the default front view, with its name written in the plane. Dash-dot axes run between the squares and replace the bed's axis triad while they show. Hovering a plane greys it and selecting one makes it solid, and picking a solid face now clears a previously picked plane. --- src/slic3r/GUI/CAD/DesignCanvas.cpp | 5 + src/slic3r/GUI/CAD/DesignCanvas.hpp | 1 + src/slic3r/GUI/CAD/DesignPanel.cpp | 18 +- src/slic3r/GUI/CAD/DesignSketchTool.cpp | 459 +++++++++++------- src/slic3r/GUI/CAD/DesignSketchTool.hpp | 43 +- src/slic3r/GUI/GLCanvas3D.cpp | 5 +- tests/slic3rutils/test_design_sketch_tool.cpp | 174 ++++--- 7 files changed, 463 insertions(+), 242 deletions(-) diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index dac8060619..7819cc248a 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -1037,6 +1037,11 @@ void DesignCanvas::set_on_datum_base_picked(std::function cb) m_sketch_tool.on_datum_base_picked = std::move(cb); } +void DesignCanvas::set_selected_base(std::function cb) +{ + m_sketch_tool.selected_base = std::move(cb); +} + void DesignCanvas::set_on_sketch_exit(std::function cb) { m_sketch_tool.on_exit = std::move(cb); diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index f5abb14a95..08ae1ea627 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -238,6 +238,7 @@ public: std::vector labels = {}); // clickable labelled reference planes void clear_base_pick(); void set_on_datum_base_picked(std::function cb); + void set_selected_base(std::function cb); // the reference plane drawn selected, or -1 void set_on_sketch_exit(std::function cb); // Esc -> exit the tool void set_on_sketch_exit_refused(std::function cb); // Esc declined: sketch has work void set_on_sketch_notice(std::function cb); // tool refusals/side effects diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index c93ee937d5..04862917d0 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -3717,8 +3717,13 @@ DesignPanel::DesignPanel(wxWindow* parent) m_pick_face_body = (level >= 1) ? body : -1; m_pick_face = (level >= 1) ? face : -1; // Last pick wins: a leftover loop pick would block Extrude's face push/pull branch, so - // Extrude would extrude a sketch instead of push/pulling the clicked face. - if (level >= 1) m_viewport->clear_loop_pick(); + // Extrude would extrude a sketch instead of push/pulling the clicked face. And a reference + // plane picked before stops being the one chosen, as a plane pick drops a face: otherwise it + // would come back as the sketch plane, and be drawn selected, once this pick is let go. + if (level >= 1) { + m_viewport->clear_loop_pick(); + m_plane_picked = false; + } // Say what got picked. Without this the ONLY feedback is the viewport highlight, so a // pick that registers but draws faintly is indistinguishable from one that never // happened — which is precisely how this failure was reported and why it resisted @@ -4005,6 +4010,15 @@ DesignPanel::DesignPanel(wxWindow* parent) m_status->Refresh(); } }); + // The reference plane drawn selected: the Plane card's base, or else the plane a sketch would go + // on — chosen, and not overridden by a picked face (sketch_plane_target's test, without its + // per-face OCCT lookup, as this is asked every frame). + m_viewport->set_selected_base([this] { + if (m_active == Tool::Plane) + return m_plane_base != nullptr && m_pl_faceA < 0 ? m_plane_base->GetSelection() : -1; + const bool face = (m_sel_solid_face >= 0 && m_sel_solid_body >= 0) || (m_pick_face >= 0 && m_pick_face_body >= 0); + return m_plane_picked && !face ? m_ref_plane : -1; + }); // Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a // display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched, diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index ba6230c2ad..baab2e7e9b 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -30,7 +30,7 @@ #include #include "libslic3r/TriangleMesh.hpp" -#include +#include #include "libslic3r/CAD/SketchEngine.hpp" #include "libslic3r/Point.hpp" #include @@ -5385,8 +5385,12 @@ void DesignSketchTool::drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& e if (on_rib_thickness_changed) on_rib_thickness_changed(m_rb_thickness); } -// ---- Reference/base planes (Onshape-style default planes) ----------------------------- +// ---- Reference/base planes ------------------------------------------------------------- namespace { +// `text` in the stroke font, `height` tall and centred on the origin; `size` is its extent. With the +// font, further down. +std::vector> text_strokes(const std::string& text, double height, Vec2d& size); + // 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 @@ -5449,33 +5453,21 @@ void paint_back_to_front(std::vector&& in, size_t k, const std::vect 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)); } +// A base square's name: its cap height, and where the text is centred in the square's frame — inset +// from the frame's (+x, +y) corner, which lies by an axis toward its far end or at the outer corner, +// away from the other two names. +double label_height(double half) { return 0.30 * half; } +Vec2d label_centre(double half, const Vec2d& size) { return Vec2d::Constant(0.88 * half) - 0.5 * size; } -double plane_distance(const SketchPlane& p, const Vec3d& x) { return p.x_axis.cross(p.y_axis).normalized().dot(x - p.origin); } +// A datum's name is not in the stroke font: it is an ImGui chip anchored at this point of its square. +Vec2d datum_label_anchor(double half) { return Vec2d(-0.8 * half, 0.84 * half); } -// 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; -} +bool is_base_plane(int base) { return base >= 0 && base < 3; } + +// The octant the base squares sit in. Each name is written facing x_axis x y_axis, which is +Z for +// XY, -Y for XZ and +X for YZ: all three point into it, so a camera there looks into the corner the +// squares form and reads every name, and from the front XY lies below the X axis rather than behind XZ. +Vec3d reference_octant() { return Vec3d(1., -1., 1.); } } // namespace std::vector planes_back_to_front(const std::vector& planes, double half, @@ -5487,41 +5479,52 @@ std::vector planes_back_to_front(const std::vector& pla 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 out; - paint_back_to_front(std::move(squares), 0, planes, eps, eye, forward, perspective, out); + paint_back_to_front(std::move(squares), 0, planes, 1e-6 * std::max(half, 1.), eye, forward, perspective, out); + return 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); - } +SketchPlane reference_square(const SketchPlane& plane, int base, double half) +{ + SketchPlane square = plane; + if (is_base_plane(base)) { + const Vec3d o = reference_octant(); + square.origin += (reference_square_gap(half) + half) * (plane.x_axis.dot(o) * plane.x_axis + plane.y_axis.dot(o) * plane.y_axis); + } + return square; +} + +std::array reference_label_box(const SketchPlane& square, double half, const std::string& text) +{ + const double th = label_height(half); + Vec2d size; + text_strokes(text, th, size); + const Vec2d c = label_centre(half, size); + const Vec2d r = 0.5 * size + Vec2d::Constant(0.15 * th); // a margin round the strokes, to aim at + return { square.to_world(c + Vec2d(-r.x(), -r.y())), square.to_world(c + Vec2d(r.x(), -r.y())), + square.to_world(c + Vec2d(r.x(), r.y())), square.to_world(c + Vec2d(-r.x(), r.y())) }; +} + +int pick_reference_square(const std::vector& squares, double half, const Vec3d& from, const Vec3d& dir) +{ + int best = -1; + double best_t = std::numeric_limits::max(); + for (int i = 0; i < int(squares.size()); ++i) { + const SketchPlane& s = squares[i]; + const Vec3d n = s.x_axis.cross(s.y_axis); + const double dn = n.dot(dir); + if (std::abs(dn) < 1e-12) + continue; // the ray runs along the square + const double t = n.dot(s.origin - from) / dn; + if (t <= 0. || t >= best_t) + continue; // behind the eye, or no nearer than one already hit + const Vec3d d = from + dir * t - s.origin; + if (std::abs(d.dot(s.x_axis)) <= half && std::abs(d.dot(s.y_axis)) <= half) { + best = i; + best_t = t; } } - return out; + return best; } void DesignSketchTool::set_base_pick(std::vector planes, std::vector bases, @@ -5543,140 +5546,237 @@ void DesignSketchTool::clear_base_pick() m_dbp_hover = -1; } -// Reference planes sit INSIDE the bed. They used to be 0.6 * the bed's larger side, i.e. a square -// 1.2x the plate, and three of them are drawn with depth testing off — so they painted over the -// plate grid from edge to edge and the bed simply was not readable any more. "The planes hide the -// bed", reported exactly that way. Small enough to leave the grid legible around them is also the -// Onshape look this was reaching for: a modest square at the origin, not a tablecloth. +// Sized from the bed, so the squares keep their size against the plate on screen: each is 0.16 of +// the bed's larger side, about the share of the view Fusion's origin planes take at the default zoom. +// Larger squares were reported as hiding the bed. double DesignSketchTool::dbp_half_extent() const { - double half = 75.0; + double half = 20.0; if (auto* pl = wxGetApp().plater()) { const BoundingBoxf bb = pl->build_volume().bounding_volume2d(); const double w = bb.max.x() - bb.min.x(), d = bb.max.y() - bb.min.y(); - if (w > 1.0 && d > 1.0) half = 0.3 * std::max(w, d); + if (w > 1.0 && d > 1.0) half = 0.08 * std::max(w, d); } return half; } -// 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. +std::vector DesignSketchTool::dbp_squares(double half) const +{ + std::vector squares; + for (size_t i = 0; i < m_dbp_planes.size(); ++i) + squares.push_back(reference_square(m_dbp_planes[i], i < m_dbp_base.size() ? m_dbp_base[i] : -1, half)); + return squares; +} + +// Dash-dot half-axes from the origin into the squares' octant, out past them along the gaps between +// them, and a disc on the origin. Both face the camera at a constant width on screen. +void DesignSketchTool::render_reference_axes(const Vec3d& origin, double half) +{ + using EPT = GLModel::Geometry::EPrimitiveType; + using EVL = GLModel::Geometry::EVertexLayout; + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d vd = cam.get_dir_forward(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); // world units per screen pixel + const double len = 1.2 * (reference_square_gap(half) + 2.0 * half); + // A dash, then a dot, every kPeriod of the axis. + constexpr double kDash = 0.07, kDotFrom = 0.10, kDotTo = 0.115, kPeriod = 0.145; + const ColorRGBA colours[3] = { ColorRGBA(0.92f, 0.28f, 0.28f, 1.0f), ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f), + ColorRGBA(0.32f, 0.55f, 0.95f, 1.0f) }; + for (int k = 0; k < 3; ++k) { + const Vec3d axis = Vec3d::Unit(k) * (len * reference_octant()[k]); + std::vector> strokes; + for (double t = 0.; t < 1.; t += kPeriod) { + strokes.push_back({ origin + axis * t, origin + axis * std::min(t + kDash, 1.) }); + if (t + kDotFrom < 1.) + strokes.push_back({ origin + axis * (t + kDotFrom), origin + axis * std::min(t + kDotTo, 1.) }); + } + GLModel::Geometry g; + append_ribbons(g, -1, strokes, vd, Vec3d::Zero(), 1.5 * upp); // body -1: already world coordinates + if (g.is_empty()) + continue; // seen end-on + GLModel m; + m.init_from(std::move(g)); + m.set_color(colours[k]); + m.render(); + } + + const Vec3d right = cam.get_dir_right(), up = cam.get_dir_up(); + auto disc = [&](double radius_px, const ColorRGBA& colour) { + constexpr int kSides = 24; + GLModel::Geometry g; + g.format = { EPT::Triangles, EVL::P3 }; + g.add_vertex((Vec3f) origin.cast()); + for (int i = 0; i < kSides; ++i) { + const double a = 2. * M_PI * i / kSides; + g.add_vertex((Vec3f) (origin + (right * std::cos(a) + up * std::sin(a)) * (radius_px * upp)).cast()); + } + for (int i = 0; i < kSides; ++i) + g.add_triangle(0, 1 + i, 1 + (i + 1) % kSides); + GLModel m; + m.init_from(std::move(g)); + m.set_color(colour); + m.render(); + }; + disc(6.0, ColorRGBA(0.30f, 0.30f, 0.32f, 1.0f)); // a dark ring round + disc(4.5, ColorRGBA(0.92f, 0.92f, 0.92f, 1.0f)); // a light centre +} + +// The reference planes, as in Fusion: translucent squares with the hovered one grey and the selected +// one solid, each base plane's name written in it, and the half-axes and origin under them. Depth +// testing is off (they overlay the bed and any bodies), so draw order is the blend order: back to +// front, piece by piece. void DesignSketchTool::render_base_pick() { if (!m_dbp_active || m_dbp_planes.empty()) return; using EPT = GLModel::Geometry::EPrimitiveType; using EVL = GLModel::Geometry::EVertexLayout; - const double H = dbp_half_extent(); - // Two layers of alpha a blended in either order differ by only a^2 of their colour difference, - // so a faint fill hides which plane is in front however well the pieces are sorted: at 0.16 that - // is under 3%, and the crossing planes read as one grey smear. At 0.35 it is ~12%, and the bed - // grid still reads through all three. - constexpr float kFillAlpha = 0.35f, kFillAlphaHot = 0.50f; - constexpr float kLineAlpha = 0.90f, kLineAlphaHot = 1.00f; - // 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 double H = dbp_half_extent(); + const std::vector squares = dbp_squares(H); + const int selected = selected_base ? selected_base() : -1; + auto base_of = [this](size_t i) { return i < m_dbp_base.size() ? m_dbp_base[i] : -1; }; + enum class Look { Idle, Hover, Selected }; + auto look = [&](size_t i) { + return base_of(i) >= 0 && base_of(i) == selected ? Look::Selected : int(i) == m_dbp_hover ? Look::Hover : Look::Idle; + }; + // Each base plane in the colour of the axis it is normal to: XY blue, XZ green, YZ red. A datum the + // Plane card offers as a base keeps a slate of its own, clear of the grey hover. + auto hue = [&](size_t i) { + switch (base_of(i)) { + case 0: return ColorRGBA(0.30f, 0.55f, 0.95f, 1.0f); + case 1: return ColorRGBA(0.35f, 0.80f, 0.45f, 1.0f); + case 2: return ColorRGBA(0.92f, 0.42f, 0.42f, 1.0f); + default: return ColorRGBA(0.55f, 0.60f, 0.72f, 1.0f); + } + }; + // Where squares overlap on screen, two layers of alpha a blended in either order differ by only + // a^2 of their colour difference, so much fainter fills would hide which one is in front however + // well the pieces are sorted. The selected plane goes near solid in its own colour: a fixed + // selection colour would match one of the three. + auto fill = [&](size_t i) { + ColorRGBA c = look(i) == Look::Hover ? ColorRGBA(0.80f, 0.80f, 0.80f, 1.0f) : hue(i); + c.a(look(i) == Look::Selected ? 0.85f : look(i) == Look::Hover ? 0.45f : 0.50f); + return c; + }; + auto ink = [&](size_t i) { + switch (look(i)) { + case Look::Selected: return ColorRGBA(0.20f, 0.25f, 0.35f, 1.0f); + case Look::Hover: return ColorRGBA(0.62f, 0.62f, 0.62f, 1.0f); + default: return hue(i); + } }; 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_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - for (const PlanePiece& piece : planes_back_to_front(m_dbp_planes, H, cam.get_position(), vd, - cam.get_type() == Camera::EType::Perspective)) { - const bool hot = piece.plane == m_dbp_hover; - ColorRGBA col = hue(piece.plane < int(m_dbp_base.size()) ? m_dbp_base[piece.plane] : -1); - GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 }; - for (const Vec3d& q : piece.corners) fill.add_vertex((Vec3f)q.cast()); - for (unsigned int i = 1; i + 1 < piece.corners.size(); ++i) fill.add_triangle(0, i, i + 1); // convex: a fan - GLModel fm; fm.init_from(std::move(fill)); - col.a(hot ? kFillAlphaHot : kFillAlpha); - fm.set_color(col); - fm.render(); + glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this; destination alpha stays 1 + glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)); - // Its outline and crossing lines go down with it, so a line behind another plane is tinted - // by it exactly like the plane it lies on. - std::vector> strokes; - for (const auto& [a, b] : piece.lines) strokes.push_back({ a, b }); - GLModel::Geometry outline; - 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(); + for (size_t i = 0; i < m_dbp_planes.size(); ++i) + if (is_base_plane(base_of(i))) { // the base planes all pass through the modeling origin + render_reference_axes(m_dbp_planes[i].origin, H); + break; } - } - // 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)); + const std::vector pieces = planes_back_to_front(squares, H, cam.get_position(), cam.get_dir_forward(), + cam.get_type() == Camera::EType::Perspective); + const SketchPlane saved_plane = m_plane; + for (size_t k = 0; k < pieces.size(); ++k) { + const PlanePiece& piece = pieces[k]; + const size_t i = size_t(piece.plane); + GLModel::Geometry g; + g.format = { EPT::Triangles, EVL::P3 }; + for (const Vec3d& q : piece.corners) g.add_vertex((Vec3f)q.cast()); + for (unsigned int c = 1; c + 1 < piece.corners.size(); ++c) g.add_triangle(0, c, c + 1); // convex: a fan + GLModel m; + m.init_from(std::move(g)); + m.set_color(fill(i)); + m.render(); + + // A base plane's name goes down with its last piece, so a square in front tints it as it + // tints the fill. + if (!is_base_plane(base_of(i)) || i >= m_dbp_labels.size() || m_dbp_labels[i].empty() + || std::any_of(pieces.begin() + k + 1, pieces.end(), [&](const PlanePiece& p) { return p.plane == piece.plane; })) + continue; + const double th = label_height(H); + Vec2d size; + std::vector> strokes = text_strokes(m_dbp_labels[i], th, size); + const Vec2d c = label_centre(H, size); + for (auto& [a, b] : strokes) { + a += c; + b += c; + } + m_plane = squares[i]; // draw_strokes lifts plane coordinates through m_plane + draw_strokes(m_line_model, strokes, 0.07 * th, ink(i)); } + for (size_t i = 0; i < m_dbp_planes.size() && i < m_dbp_labels.size(); ++i) + if (!is_base_plane(base_of(i)) && !m_dbp_labels[i].empty()) { + m_plane = squares[i]; // draw_text anchors through m_plane too + draw_text(m_line_model, m_dbp_labels[i], datum_label_anchor(H), H * 0.10, ink(i)); + } m_plane = saved_plane; glsafe(::glDisable(GL_BLEND)); } -// Ray-pick the reference planes: intersect the mouse ray with each plane, keep hits inside the -// square, return the index of the nearest by |t|. -1 on miss. +// The reference plane under the cursor, or -1. A plane's name wins over the squares: it is the part +// of a plane a user aims at deliberately, and from the back the XZ square stands between the eye and +// the XY name, so a ray pick alone reports "XZ plane selected" for a click on the word XY. int DesignSketchTool::hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const { if (!m_dbp_active) return -1; - const double H = dbp_half_extent(); - - // 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 — - // 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 - // "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. - // Anchor and text height must track render_base_pick's, which is where they are drawn. - const Camera& cam = wxGetApp().plater()->get_camera(); - const double th = H * 0.10; - const Vec2d anchor(-H + th * 2.0, H - th * 1.6); - int lbest = -1; double lbest_d = 1e30; - for (size_t i = 0; i < m_dbp_planes.size(); ++i) { - if (i >= m_dbp_labels.size() || m_dbp_labels[i].empty()) continue; - const wxPoint sp = world_to_screen_px(cam, m_dbp_planes[i].to_world(anchor)); - if (sp.x < 0 && sp.y < 0) continue; // behind the camera - const double dx = std::abs(double(evt.GetX() - sp.x)); - const double dy = std::abs(double(evt.GetY() - sp.y)); - // Chip half-extents in px, scaled like the label itself. Generous rather than tight: - // missing the text and silently selecting a different plane is the failure being fixed. - const double hw = (9.0 + 5.0 * double(m_dbp_labels[i].size())) * double(m_render_scale); - const double hh = 11.0 * double(m_render_scale); - if (dx > hw || dy > hh) continue; - const double d = dx * dx + dy * dy; // nearest label if chips overlap + const double H = dbp_half_extent(); + const std::vector squares = dbp_squares(H); + const Camera& cam = wxGetApp().plater()->get_camera(); + // As world_to_screen_px, but refusing a point behind the eye, which would come back mirrored. + const Eigen::Matrix4d to_clip = cam.get_projection_matrix().matrix() * cam.get_view_matrix().matrix(); + const std::array& vp = cam.get_viewport(); + auto to_screen = [&](const Vec3d& x, Vec2d& out) { + const Eigen::Vector4d clip = to_clip * x.homogeneous(); + if (clip.w() <= 1e-9) + return false; + const Vec3d ndc = clip.head<3>() / clip.w(); + out = Vec2d(vp[0] + (ndc.x() * 0.5 + 0.5) * vp[2], vp[1] + (1.0 - (ndc.y() * 0.5 + 0.5)) * vp[3]); + return true; + }; + const Vec2d mouse(evt.GetX(), evt.GetY()); + int lbest = -1; + double lbest_d = std::numeric_limits::max(); + for (size_t i = 0; i < squares.size() && i < m_dbp_labels.size(); ++i) { + if (m_dbp_labels[i].empty()) continue; + Vec2d centre; + if (is_base_plane(i < m_dbp_base.size() ? m_dbp_base[i] : -1)) { + // The name lies in its square, so its box is hit as projected, turning with the plane. + const std::array box = reference_label_box(squares[i], H, m_dbp_labels[i]); + std::array q; + bool seen = true; + for (size_t c = 0; c < 4 && seen; ++c) seen = to_screen(box[c], q[c]); + if (!seen) continue; + double area = 0.; + bool left = true, right = true; // the cursor on one side of every edge: inside + for (size_t c = 0; c < 4; ++c) { + const Vec2d& a = q[c]; + const Vec2d& b = q[(c + 1) % 4]; + const double s = (b.x() - a.x()) * (mouse.y() - a.y()) - (b.y() - a.y()) * (mouse.x() - a.x()); + left = left && s >= 0.; + right = right && s <= 0.; + area += a.x() * b.y() - b.x() * a.y(); + } + if (std::abs(area) < 8. || !(left || right)) + continue; // edge-on, or outside + centre = 0.25 * (q[0] + q[1] + q[2] + q[3]); + } else { + // A datum's name is a screen chip. Generous rather than tight: missing the text and + // silently selecting a different plane is the failure this guards against. + if (!to_screen(squares[i].to_world(datum_label_anchor(H)), centre)) continue; + const double hw = (9.0 + 5.0 * double(m_dbp_labels[i].size())) * double(m_render_scale); + const double hh = 11.0 * double(m_render_scale); + if (std::abs(mouse.x() - centre.x()) > hw || std::abs(mouse.y() - centre.y()) > hh) continue; + } + const double d = (mouse - centre).squaredNorm(); // the nearest name if several hold the cursor if (d < lbest_d) { lbest_d = d; lbest = int(i); } } if (lbest >= 0) return lbest; const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); - const Vec3d ro = r.a, rd = r.b - r.a; - int best = -1; double best_t = 1e30; - for (size_t i = 0; i < m_dbp_planes.size(); ++i) { - const SketchPlane& p = m_dbp_planes[i]; - const double dn = rd.dot(p.normal); - if (std::abs(dn) < 1e-9) continue; // ray parallel to plane - const double t = (p.origin - ro).dot(p.normal) / dn; - if (t < 0) continue; // behind the camera - const Vec3d hit = ro + rd * t; - const Vec3d d = hit - p.origin; - if (std::abs(d.dot(p.x_axis)) > H || std::abs(d.dot(p.y_axis)) > H) continue; - if (t < best_t) { best_t = t; best = int(i); } - } - return best; + return pick_reference_square(squares, H, r.a, r.b - r.a); } // ---- Move-body gizmo (M5) ------------------------------------------------------------- @@ -7615,9 +7715,9 @@ void DesignSketchTool::draw_strokes(GLModel& model, const std::vector>& out, double& ad break; } } + +std::vector> text_strokes(const std::string& text, double height, Vec2d& size) +{ + std::vector> out; + double pen = 0.; + for (char c : text) { + std::vector> glyph; + double advance = 0.; + glyph_strokes(c, glyph, advance); + for (const auto& [a, b] : glyph) + out.emplace_back((a + Vec2d(pen, 0.)) * height, (b + Vec2d(pen, 0.)) * height); + pen += advance; + } + BoundingBoxf box; + for (const auto& [a, b] : out) { + box.merge(a); + box.merge(b); + } + if (!box.defined) { + size = Vec2d::Zero(); + return out; + } + const Vec2d mid = box.center(); + for (auto& [a, b] : out) { + a -= mid; + b -= mid; + } + size = box.size(); + return out; +} } // namespace void DesignSketchTool::draw_dim_label(const std::string& txt, const Vec2d& plane_center) @@ -7787,8 +7917,9 @@ void DesignSketchTool::draw_text(GLModel& /*model*/, const std::string& s, const double /*height*/, const ColorRGBA& /*color*/) { // ponytail: all sketch labels now render as Measure-gizmo-style ImGui labels for visual - // parity with the Prepare/Preview tabs; the old vector-font path (glyph_strokes/draw_strokes - // for text) is retired. Leader lines/arrows still draw via draw_strokes at the call sites. + // parity with the Prepare/Preview tabs; the vector font (glyph_strokes/draw_strokes) is kept + // only for text lying in a plane, the reference planes' names. Leader lines/arrows still draw + // via draw_strokes at the call sites. // // The one label we do NOT draw is the one under an OPEN value field: the field is anchored // over it and carries the same number plus its title, so leaving the label in place shows @@ -9525,7 +9656,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) render_datum_planes(); render_mate_connectors(); render_solid_highlight(); - if (m_dbp_active) render_base_pick(); + if (draws_reference_axes()) render_base_pick(); // the canvas drops the bed's triad on the same test if (m_dz_active) render_datum_gizmo(); if (m_hx_active) render_helix_gizmo(); if (m_rb_active) render_rib_gizmo(); diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 88c003bc36..48c238a916 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -11,6 +11,7 @@ #include "slic3r/GUI/GLModel.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies #include +#include #include #include #include @@ -56,21 +57,35 @@ inline ColorRGBA design_idle_face_color() 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. +// A convex piece of the square drawn on planes[plane]. struct PlanePiece { - int plane; - std::vector corners; - std::vector> lines; + int plane; + std::vector corners; }; -// 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. +// The squares of half-extent `half` centred on `planes`' origins, 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 planes_back_to_front(const std::vector& planes, double half, const Vec3d& eye, const Vec3d& forward, bool perspective); +// The square a reference plane is drawn as, given as the frame at its centre, half-extent `half`. +// The base planes XY, XZ and YZ (`base` 0, 1, 2) sit in the octant (+X, -Y, +Z), the one all three +// names face, reference_square_gap(half) clear of the two axes bounding each, so the three never cross +// and the axes run between them. Any other base (a datum) stays centred on its own origin. The frame is the same +// plane moved within itself, so planes_back_to_front and pick_reference_square take it unchanged. +SketchPlane reference_square(const SketchPlane& plane, int base, double half); +inline double reference_square_gap(double half) { return 0.25 * half; } +// The corners of the box around a base square's label, in the square: inset from its (+x, +y) corner and +// written along the square's x axis with its y axis up, so it turns with the plane. The box the hit +// test takes for the label: round the strokes render_base_pick draws, from the same layout, with a +// margin to aim at. +std::array reference_label_box(const SketchPlane& square, double half, const std::string& text); +// The nearest of `squares` (half-extent `half`) the ray from `from` along `dir` crosses, or -1. +int pick_reference_square(const std::vector& squares, double half, const Vec3d& from, const Vec3d& dir); + class DesignSketchTool { public: enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect, @@ -348,6 +363,12 @@ public: std::vector labels = {}); void clear_base_pick(); std::function on_datum_base_picked; + // The base drawn as selected, or -1. Asked once a frame rather than set, because what decides it + // (the panel's chosen sketch plane, a picked face, the Plane card's base) changes in many places. + std::function selected_base; + // The reference planes are drawn this frame, with their own half-axes and origin mark: the canvas + // leaves out the bed's axes triad, which would sit on top of them. + bool draws_reference_axes() const { return m_dbp_active && !m_active; } // Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch // tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid @@ -1418,7 +1439,9 @@ private: std::vector m_dbp_base; std::vector m_dbp_labels; int m_dbp_hover{-1}; - double dbp_half_extent() const; // bed-derived: reference planes are larger than the bed + double dbp_half_extent() const; // bed-derived square size + std::vector dbp_squares(double half) const; // reference_square of each entry + void render_reference_axes(const Vec3d& origin, double half); void render_base_pick(); int hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 63fb7230dc..729b3aa0c4 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2425,8 +2425,11 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size) if (m_canvas_type == ECanvasType::CanvasView3D) { // m_show_bed gates the plate list too: hiding the bed but leaving its grid and outline // floating would read as a rendering fault rather than a deliberate view option. + // Design tab: while its reference planes are up they draw their own axes from the modeling + // origin, where the bed's triad would otherwise sit on top of them. if (show_bed) - _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes); + _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), + m_show_world_axes && !(m_design_sketch_tool != nullptr && m_design_sketch_tool->draws_reference_axes())); m_frame_profiler.mark("bed"); 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); diff --git a/tests/slic3rutils/test_design_sketch_tool.cpp b/tests/slic3rutils/test_design_sketch_tool.cpp index 685f661fc3..4e8a348645 100644 --- a/tests/slic3rutils/test_design_sketch_tool.cpp +++ b/tests/slic3rutils/test_design_sketch_tool.cpp @@ -59,6 +59,17 @@ void show_two_sketches(DesignSketchTool& tool) // The Design tab's base planes, all through one origin, as DesignPanel shows them. std::vector base_planes() { return { SketchPlane::XY(), SketchPlane::XZ(), SketchPlane::YZ() }; } +// The squares the base planes are drawn as, through a modeling origin at `origin`. +std::vector reference_squares(const Vec3d& origin, double half) +{ + std::vector squares; + for (SketchPlane plane : base_planes()) { + plane.origin += origin; + squares.push_back(reference_square(plane, int(squares.size()), half)); + } + return squares; +} + struct View { Vec3d eye; @@ -108,25 +119,6 @@ std::optional hit_distance(const PlanePiece& piece, const SketchPlane& p return t; } -// Whether x lies on one of the segments the pieces are outlined with. -bool outlined(const std::vector& 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 pieces_of(const std::vector& 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 @@ -134,15 +126,15 @@ struct SightLines }; // 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& planes, double half, const View& view) +// every piece behind it. The lines of sight aim at points of the box [lo, hi]. +SightLines sight_lines(const std::vector& planes, double half, const View& view, const Vec3d& lo, const Vec3d& hi) { const std::vector pieces = planes_back_to_front(planes, half, view.eye, view.forward, view.perspective); std::mt19937 rng(7); - std::uniform_real_distribution coord(-0.95 * half, 0.95 * half); + std::uniform_real_distribution unit(0., 1.); SightLines seen; for (int r = 0; r < 500; ++r) { - const Vec3d target(coord(rng), coord(rng), coord(rng)); + const Vec3d target = lo + (hi - lo).cwiseProduct(Vec3d(unit(rng), unit(rng), unit(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::max(); @@ -231,7 +223,7 @@ TEST_CASE("Fit frames nothing when the Design tab shows nothing", "[DesignSketch 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); + const SightLines seen = sight_lines(base_planes(), 75., view, Vec3d::Constant(-0.95 * 75.), Vec3d::Constant(0.95 * 75.)); CHECK(seen.overlapping >= 200); // of the 500: most lines of sight into the planes cross two CHECK(seen.out_of_order == 0); } @@ -250,51 +242,11 @@ TEST_CASE("Datum planes are drawn back to front among the base planes", "[Design planes.push_back(tilted); const View& view = kViews[GENERATE(range(0, int(std::size(kViews))))]; - const SightLines seen = sight_lines(planes, 75., view); + const SightLines seen = sight_lines(planes, 75., view, Vec3d::Constant(-0.95 * 75.), Vec3d::Constant(0.95 * 75.)); 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 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 planes = base_planes(); - SketchPlane beyond = SketchPlane::YZ(); - beyond.origin = Vec3d(100., 0., 0.); - planes.push_back(beyond); - - const std::vector 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 planes = base_planes(); - SketchPlane raised = SketchPlane::XY(); - raised.origin = Vec3d(60., 0., 30.); - planes.push_back(raised); - - const std::vector 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 planes = base_planes(); @@ -307,6 +259,98 @@ TEST_CASE("Cutting the base planes along each other keeps every plane whole", "[ CHECK_THAT(a, WithinRel(4. * half * half, 1e-9)); } +TEST_CASE("Base reference planes sit in the octant their names face, clear of the axes", "[DesignSketchTool]") +{ + // Each square is flat in one coordinate and at least `gap` out along the other two, toward + // (+X, -Y, +Z), so no two of them meet: a point of XY has z = 0, every point of XZ and YZ has + // z >= gap. + const Vec3d origin(128., 128., 0.), octant(1., -1., 1.); + const double half = 32., gap = reference_square_gap(half); + const std::vector squares = reference_squares(origin, half); + const int flat[] = { 2, 1, 0 }; // XY in z, XZ in y, YZ in x + for (int i = 0; i < 3; ++i) { + // The side a name reads from faces into the same octant. + CHECK(squares[i].x_axis.cross(squares[i].y_axis).dot(octant) > 0.); + for (const Vec2d& corner : { Vec2d(-half, -half), Vec2d(half, -half), Vec2d(half, half), Vec2d(-half, half) }) { + const Vec3d x = squares[i].to_world(corner) - origin; + for (int k = 0; k < 3; ++k) + if (k == flat[i]) + CHECK_THAT(x[k], WithinAbs(0., 1e-9)); + else { + CHECK(octant[k] * x[k] >= gap - 1e-9); + CHECK(octant[k] * x[k] <= gap + 2. * half + 1e-9); + } + } + } +} + +TEST_CASE("From the front no base reference plane stands behind another", "[DesignSketchTool]") +{ + // Looking from the front and above, as the Design tab opens: XY lies below the X axis on screen + // and XZ above it, and YZ is seen edge-on, so no line of sight crosses two squares. + const double half = 32., e = reference_square_gap(half) + 2. * half; + const std::vector squares = reference_squares(Vec3d::Zero(), half); + const double elevation = M_PI / 180. * GENERATE(20., 45., 70.); + const View front{ Vec3d::Zero(), Vec3d(0., std::cos(elevation), -std::sin(elevation)), false }; + CHECK(sight_lines(squares, half, front, Vec3d(0., -e, 0.), Vec3d(e, 0., e)).overlapping == 0); +} + +TEST_CASE("A datum's reference square stays centred on its origin", "[DesignSketchTool]") +{ + SketchPlane datum = SketchPlane::XY(); + datum.origin = Vec3d(10., 20., 30.); + CHECK((reference_square(datum, 3, 32.).origin - datum.origin).norm() < 1e-12); +} + +TEST_CASE("Base reference planes are drawn back to front without being cut", "[DesignSketchTool]") +{ + const double half = 32.; + const std::vector squares = reference_squares(Vec3d::Zero(), half); + int overlapping = 0; + for (const View& view : kViews) { + const std::vector pieces = planes_back_to_front(squares, half, view.eye, view.forward, view.perspective); + CHECK(pieces.size() == 3); + for (const PlanePiece& piece : pieces) + CHECK(piece.corners.size() == 4); + const double e = reference_square_gap(half) + 2. * half; + const SightLines seen = sight_lines(squares, half, view, Vec3d(0., -e, 0.), Vec3d(e, 0., e)); + CHECK(seen.out_of_order == 0); + overlapping += seen.overlapping; + } + // These views do see squares behind one another, so the order is actually put to the test. + CHECK(overlapping >= 50); +} + +TEST_CASE("A click takes the nearest reference square along the ray", "[DesignSketchTool]") +{ + const double half = 32.; // the squares span 8..72 out along both their axes + const std::vector squares = reference_squares(Vec3d::Zero(), half); + // From the front left through YZ (x = 0) at (0, -20, 40), then on through XZ (y = 0) at (20, 0, 40). + const Vec3d along(1., 1., 0.); + CHECK(pick_reference_square(squares, half, Vec3d(-40., -60., 40.), along) == 2); + // The same line walked the other way meets XZ first. + CHECK(pick_reference_square(squares, half, Vec3d(60., 40., 40.), -along) == 1); + // Straight down onto XY, and down the gap beside it, which holds nothing. + CHECK(pick_reference_square(squares, half, Vec3d(40., -40., 100.), Vec3d(0., 0., -1.)) == 0); + CHECK(pick_reference_square(squares, half, Vec3d(4., -40., 100.), Vec3d(0., 0., -1.)) == -1); + // Looking away from the squares. + CHECK(pick_reference_square(squares, half, Vec3d(40., -40., 100.), Vec3d(0., 0., 1.)) == -1); +} + +TEST_CASE("A base plane's name lies inside its own square", "[DesignSketchTool]") +{ + const double half = 32.; + const std::vector squares = reference_squares(Vec3d(128., 128., 0.), half); + const char* names[] = { "XY", "XZ", "YZ" }; + for (int i = 0; i < 3; ++i) + for (const Vec3d& corner : reference_label_box(squares[i], half, names[i])) { + const Vec3d d = corner - squares[i].origin; + CHECK_THAT(d.dot(squares[i].x_axis.cross(squares[i].y_axis)), WithinAbs(0., 1e-9)); + CHECK(std::abs(d.dot(squares[i].x_axis)) < half); + CHECK(std::abs(d.dot(squares[i].y_axis)) < half); + } +} + TEST_CASE("A move arrow drag moves the body by the cursor's travel, wherever the arrow is grabbed", "[DesignSketchTool]") { DesignSketchTool tool;