// ============================================================================
// BRUSH EDGES — the wireframe that makes the structure of a level visible.
//
// The reported bug was "C does nothing, Delete works". Delete removes geometry you
// can SEE; a split produces two brushes that tile the original exactly, so the
// rendered mesh is byte-identical and nothing visible changes. The cut was fine and
// there was no way to know.
//
// The underlying gap is bigger than the cut: brush boundaries have never been drawn
// anywhere. The level renders as one merged surface, so the structure being edited
// is invisible — which brush is which, where one ends and the next begins, whether
// the thing under the crosshair is one wall or three.
//
// The data already exists. Every face has an exact polygon (face_winding), so the
// wireframe is those polygons as line loops. This lives in the STORE rather than in
// the Godot node so it can be tested from a bare main(), and so blender-bridge gets
// it too.
//
// The assertion that matters is the last one: a split CHANGES THE EDGE SET even
// though it does not change the rendered surface. That is the difference between an
// editor you can use and one you cannot.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/bedges \
// tests/test_brush_edges.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include <cstdio>
#include <cmath>
#include <vector>
static int fails=0;
static void ok(const char*w,bool c){ if(!c)fails++; printf(" %-58s %s\n",w,c?"OK":"**FAIL**"); }
static void eqi(const char*w,long g,long v){ bool c=g==v; if(!c)fails++; printf(" %-58s %-6ld want %-6ld %s\n",w,g,v,c?"OK":"**FAIL**"); }
static std::vector<ConvexPlane> box(float a,float b,float c,float d,float e,float f){
return {{-1,0,0,-a},{1,0,0,d},{0,-1,0,-b},{0,1,0,e},{0,0,-1,-c},{0,0,1,f}};
}
static const std::vector<uint32_t> P6{0,0,0,0,0,0};
// Is this point one of the segment endpoints anywhere in the soup?
static bool has_point(const std::vector<CSG4Vec3>&v,float x,float y,float z){
for(const CSG4Vec3&p:v)
if(std::fabs(p.x-x)<1e-3f&&std::fabs(p.y-y)<1e-3f&&std::fabs(p.z-z)<1e-3f) return true;
return false;
}
int main(){
printf("=== Brush edges (the wireframe) ===\n\n");
printf("-- a cube is twelve edges, as line pairs --\n");
DCBrushStore s;
const uint32_t cube = s.add_brush(box(0,0,0,2,2,2),P6,1,0);
std::vector<CSG4Vec3> e;
s.brush_edges_of(cube,e);
ok ("something came back",!e.empty());
eqi("an even number of points — they are PAIRS",(long)(e.size()%2),0);
// Six quad faces, each emitted as a closed loop of 4 segments = 24 segments.
// Every edge is shared by two faces, so each is emitted twice: that is 12 distinct
// edges drawn twice, which costs nothing visually and saves a dedup pass that
// would cost more CPU than the duplicate vertices do.
eqi("24 segments (12 edges, each shared by two faces)",(long)(e.size()/2),24);
ok ("a corner of the cube is among them",has_point(e,0,0,0));
ok ("and the opposite corner",has_point(e,2,2,2));
ok ("but not a point outside it",!has_point(e,9,9,9));
printf("\n-- the whole level, live brushes only --\n");
const uint32_t second = s.add_brush(box(8,0,0,10,2,2),P6,2,0);
std::vector<CSG4Vec3> all;
s.brush_edges(all);
eqi("two cubes, twice the segments",(long)(all.size()/2),48);
ok ("tombstone one",s.remove_brush(second));
s.brush_edges(all);
eqi("a deleted brush is not drawn",(long)(all.size()/2),24);
ok ("restore",s.restore_brush(second));
printf("\n-- A DORMANT PLANE IS NOT AN EDGE --\n");
// A plane that bounds nothing stays in the set (that is what makes topology change
// reversible), but drawing it would show the player an edge that is not there.
DCBrushStore d;
const uint32_t b = d.add_brush(box(0,0,0,2,2,2),P6,1,0);
d.brush_edges_of(b,e);
const size_t before = e.size();
d.add_face(b,{1,0,0,10.0f},7,"alice:1"); // far away: dormant
ok ("the plane is there",d.get(b)->planes.size()==7);
ok ("but bounds nothing",!d.face_is_live(b,6));
d.brush_edges_of(b,e);
eqi("the wireframe is unchanged",(long)e.size(),(long)before);
printf("\n-- THE POINT: a split changes the EDGES but not the surface --\n");
// This is the whole reason the feature exists. The rendered mesh after a cut is
// byte-identical — two halves tile the original — so without a wireframe there is
// no feedback at all that the cut happened.
DCBrushStore w;
const uint32_t wall = w.add_brush(box(0,0,0,10,4,1),P6,3,0);
std::vector<CSG4Vec3> before_cut;
w.brush_edges(before_cut);
const uint64_t fp_before = w.fingerprint();
ok ("cut it",w.split_brush(wall,{1,0,0,4.0f},"alice:1")!=0);
std::vector<CSG4Vec3> after_cut;
w.brush_edges(after_cut);
ok ("the wireframe gained edges — the cut is now VISIBLE",
after_cut.size() > before_cut.size());
ok ("there is now a line at the cut plane",has_point(after_cut,4,0,0));
ok ("...which was not there before",!has_point(before_cut,4,0,0));
ok ("and the brush set really did change",w.fingerprint()!=fp_before);
printf("\n-- VERTICES: the distinct corners, not every winding point --\n");
// Handles to aim at. Each corner appears in three face windings on a cube, so the
// dedup is the whole job — 24 winding points, 8 vertices. Doing it in the store
// rather than in the UI means blender-bridge gets handles too, and it is one place
// to be careful about epsilon rather than one per consumer.
DCBrushStore v;
const uint32_t vc = v.add_brush(box(0,0,0,2,2,2),P6,1,0);
std::vector<CSG4Vec3> verts;
v.brush_vertices_of(vc,verts);
eqi("a cube has eight",(long)verts.size(),8);
ok ("including the origin corner",has_point(verts,0,0,0));
ok ("and the far one",has_point(verts,2,2,2));
ok ("and nothing spurious",!has_point(verts,1,1,1));
// A bevel adds a face and turns one corner into three.
ok ("bevel it",v.add_face(vc,{0.5774f,0.5774f,0.5774f,2.6f},7,"a:1")!=UINT32_MAX);
v.brush_vertices_of(vc,verts);
eqi("ten now",(long)verts.size(),10);
ok ("the cut corner is gone",!has_point(verts,2,2,2));
printf("\n%s\n",fails?"FAILED":"PASS");
return fails?1:0;
}