OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_brush_edges.cpp

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