mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 15:21:09 +00:00
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.
This commit is contained in:
@@ -1037,6 +1037,11 @@ void DesignCanvas::set_on_datum_base_picked(std::function<void(int)> cb)
|
||||
m_sketch_tool.on_datum_base_picked = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_selected_base(std::function<int()> cb)
|
||||
{
|
||||
m_sketch_tool.selected_base = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_exit(std::function<void()> cb)
|
||||
{
|
||||
m_sketch_tool.on_exit = std::move(cb);
|
||||
|
||||
@@ -238,6 +238,7 @@ public:
|
||||
std::vector<std::string> labels = {}); // clickable labelled reference planes
|
||||
void clear_base_pick();
|
||||
void set_on_datum_base_picked(std::function<void(int)> cb);
|
||||
void set_selected_base(std::function<int()> cb); // the reference plane drawn selected, or -1
|
||||
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
|
||||
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
|
||||
void set_on_sketch_notice(std::function<void(const std::string&, bool)> cb); // tool refusals/side effects
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
#include <map>
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <GL/glew.h>
|
||||
#include <glad/gl.h>
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <vector>
|
||||
@@ -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<std::pair<Vec2d, Vec2d>> 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<PlanePiece>&& 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<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& planes, double half,
|
||||
@@ -5487,41 +5479,52 @@ std::vector<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& 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<PlanePiece> 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<Vec3d, 4> 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<SketchPlane>& squares, double half, const Vec3d& from, const Vec3d& dir)
|
||||
{
|
||||
int best = -1;
|
||||
double best_t = std::numeric_limits<double>::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<SketchPlane> planes, std::vector<int> 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<SketchPlane> DesignSketchTool::dbp_squares(double half) const
|
||||
{
|
||||
std::vector<SketchPlane> 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<std::vector<Vec3d>> 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<float>());
|
||||
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<float>());
|
||||
}
|
||||
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<SketchPlane> 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<float>());
|
||||
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<std::vector<Vec3d>> 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<PlanePiece> 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<float>());
|
||||
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<std::pair<Vec2d, Vec2d>> 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<SketchPlane> 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<int, 4>& 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<double>::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<Vec3d, 4> box = reference_label_box(squares[i], H, m_dbp_labels[i]);
|
||||
std::array<Vec2d, 4> 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<std::pair<
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Smooth single-stroke (Hershey-style) vector font for dimension labels. Glyphs
|
||||
// live in a 0..0.6 (x) by 0..1 (y) cell, baseline at y=0, cap height y=1; curved
|
||||
// digits are sampled as short segments so they read as rounded shapes, not blocks.
|
||||
// Smooth single-stroke (Hershey-style) vector font, for text lying in a plane: the reference planes'
|
||||
// names (render_base_pick). Glyphs live in a 0..0.6 (x) by 0..1 (y) cell, baseline at y=0, cap
|
||||
// height y=1; curved digits are sampled as short segments so they read as rounded shapes, not blocks.
|
||||
// `advance` is the pen step after the glyph.
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
inline double rad(double deg) { return deg * kPi / 180.0; }
|
||||
@@ -7727,6 +7827,36 @@ void glyph_strokes(char c, std::vector<std::pair<Vec2d, Vec2d>>& out, double& ad
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<Vec2d, Vec2d>> text_strokes(const std::string& text, double height, Vec2d& size)
|
||||
{
|
||||
std::vector<std::pair<Vec2d, Vec2d>> out;
|
||||
double pen = 0.;
|
||||
for (char c : text) {
|
||||
std::vector<std::pair<Vec2d, Vec2d>> 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();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
|
||||
#include <Eigen/Core>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <wx/event.h>
|
||||
#include <functional>
|
||||
@@ -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<Vec3d> corners;
|
||||
std::vector<std::pair<Vec3d, Vec3d>> lines;
|
||||
int plane;
|
||||
std::vector<Vec3d> 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<PlanePiece> planes_back_to_front(const std::vector<SketchPlane>& 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<Vec3d, 4> 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<SketchPlane>& 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<std::string> labels = {});
|
||||
void clear_base_pick();
|
||||
std::function<void(int base)> 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<int()> 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<int> m_dbp_base;
|
||||
std::vector<std::string> 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<SketchPlane> 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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<SketchPlane> 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<SketchPlane> reference_squares(const Vec3d& origin, double half)
|
||||
{
|
||||
std::vector<SketchPlane> 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<double> 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<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
|
||||
@@ -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<SketchPlane>& 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<SketchPlane>& planes, double half, const View& view, const Vec3d& lo, const Vec3d& hi)
|
||||
{
|
||||
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);
|
||||
std::uniform_real_distribution<double> 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<double>::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<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();
|
||||
@@ -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<SketchPlane> 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<SketchPlane> 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<SketchPlane> squares = reference_squares(Vec3d::Zero(), half);
|
||||
int overlapping = 0;
|
||||
for (const View& view : kViews) {
|
||||
const std::vector<PlanePiece> 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<SketchPlane> 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<SketchPlane> 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;
|
||||
|
||||
Reference in New Issue
Block a user