// ============================================================================
// CSG4 PORT GATE — does libdc/src/dc_csg4.cpp reproduce the oracle?
//
// tests/test_csg4_polycount.cpp is the reference implementation, validated on
// the real 1164-brush START.MAP. This gate runs the LIBRARY port over the same
// dump and asserts it produces the same answer. If it diverges, the port is
// wrong — the oracle is not.
//
// ACCEPTANCE (docs/level-mesh-implementation.md §6.2, §12 step 1):
// 6487 faces · 12747 triangles · 32101 m² · 2593 removed · 837 fragmented
// mean AABB overlap 6.9 · max 53
//
// Also exercises the INCREMENTAL path (dc_csg4::clip_brush), which is what makes
// a brush edit local (docs §6.3). A brush re-clipped on its own must produce
// exactly the faces the full pass produced for it — otherwise interactive edits
// would silently drift from the baked mesh.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/csg4_gate \
// tests/test_csg4_gate.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_csg4.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
// ---------------------------------------------------------------------------
// Dump loader (format written by hyper-tps/scenes/test/oracle_dump.gd)
// ---------------------------------------------------------------------------
struct LoadedBrush {
std::vector<ConvexPlane> planes;
std::vector<uint32_t> proj;
uint32_t material = 0;
};
static bool load_dump(const char* path, std::vector<LoadedBrush>& out, float& ws) {
FILE* f = fopen(path, "rb");
if (!f) { fprintf(stderr, "cannot open %s\n", path); return false; }
char m[4]; size_t r = fread(m, 1, 4, f); (void)r;
if (memcmp(m, "DCOR", 4) != 0) { fprintf(stderr, "bad magic\n"); fclose(f); return false; }
unsigned ver; r = fread(&ver, 4, 1, f);
r = fread(&ws, 4, 1, f);
float carve[6]; r = fread(carve, 4, 6, f);
unsigned n; r = fread(&n, 4, 1, f);
out.reserve(n);
for (unsigned i = 0; i < n; i++) {
LoadedBrush b;
float c[3]; r = fread(c, 4, 3, f);
float radius, strength; r = fread(&radius, 4, 1, f); r = fread(&strength, 4, 1, f);
r = fread(&b.material, 4, 1, f);
unsigned pc; r = fread(&pc, 4, 1, f);
b.planes.reserve(pc); b.proj.reserve(pc);
for (unsigned p = 0; p < pc; p++) {
float v[4]; unsigned pr;
r = fread(v, 4, 4, f); r = fread(&pr, 4, 1, f);
b.planes.push_back({v[0], v[1], v[2], v[3]});
b.proj.push_back(pr);
}
out.push_back(std::move(b));
}
fclose(f);
return true;
}
// ---------------------------------------------------------------------------
static int g_fail = 0;
static void check_u(const char* label, uint32_t got, uint32_t want) {
const bool ok = (got == want);
if (!ok) g_fail++;
printf(" %-28s %-10u expected %-10u %s\n", label, got, want, ok ? "OK" : "**FAIL**");
}
static void check_f(const char* label, double got, double want, double tol) {
const bool ok = std::fabs(got - want) <= tol;
if (!ok) g_fail++;
printf(" %-28s %-10.1f expected %-10.1f %s\n", label, got, want, ok ? "OK" : "**FAIL**");
}
int main(int argc, char** argv) {
const char* dump = (argc > 1) ? argv[1] : "/tmp/startmap_brushes.bin";
std::vector<LoadedBrush> loaded;
float ws = 0.0f;
if (!load_dump(dump, loaded, ws)) return 2;
std::vector<CSG4Brush> brushes;
brushes.reserve(loaded.size());
for (uint32_t i = 0; i < (uint32_t)loaded.size(); i++) {
CSG4Brush b;
b.planes = loaded[i].planes.data();
b.plane_count = (uint32_t)loaded[i].planes.size();
b.plane_proj = loaded[i].proj.data();
b.material_id = loaded[i].material;
b.id = i;
brushes.push_back(b);
}
dc_csg4::compute_bounds(brushes);
printf("CSG4 PORT GATE — libdc/src/dc_csg4.cpp vs tests/test_csg4_polycount.cpp\n");
printf("brushes=%zu world_scale=%.4f\n\n", brushes.size(), ws);
std::vector<CSG4Face> faces;
CSG4Stats st;
dc_csg4::clip_all(brushes, faces, &st);
printf("FULL PASS\n");
check_u("raw faces", st.raw_faces, 6930);
check_u("raw triangles", st.raw_tris, 13718);
check_u("clipped faces", st.out_faces, 6487);
check_u("clipped triangles",st.out_tris, 12747);
check_u("faces removed", st.removed, 2593);
check_u("faces fragmented", st.fragmented, 837);
check_u("max overlap", st.max_overlap, 53);
check_f("raw area (m2)", st.raw_area, 48338.7, 1.0);
check_f("clipped area (m2)",st.out_area, 32100.8, 1.0);
check_f("mean overlap", st.mean_overlap, 6.9, 0.05);
printf(" %-28s %zu\n", "faces emitted (vector)", faces.size());
if (faces.size() != st.out_faces) {
printf(" **FAIL** emitted face count disagrees with stats\n");
g_fail++;
}
// ---- provenance: every face must carry a usable brush/face/material ----
{
uint32_t bad = 0;
for (const CSG4Face& f : faces) {
if (f.brush_id >= brushes.size()) { bad++; continue; }
if (f.face_index >= brushes[f.brush_id].plane_count) { bad++; continue; }
if (f.pts.size() < 3) bad++;
}
printf("\nPROVENANCE\n");
check_u("faces with bad provenance", bad, 0);
}
// ---- incremental path must agree with the full pass ----
// A brush re-clipped alone must yield exactly the faces the full pass gave
// it; otherwise an interactive edit would drift from the baked mesh.
{
printf("\nINCREMENTAL (dc_csg4::clip_brush) vs FULL PASS\n");
const uint32_t probe[] = {0, 1, 7, 42, 100, 523, 900, 1163};
uint32_t mismatches = 0, checked = 0;
for (uint32_t bi : probe) {
if (bi >= brushes.size()) continue;
std::vector<CSG4Face> inc;
dc_csg4::clip_brush(brushes, bi, inc);
uint32_t full_faces = 0, full_tris = 0, inc_tris = 0;
for (const CSG4Face& f : faces)
if (f.brush_id == bi) { full_faces++; full_tris += dc_csg4::face_tri_count(f); }
for (const CSG4Face& f : inc) inc_tris += dc_csg4::face_tri_count(f);
const bool ok = (inc.size() == full_faces) && (inc_tris == full_tris);
if (!ok) { mismatches++; g_fail++; }
printf(" brush %-5u incr=%-4zu/%-5u full=%-4u/%-5u %s\n",
bi, inc.size(), inc_tris, full_faces, full_tris, ok ? "OK" : "**FAIL**");
checked++;
}
printf(" %u brushes checked, %u mismatches\n", checked, mismatches);
}
printf("\n%s\n", g_fail == 0 ? "GATE PASSED" : "GATE FAILED");
return g_fail == 0 ? 0 : 1;
}