Make the Design tab's reference planes readable where they cross

The XY/XZ/YZ planes are cut along each other and drawn back to front, with
lines along every crossing, and their fills are strong enough for the order
to show. Before, they blended into one grey smear and were too pale to work with.
This commit is contained in:
SoftFever
2026-10-06 14:19:46 +08:00
parent afa9252b59
commit e328b60992
3 changed files with 405 additions and 30 deletions
@@ -13,8 +13,19 @@
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <cmath>
#include <initializer_list>
#include <iterator>
#include <limits>
#include <optional>
#include <random>
#include <vector>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/Point.hpp"
@@ -24,6 +35,7 @@
using namespace Slic3r;
using namespace Slic3r::GUI;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
namespace {
@@ -43,6 +55,112 @@ void show_two_sketches(DesignSketchTool& tool)
{ { circle({ -60., 0. }, 5.) }, SketchPlane::XY(), 2 } });
}
// The Design tab's base planes, all through one origin, as DesignPanel shows them.
std::vector<SketchPlane> base_planes() { return { SketchPlane::XY(), SketchPlane::XZ(), SketchPlane::YZ() }; }
struct View
{
Vec3d eye;
Vec3d forward;
bool perspective;
};
// Eyes in four octants, off every plane, plus two orthographic directions.
const View kViews[] = {
{ Vec3d(300., -400., 250.), Vec3d(-300., 400., -250.).normalized(), true },
{ Vec3d(-350., -200., 300.), Vec3d(350., 200., -300.).normalized(), true },
{ Vec3d(250., 300., -200.), Vec3d(-250., -300., 200.).normalized(), true },
{ Vec3d(-300., 350., -250.), Vec3d(300., -350., 250.).normalized(), true },
{ Vec3d::Zero(), Vec3d(-0.5, 0.7, -0.5).normalized(), false },
{ Vec3d::Zero(), Vec3d(0.3, 0.4, 0.85).normalized(), false },
};
double area(const PlanePiece& piece, const SketchPlane& plane)
{
Vec3d sum = Vec3d::Zero();
for (size_t i = 0; i < piece.corners.size(); ++i)
sum += piece.corners[i].cross(piece.corners[(i + 1) % piece.corners.size()]);
return 0.5 * std::abs(sum.dot(plane.x_axis.cross(plane.y_axis)));
}
// How far along the ray (from, unit dir) it crosses `piece`, or nothing if it misses.
std::optional<double> hit_distance(const PlanePiece& piece, const SketchPlane& plane, const Vec3d& from, const Vec3d& dir)
{
const Vec3d n = plane.x_axis.cross(plane.y_axis);
const double dn = n.dot(dir);
if (std::abs(dn) < 1e-9)
return std::nullopt;
const double t = n.dot(piece.corners.front() - from) / dn;
if (t <= 0.)
return std::nullopt;
const Vec3d x = from + dir * t;
double lo = 0., hi = 0.;
for (size_t i = 0; i < piece.corners.size(); ++i) {
const Vec3d& a = piece.corners[i];
const Vec3d& b = piece.corners[(i + 1) % piece.corners.size()];
const double s = (b - a).cross(x - a).dot(n);
lo = std::min(lo, s);
hi = std::max(hi, s);
}
if (lo < 0. && hi > 0.)
return std::nullopt; // outside one of the edges
return t;
}
// Whether x lies on one of the segments the pieces are outlined with.
bool outlined(const std::vector<PlanePiece>& pieces, const Vec3d& x)
{
for (const PlanePiece& piece : pieces)
for (const auto& [a, b] : piece.lines) {
const Vec3d ab = b - a;
const double t = std::clamp((x - a).dot(ab) / ab.squaredNorm(), 0., 1.);
if ((a + ab * t - x).norm() < 1e-6)
return true;
}
return false;
}
std::vector<PlanePiece> pieces_of(const std::vector<SketchPlane>& planes)
{
const View& view = kViews[0];
return planes_back_to_front(planes, 75., view.eye, view.forward, view.perspective);
}
struct SightLines
{
int overlapping = 0; // sight lines through two or more pieces, where draw order matters
int out_of_order = 0; // ...of which meet a nearer piece before a farther one
};
// Translucent pieces blend correctly only if, along every line of sight, each piece is drawn after
// every piece behind it.
SightLines sight_lines(const std::vector<SketchPlane>& planes, double half, const View& view)
{
const std::vector<PlanePiece> pieces = planes_back_to_front(planes, half, view.eye, view.forward, view.perspective);
std::mt19937 rng(7);
std::uniform_real_distribution<double> coord(-0.95 * half, 0.95 * half);
SightLines seen;
for (int r = 0; r < 500; ++r) {
const Vec3d target(coord(rng), coord(rng), coord(rng));
const Vec3d from = view.perspective ? view.eye : Vec3d(target - view.forward * (10. * half));
const Vec3d dir = (target - from).normalized();
double last = std::numeric_limits<double>::max();
int hits = 0;
bool ok = true;
for (const PlanePiece& piece : pieces)
if (const std::optional<double> t = hit_distance(piece, planes[piece.plane], from, dir)) {
ok = ok && *t <= last + 1e-6 * half;
last = *t;
++hits;
}
if (hits >= 2) {
++seen.overlapping;
seen.out_of_order += ok ? 0 : 1;
}
}
return seen;
}
} // namespace
TEST_CASE("Fit frames the picked sketch region, not the other sketches", "[DesignSketchTool]")
@@ -108,3 +226,82 @@ TEST_CASE("Fit frames nothing when the Design tab shows nothing", "[DesignSketch
GLVolumeCollection no_bodies;
CHECK_FALSE(tool.fit_box(no_bodies).defined);
}
TEST_CASE("Crossing base planes are drawn back to front from any viewpoint", "[DesignSketchTool]")
{
const View& view = kViews[GENERATE(range(0, int(std::size(kViews))))];
const SightLines seen = sight_lines(base_planes(), 75., view);
CHECK(seen.overlapping >= 200); // of the 500: most lines of sight into the planes cross two
CHECK(seen.out_of_order == 0);
}
TEST_CASE("Datum planes are drawn back to front among the base planes", "[DesignSketchTool]")
{
// A datum parallel to XY 30 mm up, and one tilted 30 degrees about X through (0, 0, 10).
std::vector<SketchPlane> planes = base_planes();
SketchPlane raised = SketchPlane::XY();
raised.origin = Vec3d(0., 0., 30.);
SketchPlane tilted;
tilted.origin = Vec3d(0., 0., 10.);
tilted.y_axis = Vec3d(0., std::cos(M_PI / 6.), std::sin(M_PI / 6.));
tilted.normal = tilted.x_axis.cross(tilted.y_axis);
planes.push_back(raised);
planes.push_back(tilted);
const View& view = kViews[GENERATE(range(0, int(std::size(kViews))))];
const SightLines seen = sight_lines(planes, 75., view);
CHECK(seen.overlapping >= 200);
CHECK(seen.out_of_order == 0);
}
TEST_CASE("Base planes are outlined along every line where they cross", "[DesignSketchTool]")
{
// XY, XZ and YZ cross along the three axes, all through the middle of each 75 mm half-square.
const std::vector<PlanePiece> pieces = pieces_of(base_planes());
int missed = 0;
for (double t = -70.; t <= 70.; t += 10.)
for (const Vec3d& axis : { Vec3d(1., 0., 0.), Vec3d(0., 1., 0.), Vec3d(0., 0., 1.) })
missed += outlined(pieces, axis * t) ? 0 : 1;
CHECK(missed == 0);
}
TEST_CASE("A datum clear of the base planes gets no lines across it", "[DesignSketchTool]")
{
// Parallel to YZ at x = 100, past the 75 mm half-squares of XY and XZ: the infinite XY and XZ
// planes still cut it, along z = 0 and y = 0, but the squares never meet.
std::vector<SketchPlane> planes = base_planes();
SketchPlane beyond = SketchPlane::YZ();
beyond.origin = Vec3d(100., 0., 0.);
planes.push_back(beyond);
const std::vector<PlanePiece> pieces = pieces_of(planes);
CHECK_FALSE(outlined(pieces, Vec3d(100., 30., 0.)));
CHECK_FALSE(outlined(pieces, Vec3d(100., 0., 30.)));
}
TEST_CASE("A line where two squares cross stops where either square ends", "[DesignSketchTool]")
{
// Parallel to XY 30 mm up and moved 60 mm along X, so it spans x = -15..135. It meets XZ along
// y = 0, z = 30, but XZ's square only reaches x = 75.
std::vector<SketchPlane> planes = base_planes();
SketchPlane raised = SketchPlane::XY();
raised.origin = Vec3d(60., 0., 30.);
planes.push_back(raised);
const std::vector<PlanePiece> pieces = pieces_of(planes);
CHECK(outlined(pieces, Vec3d(0., 0., 30.)));
CHECK(outlined(pieces, Vec3d(70., 0., 30.)));
CHECK_FALSE(outlined(pieces, Vec3d(100., 0., 30.)));
}
TEST_CASE("Cutting the base planes along each other keeps every plane whole", "[DesignSketchTool]")
{
const std::vector<SketchPlane> planes = base_planes();
const double half = 75.;
const View& view = kViews[0];
std::vector<double> covered(planes.size(), 0.);
for (const PlanePiece& piece : planes_back_to_front(planes, half, view.eye, view.forward, view.perspective))
covered[piece.plane] += area(piece, planes[piece.plane]);
for (double a : covered)
CHECK_THAT(a, WithinRel(4. * half * half, 1e-9));
}