OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_brush_hull.cpp

234 lines · cpp ref:
// ============================================================================
// 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;
}