// ============================================================================
// CLIP / SPLIT — cut a brush with a plane, keeping both halves.
//
// The gap this closes (docs/trenchbroom-parity.md step 1): add_face can only ever
// SHRINK a convex solid, because a brush is an intersection of half-spaces. So
// there was no way to cut a doorway through a wall — the one operation that stands
// between adjusting a level and authoring one. TrenchBroom has ClipTool for exactly
// this (lib/TbAppLib/include/ui/ClipTool.h, Polyhedron_Clip.h).
//
// On a half-space representation it is almost free, and that is the point:
//
// keep front = planes + P
// keep back = planes + flip(P)
// split = both, as two brushes
//
// No polyhedron rebuild, no plane re-derivation, no matcher to work out which face
// is which afterwards — the two halves are the original plane set plus one plane,
// so every surviving face keeps its own plane AND ITS DOT. TrenchBroom cannot do
// that; it re-derives planes from new vertices and then guesses the correspondence
// (Polyhedron_Matcher.h exists for that guess).
//
// A doorway is then: split, split, tombstone the middle. Three journalled ops, all
// exact, all undoable.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/bsplit \
// tests/test_brush_split_plane.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include <cstdio>
#include <string>
#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 void eqf(const char*w,float g,float v){ bool c=std::fabs(g-v)<1e-3f; if(!c)fails++; printf(" %-58s %-9.4f want %-9.4f %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};
// The solid's extent along x, measured from the faces that actually bound it — the
// only honest way to ask "how wide is this brush now".
static void x_extent(const DCBrushStore&s,uint32_t id,float&lo,float&hi){
lo=1e30f; hi=-1e30f;
std::vector<CSG4Vec3> pts;
const DCBrush*b=s.get(id);
for(uint32_t i=0;i<(uint32_t)b->planes.size();i++){
if(!s.face_winding(id,i,pts)) continue;
for(const CSG4Vec3&p:pts){ if(p.x<lo)lo=p.x; if(p.x>hi)hi=p.x; }
}
}
int main(){
printf("=== Clip / split a brush with a plane ===\n\n");
printf("-- a wall splits into two solids that tile the original --\n");
DCBrushStore s;
const uint32_t wall = s.add_brush(box(0,0,0,10,4,1),P6,3,0); // x 0..10
// Cut at x = 4, front half keeps x <= 4.
const uint32_t back = s.split_brush(wall,{1,0,0,4.0f},"alice:1");
ok ("the split produced a new brush",back!=0);
ok ("named by the dot of the op",s.get(back)->key=="alice:1");
float lo,hi;
x_extent(s,wall,lo,hi);
eqf("the original keeps the FRONT half, lo",lo,0.0f);
eqf("...and ends at the cut",hi,4.0f);
x_extent(s,back,lo,hi);
eqf("the new brush starts at the cut",lo,4.0f);
eqf("...and keeps the rest",hi,10.0f);
printf("\n-- the halves inherit what a half of a wall should inherit --\n");
eqi("material",(long)s.get(back)->material_id,3);
eqi("level",(long)s.get(back)->level_id,0);
ok ("both are live",!s.get(wall)->tombstoned && !s.get(back)->tombstoned);
eqi("each gained exactly one plane",(long)s.get(back)->planes.size(),7);
eqi("and so did the original",(long)s.get(wall)->planes.size(),7);
printf("\n-- SURVIVING FACES KEEP THEIR DOTS (TrenchBroom cannot do this) --\n");
// Their clip rebuilds a polyhedron and re-derives planes, then matches old faces
// to new heuristically. Here nothing is rebuilt, so identity is preserved by
// construction rather than guessed at.
ok ("the original's +y face still answers to its imported dot",
s.face_by_key(wall,"i:3")>=0);
ok ("so does the new brush's",s.face_by_key(back,"i:3")>=0);
ok ("and the cut face carries the split's dot on both halves",
s.face_by_key(wall,"alice:1")>=0 && s.face_by_key(back,"alice:1")>=0);
printf("\n-- a cut that misses the solid is refused, not applied --\n");
DCBrushStore m;
const uint32_t b2 = m.add_brush(box(0,0,0,2,2,2),P6,1,0);
const int before=(int)m.brushes().size();
eqi("a plane entirely outside changes nothing",(long)m.split_brush(b2,{1,0,0,50.0f},"a:1"),0);
eqi("no brush was added",(long)m.brushes().size(),before);
eqi("the original still has its 6 planes",(long)m.get(b2)->planes.size(),6);
eqi("a plane exactly ON a face is also refused",(long)m.split_brush(b2,{1,0,0,2.0f},"a:2"),0);
eqi("still 6",(long)m.get(b2)->planes.size(),6);
printf("\n-- replay is idempotent, like every other create --\n");
const uint32_t again = s.split_brush(wall,{1,0,0,4.0f},"alice:1");
eqi("re-applying the same dot returns the same brush",(long)again,(long)back);
eqi("and adds nothing",(long)s.get(wall)->planes.size(),7);
eqi("nor to the new half",(long)s.get(back)->planes.size(),7);
printf("\n-- A DOORWAY: split, split, tombstone the middle --\n");
DCBrushStore d;
const uint32_t w = d.add_brush(box(0,0,0,10,4,1),P6,3,0);
const uint32_t mid = d.split_brush(w,{1,0,0,4.0f},"bob:1"); // w = 0..4, mid = 4..10
const uint32_t right = d.split_brush(mid,{1,0,0,6.0f},"bob:2"); // mid = 4..6, right = 6..10
ok ("two cuts, three pieces",mid!=0 && right!=0);
x_extent(d,mid,lo,hi);
eqf("the middle piece starts at the first cut",lo,4.0f);
eqf("and ends at the second",hi,6.0f);
ok ("tombstone it",d.remove_brush(mid));
std::vector<uint32_t> clean;
d.clean_brushes(clean);
eqi("two solids remain, with a hole between them",(long)clean.size(),2);
ok ("and the hole is reversible — nothing was destroyed",d.restore_brush(mid));
d.clean_brushes(clean);
eqi("three again",(long)clean.size(),3);
printf("\n-- THE PREVIEW is the face the cut would actually make --\n");
// Built from the candidate half rather than by intersecting the plane with the hull
// separately: two ways of computing the same polygon would drift, and the one that
// matters is the one the split will produce.
DCBrushStore pv;
const uint32_t pb = pv.add_brush(box(0,0,0,10,4,1),P6,3,0);
std::vector<CSG4Vec3> poly;
ok ("a cut through the middle previews",pv.cut_preview(pb,{1,0,0,4.0f},poly));
eqi("the preview is a quad",(long)poly.size(),4);
// The wall is y 0..4, z 0..1, so the cut face is 4 x 1 at x = 4.
for(const CSG4Vec3&q:poly) eqf("every corner is on the cut plane",q.x,4.0f);
const long previewed = (long)poly.size(); // the negative case below CLEARS poly
ok ("a plane that misses previews NOTHING — it is not an available cut",
!pv.cut_preview(pb,{1,0,0,50.0f},poly));
ok ("and the preview left the store untouched",pv.get(pb)->planes.size()==6);
// What you previewed is what you get.
ok ("commit it",pv.split_brush(pb,{1,0,0,4.0f},"alice:9")!=0);
const uint32_t nb=pv.brush_by_key("alice:9");
const int cf=pv.face_by_key(nb,"alice:9");
std::vector<CSG4Vec3> actual;
ok ("the new face exists",cf>=0 && pv.face_winding(nb,(uint32_t)cf,actual));
eqi("and it has the same corner count as the preview",(long)actual.size(),previewed);
printf("\n%s\n",fails?"FAILED":"PASS");
return fails?1:0;
}