// ============================================================================
// PICKING WITHOUT AN ENGINE — resolve a ray to (brush, face) with no physics.
//
// hyper-games #22 §5 item 3. Picking is engine-coupled today: pick_face needs a
// brush id, and the only way to get one is get_brush_id_from_shape(), which takes
// the index of a Godot collision shape. That makes the editor's most basic query
// depend on the physics server having been fed, stepped and queried — so
// blender-bridge, which has no physics server at all, cannot pick anything.
//
// A brush is an intersection of half-spaces, so the ray query is the standard
// convex slab clip and needs nothing but the plane set:
//
// for each plane (n, d): t = (d - n.o) / (n.dir)
// n.dir < 0 -> the ray is ENTERING that half-space -> t_enter = max(t_enter, t)
// n.dir > 0 -> it is LEAVING -> t_exit = min(t_exit, t)
// hit iff t_enter <= t_exit and t_exit >= 0
//
// and the face is simply the plane that produced t_enter — the one the ray crossed
// last on the way in. Exact, not sampled: no epsilon-marching, no collision mesh,
// and it gives the same answer on a machine with no renderer.
//
// TWO DELIBERATE DIFFERENCES FROM THE PHYSICS PATH, both tested below:
// - SKY BRUSHES ARE PICKABLE. They render and clip but carry no collider, so a
// physics ray passes straight through them and the author cannot select the
// boundary they marked. Here they are ordinary brushes.
// - CARVED BRUSHES ARE STILL HIT AT THEIR HULL. The pocket is a derived voxel
// cache; the BRUSH is the editable thing, and that is what a pick should
// resolve to.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/pick \
// tests/test_pick_raycast.cpp libdc/src/dc_csg4.cpp
// ============================================================================
#include "dc_brush_store.h"
#include "dc_pick.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 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};
int main(){
printf("=== Ray -> (brush, face), with no physics server ===\n\n");
DCBrushStore s;
const uint32_t a = s.add_brush(box(0,0,0,2,2,2),P6,1,0); // x 0..2
const uint32_t b = s.add_brush(box(6,0,0,8,2,2),P6,2,0); // x 6..8
printf("-- a ray hits the nearest brush, on the face it crossed --\n");
dc_pick::Hit h;
ok ("hit",dc_pick::raycast(s,{-5,1,1},{1,0,0},100.0f,h));
eqi("the near brush, not the far one",(long)h.brush,(long)a);
eqi("the -x face",(long)h.face,0);
eqf("at t",h.t,5.0f);
eqf("the point is on the surface",h.point.x,0.0f);
eqf("and the normal is that face's",h.normal.x,-1.0f);
printf("\n-- from the other side it is the other face, same brush --\n");
ok ("hit",dc_pick::raycast(s,{5,1,1},{-1,0,0},100.0f,h));
eqi("still the near brush",(long)h.brush,(long)a);
eqi("the +x face",(long)h.face,1);
eqf("at t",h.t,3.0f);
printf("\n-- all six faces, so nothing is special-cased by axis --\n");
struct Probe { float ox,oy,oz,dx,dy,dz; int face; };
const Probe probes[6] = {
{-5, 1, 1, 1, 0, 0, 0}, { 5, 1, 1, -1, 0, 0, 1},
{ 1,-5, 1, 0, 1, 0, 2}, { 1, 5, 1, 0,-1, 0, 3},
{ 1, 1,-5, 0, 0, 1, 4}, { 1, 1, 5, 0, 0,-1, 5},
};
for (const Probe& p : probes) {
dc_pick::Hit q;
const bool got = dc_pick::raycast(s,{p.ox,p.oy,p.oz},{p.dx,p.dy,p.dz},100.0f,q);
char w[64]; snprintf(w,sizeof(w),"face %d",p.face);
ok (w, got && (int)q.face==p.face && q.brush==a);
}
printf("\n-- misses are misses --\n");
ok("a ray that passes beside it",!dc_pick::raycast(s,{-5,9,9},{1,0,0},100.0f,h));
ok("a ray pointing away",!dc_pick::raycast(s,{-5,1,1},{-1,0,0},100.0f,h));
ok("a ray that stops short (max_t)",!dc_pick::raycast(s,{-5,1,1},{1,0,0},3.0f,h));
ok("a zero direction",!dc_pick::raycast(s,{-5,1,1},{0,0,0},100.0f,h));
printf("\n-- the FAR brush when the near one is not in the way --\n");
ok ("hit",dc_pick::raycast(s,{4,1,1},{1,0,0},100.0f,h));
eqi("brush b",(long)h.brush,(long)b);
eqf("at t",h.t,2.0f);
printf("\n-- a tombstoned brush is not there to be hit --\n");
ok ("tombstone the near brush",s.remove_brush(a));
ok ("hit",dc_pick::raycast(s,{-5,1,1},{1,0,0},100.0f,h));
eqi("the ray reaches the far brush",(long)h.brush,(long)b);
eqf("at t",h.t,11.0f);
ok ("restore",s.restore_brush(a));
printf("\n-- an origin INSIDE a brush reports the face it is looking at --\n");
// A camera can be inside a wall (clipping, a carved pocket, spectating). The
// entry is behind us, so the meaningful answer is the surface ahead: the exit.
ok ("hit",dc_pick::raycast(s,{1,1,1},{1,0,0},100.0f,h));
eqi("the brush we are inside",(long)h.brush,(long)a);
eqi("the face ahead",(long)h.face,1);
ok ("flagged as an inside hit",h.inside);
eqf("at t",h.t,1.0f);
printf("\n-- a DORMANT plane never wins the pick --\n");
// A plane that bounds nothing stays in the set (that is what makes topology
// change reversible), but it is not a surface: picking it would hand the editor
// a face that cannot be seen or dragged. nearest_face already skips these.
DCBrushStore d;
const uint32_t c = d.add_brush(box(0,0,0,2,2,2),P6,1,0);
const uint32_t bev = d.add_face(c,{1,0,0,10.0f},7,"alice:1"); // far away: dormant
ok ("the plane exists",bev!=UINT32_MAX);
ok ("but bounds nothing",!d.face_is_live(c,bev));
dc_pick::Hit e;
ok ("hit",dc_pick::raycast(d,{-5,1,1},{1,0,0},100.0f,e));
eqi("the live -x face, not the dormant plane",(long)e.face,0);
ok ("and the face it reports IS live",d.face_is_live(c,e.face));
printf("\n-- it agrees with nearest_face, which is the physics path's query --\n");
// get_brush_id_from_shape + pick_face is what the engine does today; the second
// half of that is nearest_face on the hit POINT. Given the same point, the two
// must name the same face or the editor would behave differently depending on
// which way the pick was routed.
for (const Probe& p : probes) {
dc_pick::Hit q;
if (!dc_pick::raycast(s,{p.ox,p.oy,p.oz},{p.dx,p.dy,p.dz},100.0f,q)) { fails++; continue; }
const int nf = s.nearest_face(q.brush,q.point.x,q.point.y,q.point.z);
char w[64]; snprintf(w,sizeof(w),"face %d matches nearest_face",p.face);
ok (w, nf==(int)q.face);
}
printf("\n-- a sky brush IS pickable, unlike through the physics path --\n");
DCBrushStore sk;
sk.add_brush(box(0,0,0,2,2,2),P6,3,0,true); // sky: renders, clips, no collider
dc_pick::Hit y;
ok("the author can select the boundary they marked",
dc_pick::raycast(sk,{-5,1,1},{1,0,0},100.0f,y));
printf("\n%s\n",fails?"FAILED":"PASS");
return fails?1:0;
}