// ============================================================================
// TRUE VERTEX / EDGE DRAG — move a corner and leave its neighbours alone.
//
// Everything else in this store moves PLANES. That is what makes it exact: a face
// is one plane, an edge is two, a corner is three, and moving any of them is
// d_i = n_i . P with no solving. It is also why dragging a corner drags every face
// that meets there, and with them every OTHER corner on those faces — which is
// correct for the representation and wrong for the person doing it. Reported four
// times as "vertex mode moves the whole brush"; it is not a bug in the operator.
//
// The only way a corner moves ALONE is to stop treating the brush as a plane set
// for the duration: take its vertices, move the one, and rebuild the solid from the
// points. That is TrenchBroom's model (Polyhedron_Vertex.h), and it costs exactly
// what theirs costs — the plane set is re-derived, so faces are no longer the same
// objects and their identity has to be re-matched afterwards (Polyhedron_Matcher.h).
//
// So this is deliberately a SEPARATE operation with its own record kind. The exact
// path stays exact; this one is the one that trades exactness for the drag you
// actually want, and it says so.
//
// THE HULL IS BRUTE FORCE OVER TRIPLES, and that is a choice, not a shortcut. A
// brush has 8-30 vertices, so every triple with an all-points side test is a few
// thousand operations — nothing, once, on a keypress. Quickhull would be faster and
// would bring merge/degeneracy cases that are famously easy to get subtly wrong;
// this cannot get them wrong, because it only ever asks "is every point on one side
// of the plane through these three".
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/bhull \
// tests/test_brush_hull.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_brush_hull.h"
#include "dc_csg4.h"
#include <cstdio>
#include <cmath>
static int fails = 0;
static void ok(const char* what, bool cond) {
if (!cond) fails++;
std::printf(" %-62s %s\n", what, cond ? "OK" : "**FAIL**");
}
static void eqi(const char* what, long got, long want) {
bool c = got == want;
if (!c) fails++;
std::printf(" %-62s %-5ld want %-5ld %s\n", what, got, want, c ? "OK" : "**FAIL**");
}
static bool has_point(const std::vector<CSG4Vec3>& v, float x, float y, float z,
float eps = 1e-3f) {
for (const CSG4Vec3& p : v)
if (std::fabs(p.x - x) < eps && std::fabs(p.y - y) < eps &&
std::fabs(p.z - z) < eps) return true;
return false;
}
// A unit cube 0..1 on every axis, as six half-spaces.
static uint32_t add_cube(DCBrushStore& s, const char* key) {
std::vector<ConvexPlane> pl = {
{-1, 0, 0, 0}, {1, 0, 0, 1},
{0, -1, 0, 0}, {0, 1, 0, 1},
{0, 0, -1, 0}, {0, 0, 1, 1},
};
// Distinct projections per face, so the texture matcher has something to get
// right or wrong.
std::vector<uint32_t> proj = {11, 12, 13, 14, 15, 16};
return s.add_brush(pl, proj, 7, 0, false, key);
}
int main() {
std::printf("=== brush hull: true vertex and edge drag ===\n\n");
std::printf("-- the hull itself --\n");
{
std::vector<CSG4Vec3> pts = {
{0,0,0},{1,0,0},{1,1,0},{0,1,0},
{0,0,1},{1,0,1},{1,1,1},{0,1,1},
};
std::vector<ConvexPlane> out;
ok("a cube's eight corners hull", dc_hull::hull_planes(pts, out));
eqi("into exactly six planes", (long)out.size(), 6);
// Every corner must satisfy every plane: the hull CONTAINS its own points.
bool inside = true;
for (const CSG4Vec3& p : pts)
for (const ConvexPlane& q : out)
if (p.x*q.nx + p.y*q.ny + p.z*q.nz > q.d + 1e-3f) inside = false;
ok("and every corner is inside all six", inside);
// Interior points must not create faces.
pts.push_back({0.5f, 0.5f, 0.5f});
out.clear();
dc_hull::hull_planes(pts, out);
eqi("an interior point adds no face", (long)out.size(), 6);
std::vector<CSG4Vec3> flat = {{0,0,0},{1,0,0},{0,1,0},{1,1,0}};
out.clear();
ok("a flat point set has no hull, and says so", !dc_hull::hull_planes(flat, out));
}
std::printf("\n-- ONE CORNER MOVES, THE OTHER SEVEN DO NOT --\n");
std::printf(" (this is the whole point; the plane-move path fails it)\n");
{
DCBrushStore s;
uint32_t c = add_cube(s, "cube:1");
std::vector<CSG4Vec3> before;
s.brush_vertices_of(c, before);
eqi("a cube has eight corners", (long)before.size(), 8);
// Drag the (1,1,1) corner out along +x.
std::vector<CSG4Vec3> from = {{1,1,1}};
ok("the drag is accepted", s.move_brush_points(c, from, {0.5f, 0, 0}, "alice:1"));
std::vector<CSG4Vec3> after;
s.brush_vertices_of(c, after);
ok("the dragged corner is where it was put", has_point(after, 1.5f, 1, 1));
ok("and it is no longer where it was", !has_point(after, 1, 1, 1));
// THE ASSERTION THAT MATTERS.
const float keep[7][3] = {
{0,0,0},{1,0,0},{1,1,0},{0,1,0},{0,0,1},{1,0,1},{0,1,1},
};
bool all_kept = true;
for (auto& k : keep)
if (!has_point(after, k[0], k[1], k[2])) all_kept = false;
ok("ALL SEVEN other corners are exactly where they were", all_kept);
eqi("and there are still eight corners", (long)after.size(), 8);
}
std::printf("\n-- an edge drag moves two corners and no more --\n");
{
DCBrushStore s;
uint32_t c = add_cube(s, "cube:2");
std::vector<CSG4Vec3> from = {{1,1,1}, {1,1,0}}; // the +x+y edge
ok("the edge drag is accepted", s.move_brush_points(c, from, {0.5f, 0, 0}, "alice:2"));
std::vector<CSG4Vec3> after;
s.brush_vertices_of(c, after);
ok("both endpoints moved", has_point(after, 1.5f, 1, 1) && has_point(after, 1.5f, 1, 0));
const float keep[6][3] = {
{0,0,0},{1,0,0},{0,1,0},{0,0,1},{1,0,1},{0,1,1},
};
bool all_kept = true;
for (auto& k : keep)
if (!has_point(after, k[0], k[1], k[2])) all_kept = false;
ok("the other six are untouched", all_kept);
}
std::printf("\n-- face identity survives where the face does --\n");
{
DCBrushStore s;
uint32_t c = add_cube(s, "cube:3");
// The -x face is nowhere near the corner being dragged, so it is the SAME
// face afterwards and must still answer to its own dot.
const DCBrush* b = s.get(c);
std::string minus_x;
for (size_t i = 0; i < b->planes.size(); i++)
if (b->planes[i].nx < -0.5f) minus_x = b->plane_key[i];
ok("the -x face has a key to begin with", !minus_x.empty());
std::vector<CSG4Vec3> from = {{1,1,1}};
s.move_brush_points(c, from, {0.5f, 0, 0}, "alice:3");
ok("and still resolves after the rebuild", s.face_by_key(c, minus_x) >= 0);
}
std::printf("\n-- a new face gets a TEXTURE, not a blank --\n");
{
DCBrushStore s;
uint32_t c = add_cube(s, "cube:tex");
std::vector<CSG4Vec3> from = {{1,1,1}};
ok("the drag is accepted", s.move_brush_points(c, from, {0.5f, 0.25f, 0}, "alice:tex"));
const DCBrush* b = s.get(c);
bool any_blank = false;
for (uint32_t pj : b->plane_proj) if (pj == 0u) any_blank = true;
// plane_proj indexes the import's projection table; 0 means NO projection,
// which renders as an untextured face. A face born from a corner drag has no
// entry of its own, so it borrows from the old face it most resembles.
ok("NO face was left with projection 0 (the untextured-face bug)", !any_blank);
// And a face that plainly survived keeps its OWN texture, not a borrowed one.
uint32_t minus_x_proj = 0;
for (size_t i = 0; i < b->planes.size(); i++)
if (b->planes[i].nx < -0.9f) minus_x_proj = b->plane_proj[i];
eqi("the untouched -x face kept its own projection", (long)minus_x_proj, 11);
}
std::printf("\n-- refusals: nothing is committed unless the result is a solid --\n");
{
DCBrushStore s;
uint32_t c = add_cube(s, "cube:4");
std::vector<CSG4Vec3> before;
s.brush_vertices_of(c, before);
std::vector<CSG4Vec3> nowhere = {{9, 9, 9}};
ok("a point that is not a corner of this brush is refused",
!s.move_brush_points(c, nowhere, {1, 0, 0}, "alice:4"));
// Collapse the cube onto a plane: no volume left.
std::vector<CSG4Vec3> all_top = {{0,1,0},{1,1,0},{1,1,1},{0,1,1}};
ok("a drag that flattens the solid is refused",
!s.move_brush_points(c, all_top, {0, -1, 0}, "alice:5"));
std::vector<CSG4Vec3> after;
s.brush_vertices_of(c, after);
eqi("and the brush is untouched by either", (long)after.size(), (long)before.size());
ok("still a cube", has_point(after, 1, 1, 1) && has_point(after, 0, 0, 0));
}
std::printf("\n-- deterministic: the same drag on two peers gives the same solid --\n");
{
DCBrushStore a, b;
uint32_t ca = add_cube(a, "cube:5");
uint32_t cb = add_cube(b, "cube:5");
std::vector<CSG4Vec3> from = {{1,1,1}};
a.move_brush_points(ca, from, {0.5f, 0.25f, 0}, "alice:6");
b.move_brush_points(cb, from, {0.5f, 0.25f, 0}, "alice:6");
const DCBrush* pa = a.get(ca);
const DCBrush* pb = b.get(cb);
bool same = pa->planes.size() == pb->planes.size();
if (same)
for (size_t i = 0; i < pa->planes.size(); i++) {
const ConvexPlane& x = pa->planes[i];
const ConvexPlane& y = pb->planes[i];
if (std::fabs(x.nx - y.nx) > 1e-6f || std::fabs(x.ny - y.ny) > 1e-6f ||
std::fabs(x.nz - y.nz) > 1e-6f || std::fabs(x.d - y.d) > 1e-6f)
same = false;
}
ok("identical plane sets, in identical order", same);
bool keys_same = pa->plane_key == pb->plane_key;
ok("and identical face keys — replay names the same faces", keys_same);
}
std::printf("\n%s\n", fails ? "FAILED" : "PASS");
return fails ? 1 : 0;
}