// ============================================================================
// CLEARANCE BRUSHES — does the shell enclose air the level owns?
//
// hyper-games #25 §2. dc_level_shell floods EXTERIOR in from the grid border and
// emits the boundary between what it reached and what it did not. That is right
// for a sealed room and wrong for anything open: the bore of an archway driven
// into a hillside is reachable from outside, so it is marked EXTERIOR, no terrain
// is subtracted from it, and the tunnel is full of dirt. The ring renders; you
// cannot walk through it.
//
// A CLEARANCE brush is the fix — a solid that exists only to stop that flood, so
// the sealed volume extends through the passage. It must do exactly three things
// and no others:
//
// 1. BLOCK THE FILL, so the bore ends up inside the shell.
// 2. NOT COUNT AS WALL. sealed() is interior_volume > solid_volume * 0.5
// (dc_level_shell.h:105). Clearance is air, so it belongs in interior; count
// it as solid and a big enough passage tips a good level into "not sealed"
// and drops the map to the moat fallback.
// 3. CHANGE NOTHING ELSE. Closure, and the sign everywhere the level already
// answered for, are unaffected.
//
// SYNTHETIC ON PURPOSE — no START.MAP dump, no Godot. The property is about the
// flood fill, not about any particular level, and a test that needs a 1183-brush
// binary from a headless export is one nobody runs.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/clearance_shell \
// tests/test_clearance_shell.cpp libdc/src/dc_csg4.cpp \
// libdc/src/dc_level_shell.cpp libdc/src/generators/dc_generator_mesh_sdf.cpp
// ============================================================================
#include "dc_level_shell.h"
#include "dc_generator_mesh_sdf.h"
#include <cmath>
#include <cstdio>
#include <vector>
// ---- the scene -------------------------------------------------------------
//
// A sealed room, 7 x 5 x 7 outside and 6 x 4 x 6 of air inside, walls 0.5 thick
// — the same order of magnitude as a Quake wall at world_scale (0.448 m), which
// is the thickness the conservative rasterization exists for.
//
// Then a tunnel: a prism leaving the +X wall's outer face and running 6 m out
// into what would be hillside. Nothing in the shell knows about terrain, so the
// hill is not modelled; what is being tested is whether the shell ENCLOSES that
// prism, because enclosing it is what makes the subtract remove the hill.
struct Box {
float lo[3], hi[3];
};
static const Box ROOM_WALLS[6] = {
{{0.0f, 0.0f, 0.0f}, {7.0f, 0.5f, 7.0f}}, // floor
{{0.0f, 4.5f, 0.0f}, {7.0f, 5.0f, 7.0f}}, // ceiling
{{0.0f, 0.5f, 0.0f}, {0.5f, 4.5f, 7.0f}}, // -X
{{6.5f, 0.5f, 0.0f}, {7.0f, 4.5f, 7.0f}}, // +X
{{0.5f, 0.5f, 0.0f}, {6.5f, 4.5f, 0.5f}}, // -Z
{{0.5f, 0.5f, 6.5f}, {6.5f, 4.5f, 7.0f}}, // +Z
};
// Abuts the +X wall's outer face at x = 7 and runs out to x = 13.
static const Box TUNNEL = {{7.0f, 0.5f, 3.0f}, {13.0f, 2.5f, 4.0f}};
static const float TUNNEL_VOLUME = 6.0f * 2.0f * 1.0f; // 12 m^3
// A point in the middle of the bore. THE point: outside the level's own volume,
// inside the passage, and the whole question is which of those the shell agrees
// with.
static const float MID_TUNNEL[3] = {10.0f, 1.5f, 3.5f};
// Directly above the tunnel, in open air the level has no claim on. Guards
// against a "fix" that simply grows the sealed volume.
static const float ABOVE_TUNNEL[3] = {10.0f, 6.0f, 3.5f};
// Inside the floor slab — solid the shell has always enclosed.
static const float IN_FLOOR[3] = {3.5f, 0.25f, 3.5f};
static void box_planes(const Box& b, std::vector<ConvexPlane>& out) {
out.clear();
out.push_back({-1.0f, 0.0f, 0.0f, -b.lo[0]});
out.push_back({ 1.0f, 0.0f, 0.0f, b.hi[0]});
out.push_back({ 0.0f,-1.0f, 0.0f, -b.lo[1]});
out.push_back({ 0.0f, 1.0f, 0.0f, b.hi[1]});
out.push_back({ 0.0f, 0.0f,-1.0f, -b.lo[2]});
out.push_back({ 0.0f, 0.0f, 1.0f, b.hi[2]});
}
// Storage the CSG4Brush views point into — they are non-owning, so this has to
// outlive every build below.
struct Scene {
std::vector<std::vector<ConvexPlane>> planes;
std::vector<CSG4Brush> brushes;
};
// `tunnel`: 0 = absent, 1 = present as CLEARANCE, 2 = present as ordinary solid.
static void build_scene(Scene& s, int tunnel) {
s.planes.clear();
s.brushes.clear();
const int n = 6 + (tunnel ? 1 : 0);
s.planes.resize(n);
for (int i = 0; i < 6; i++) box_planes(ROOM_WALLS[i], s.planes[i]);
if (tunnel) box_planes(TUNNEL, s.planes[6]);
s.brushes.resize(n);
for (int i = 0; i < n; i++) {
s.brushes[i].planes = s.planes[i].data();
s.brushes[i].plane_count = (uint32_t)s.planes[i].size();
s.brushes[i].id = (uint32_t)i;
s.brushes[i].clearance = (i == 6 && tunnel == 1);
}
dc_csg4::compute_bounds(s.brushes);
}
// ---- the sign test ---------------------------------------------------------
//
// Through DCGeneratorMeshSDF, because that is what actually consumes the shell:
// stamp_mesh_arrays hands these triangles to the same class. A bespoke parity
// test here could disagree with the consumer and we would not find out.
// Convention (dc_generator_mesh_sdf.h): positive = inside.
static bool sdf_inside(const dc_level_shell::ShellMesh& sh, const float p[3],
const char* obj_path) {
FILE* o = fopen(obj_path, "w");
for (int i = 0; i < sh.vertex_count(); i++)
fprintf(o, "v %.6f %.6f %.6f\n",
sh.vertices[i*3], sh.vertices[i*3+1], sh.vertices[i*3+2]);
for (size_t i = 0; i + 2 < sh.indices.size(); i += 3)
fprintf(o, "f %d %d %d\n", sh.indices[i]+1, sh.indices[i+1]+1, sh.indices[i+2]+1);
fclose(o);
DCGeneratorMeshSDF sdf;
if (!sdf.load_obj(obj_path)) { printf(" (could not load %s)\n", obj_path); return false; }
return sdf.evaluate(p[0], p[1], p[2], dc_level_shell::DEFAULT_CELL) > 0.0f;
}
static int failures = 0;
static void check(const char* label, bool ok, const char* detail) {
if (!ok) failures++;
printf(" %-44s %s %s\n", label, ok ? "OK " : "FAIL", detail);
}
int main() {
char buf[160];
// ---- CONTROL: no tunnel at all --------------------------------------
// Establishes what the room alone answers, so every later number has
// something to be a difference from.
Scene plain;
build_scene(plain, 0);
dc_level_shell::ShellMesh sh_plain;
if (!dc_level_shell::build(plain.brushes, sh_plain)) {
printf("FAIL — build() returned false on the plain room\n");
return 1;
}
printf("plain room: solid %.1f m^3 interior %.1f m^3 tris %d boundary_edges %lld sealed=%s\n",
sh_plain.solid_volume, sh_plain.interior_volume, sh_plain.triangle_count(),
sh_plain.boundary_edges, sh_plain.sealed() ? "yes" : "NO");
check("plain room is closed", sh_plain.boundary_edges == 0, "0 boundary edges expected");
check("plain room is sealed", sh_plain.sealed(), "air > wall");
const bool plain_mid = sdf_inside(sh_plain, MID_TUNNEL, "/tmp/clearance_plain.obj");
// THE BUG, stated as an assertion. With no clearance brush the bore is open
// to the void, the flood reaches it, and the terrain keeps its hill. This
// must read OUTSIDE — if it does not, the rest of the test proves nothing.
check("without clearance, the bore reads OUTSIDE", !plain_mid,
plain_mid ? "already inside — the premise is wrong" : "as expected");
// ---- CLEARANCE: the same prism, flagged ------------------------------
Scene clear_;
build_scene(clear_, 1);
dc_level_shell::ShellMesh sh_clear;
if (!dc_level_shell::build(clear_.brushes, sh_clear)) {
printf("FAIL — build() returned false with a clearance brush\n");
return 1;
}
printf("with clearance: solid %.1f m^3 interior %.1f m^3 tris %d boundary_edges %lld sealed=%s\n",
sh_clear.solid_volume, sh_clear.interior_volume, sh_clear.triangle_count(),
sh_clear.boundary_edges, sh_clear.sealed() ? "yes" : "NO");
// 1. BLOCK THE FILL.
const bool clear_mid = sdf_inside(sh_clear, MID_TUNNEL, "/tmp/clearance_tunnel.obj");
check("the bore reads INSIDE the shell", clear_mid,
clear_mid ? "terrain will be subtracted from it" : "still exterior — the flood got in");
// 2. NOT COUNT AS WALL. Conservative rasterization rounds outward, so the
// interior gain is compared with a cell of slack rather than exactly.
const double solid_delta = sh_clear.solid_volume - sh_plain.solid_volume;
const double air_delta = sh_clear.interior_volume - sh_plain.interior_volume;
snprintf(buf, sizeof(buf), "%+.2f m^3", solid_delta);
check("clearance adds NO wall volume", std::fabs(solid_delta) < 1e-6, buf);
snprintf(buf, sizeof(buf), "%+.1f m^3 vs %.0f expected", air_delta, (double)TUNNEL_VOLUME);
check("clearance adds its volume as AIR",
air_delta > TUNNEL_VOLUME * 0.9 && air_delta < TUNNEL_VOLUME * 1.6, buf);
// 3. CHANGE NOTHING ELSE.
check("still closed", sh_clear.boundary_edges == 0, "0 boundary edges expected");
check("still manifold", sh_clear.nonmanifold_edges == 0, "0 non-manifold edges expected");
check("still sealed", sh_clear.sealed(), "air > wall");
const bool clear_floor = sdf_inside(sh_clear, IN_FLOOR, "/tmp/clearance_floor.obj");
check("a point in the floor still reads INSIDE", clear_floor, "unchanged by clearance");
const bool clear_above = sdf_inside(sh_clear, ABOVE_TUNNEL, "/tmp/clearance_above.obj");
check("open air above the bore reads OUTSIDE", !clear_above,
clear_above ? "the sealed volume grew past the passage" : "bounded");
// ---- DISCRIMINATING CONTROL: the same prism, NOT flagged -------------
// Everything above is also true of an ordinary solid brush placed there — it
// blocks the fill too. What separates them is the accounting: a solid is
// wall, clearance is air. Without this, `clearance` could be ignored
// entirely and the suite would still pass.
Scene solid;
build_scene(solid, 2);
dc_level_shell::ShellMesh sh_solid;
if (!dc_level_shell::build(solid.brushes, sh_solid)) {
printf("FAIL — build() returned false with a solid tunnel brush\n");
return 1;
}
const double solid_wall = sh_solid.solid_volume - sh_plain.solid_volume;
snprintf(buf, sizeof(buf), "solid +%.1f wall vs clearance +%.1f", solid_wall, solid_delta);
check("an UNFLAGGED prism counts as wall instead", solid_wall > TUNNEL_VOLUME * 0.9, buf);
printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "PASSED",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}