#ifndef DC_BRUSH_SPLIT_H
#define DC_BRUSH_SPLIT_H
// ---------------------------------------------------------------------------
// dc_brush_split — peel a brush around a carve's AABB so the voxel pocket is
// bounded by the EDIT instead of by the BRUSH.
//
// THE PROBLEM, measured in-game (hyper-tps 0.823.0): a 0.6 m sphere carve
// produced 34,520 then 45,384 pocket triangles, because dc_pocket voxelized the
// whole connected component. Carve a 20 m wall and you voxelize 20 m of wall to
// show a 1 m dent. Pocket cost tracked the brush; it should track the edit.
//
// THE ENABLING PROPERTY. A brush is not a mesh, it is a SET OF HALF-SPACES, so
// splitting one by a plane costs nothing:
//
// brush AND halfspace(P) == brush.planes + P <- still a brush
// brush AND halfspace(-P) == brush.planes + (-P) <- also still a brush
//
// No boolean, no clipping, no geometry. So the carve's AABB peels the brush by
// its six planes, one slab at a time:
//
// remainder = brush
// for each AABB plane P:
// piece = remainder + outward(P) -> EXACT, stays a brush
// remainder = remainder + inward(P)
// remainder == brush AND aabb -> the ONLY part that voxelizes
//
// At most 6 pieces, each with at most plane_count+6 planes. Pieces keep sharp
// edges, real texture projections and convex colliders — everything voxelizing
// would have thrown away.
//
// DERIVED, NOT DURABLE. This never touches DCBrushStore's journal. The store
// keeps brush + carve exactly as before and this runs in the derive step beside
// the pocket, so undo still drops the carve and returns the ORIGINAL brush
// bit-identical (tests/test_pocket.cpp PART 3 is unaffected). There is still no
// model->brush conversion and nothing re-fits planes.
//
// INTERNAL FACES. The six added planes create faces that are interior to the
// union: solid on both sides, because the brush continues into the AABB where
// the pocket takes over. csg4 cannot cancel them — it only cancels a face
// against another BRUSH, and the pocket is not one — so they are flagged here
// via plane_internal and the renderer must skip them. Unflagged, they would
// draw as untextured surfaces sitting inside the wall.
//
// Engine-free (libdc rule); gated by tests/test_brush_split.cpp.
// ---------------------------------------------------------------------------
#include <cstdint>
#include <vector>
#include <algorithm>
#include "dc_edit.h" // ConvexPlane
#include "dc_csg4.h" // CSG4Vec3, CSG4Brush, face_winding, compute_bounds
#include "dc_brush_store.h" // DCBrush
namespace dc_brush_split {
struct Piece {
std::vector<ConvexPlane> planes;
std::vector<uint32_t> plane_proj; // parallel to planes
// Parallel to planes. 1 = a cut face introduced by the split, interior to
// the union and therefore NOT to be rendered. 0 = an original brush face.
std::vector<uint8_t> plane_internal;
uint32_t material_id = 0;
uint32_t brush_id = 0; // provenance: the brush this came from
bool sky = false;
// Set on the remainder only: clips, draws nothing, and gets NO COLLIDER.
// Without the collider half, the carved volume stays solid to the player.
bool clip_only = false;
};
struct SplitResult {
std::vector<Piece> pieces; // exact geometry OUTSIDE the pocket
// brush AND aabb — the pocket's region, as a brush. CLIP-ONLY: every plane is
// flagged internal so it emits no face, and it must NOT get a collider (a
// convex shape here would fill the hole you just carved).
//
// It exists because replacing a brush with its pieces would otherwise LOSE
// hidden-face removal inside the pocket: the pieces cover only brush-minus-
// AABB, so a neighbouring brush's face buried in the AABB portion is suddenly
// clipped by nothing and becomes visible through the carve hole — the one
// place the player is looking. Feeding the remainder back as a clip-only
// solid restores exactly what the un-split brush used to do.
Piece remainder;
bool has_remainder = false;
CSG4Vec3 pocket_lo{0,0,0}, pocket_hi{0,0,0};
bool pocket_empty = false; // the AABB missed the brush entirely
};
// Is this plane set a non-empty solid? Asked via the winding builder rather than
// a linear-programming feasibility test, because that is the SAME predicate csg4
// and compute_bounds use — a piece this call accepts but csg4 later finds
// degenerate would be a piece that renders as nothing.
inline bool is_non_empty(const std::vector<ConvexPlane>& planes,
const std::vector<uint32_t>& proj) {
if (planes.size() < 4) return false; // <4 half-spaces cannot bound a solid
CSG4Brush v;
v.planes = planes.data();
v.plane_count = (uint32_t)planes.size();
v.plane_proj = proj.empty() ? nullptr : proj.data();
dc_csg4::compute_bounds(v);
if (!v.bounds_valid) return false;
// compute_bounds succeeds on a degenerate slab (a plane touching a corner
// yields one point). Require real extent on every axis, or the split emits
// zero-volume pieces that cost a collider and draw nothing.
const float ex = v.hi.x - v.lo.x, ey = v.hi.y - v.lo.y, ez = v.hi.z - v.lo.z;
return ex > 1e-5f && ey > 1e-5f && ez > 1e-5f;
}
// Peel `b` around [lo,hi]. Returns false only when the brush itself is unusable;
// an AABB that swallows the brush is a SUCCESS with zero pieces, because that is
// the legitimate "the whole brush voxelizes" case the old behaviour always took.
inline bool split_by_aabb(const DCBrush& b, const CSG4Vec3& lo, const CSG4Vec3& hi,
SplitResult& out) {
out.pieces.clear();
out.pocket_empty = false;
if (b.planes.size() < 4) return false;
// CLAMP the pocket to the brush before peeling. A carve larger than the
// brush would otherwise leave the pocket the size of the CARVE, and the
// sampler would spend its whole grid on empty space outside the solid —
// the exact cost this module exists to remove, reintroduced at the other
// end. Clamping cannot change the split geometry (a cut outside the brush
// yields an empty piece and an unchanged remainder), only how much volume
// the pocket has to sample.
CSG4Vec3 clo = lo, chi = hi;
if (b.bounds_valid) {
clo.x = std::max(clo.x, b.lo.x); chi.x = std::min(chi.x, b.hi.x);
clo.y = std::max(clo.y, b.lo.y); chi.y = std::min(chi.y, b.hi.y);
clo.z = std::max(clo.z, b.lo.z); chi.z = std::min(chi.z, b.hi.z);
}
out.pocket_lo = clo;
out.pocket_hi = chi;
// The carve missed this brush entirely: one piece, the whole brush, no
// pocket. Returning early keeps that case from generating six empty cuts.
if (clo.x > chi.x || clo.y > chi.y || clo.z > chi.z) {
Piece p;
p.planes = b.planes;
p.plane_proj = b.plane_proj;
p.plane_proj.resize(p.planes.size(), 0u);
p.plane_internal.assign(p.planes.size(), 0u);
p.material_id = b.material_id;
p.brush_id = b.id;
p.sky = b.sky;
out.pieces.push_back(std::move(p));
out.pocket_empty = true;
return true;
}
// The six AABB planes, expressed the way a brush wants them: inside is
// n.p <= d. `inward` keeps the side TOWARDS the box; `outward` is its
// negation and keeps the slab outside it.
struct Cut { ConvexPlane inward, outward; };
const Cut cuts[6] = {
{{-1, 0, 0, -clo.x}, { 1, 0, 0, clo.x}},
{{ 1, 0, 0, chi.x}, {-1, 0, 0, -chi.x}},
{{ 0,-1, 0, -clo.y}, { 0, 1, 0, clo.y}},
{{ 0, 1, 0, chi.y}, { 0,-1, 0, -chi.y}},
{{ 0, 0,-1, -clo.z}, { 0, 0, 1, clo.z}},
{{ 0, 0, 1, chi.z}, { 0, 0,-1, -chi.z}},
};
std::vector<ConvexPlane> rem = b.planes;
std::vector<uint32_t> rem_proj = b.plane_proj;
std::vector<uint8_t> rem_internal(b.planes.size(), 0u);
rem_proj.resize(rem.size(), 0u);
for (int i = 0; i < 6; i++) {
// The slab outside this AABB face, still bounded by everything peeled so
// far — which is what makes the pieces disjoint by construction rather
// than by a later overlap test.
Piece p;
p.planes = rem;
p.plane_proj = rem_proj;
p.plane_internal = rem_internal;
p.planes.push_back(cuts[i].outward);
p.plane_proj.push_back(0u);
p.plane_internal.push_back(1u); // a cut face: interior, do not draw
p.material_id = b.material_id;
p.brush_id = b.id;
p.sky = b.sky;
if (is_non_empty(p.planes, p.plane_proj)) out.pieces.push_back(std::move(p));
// ...and keep peeling the inside.
rem.push_back(cuts[i].inward);
rem_proj.push_back(0u);
rem_internal.push_back(1u);
}
// What survives all six cuts is brush AND aabb: the voxel pocket's region.
// If that is empty the carve never actually reached this brush.
out.pocket_empty = !is_non_empty(rem, rem_proj);
if (!out.pocket_empty) {
out.remainder.planes = rem;
out.remainder.plane_proj = rem_proj;
// ALL internal: it is a real solid for clipping purposes and draws
// nothing. The original brush faces are internal here too — the pocket's
// DC surface is what renders that geometry now.
out.remainder.plane_internal.assign(rem.size(), 1u);
out.remainder.material_id = b.material_id;
out.remainder.brush_id = b.id;
out.remainder.sky = b.sky;
out.remainder.clip_only = true;
out.has_remainder = true;
}
return true;
}
// World AABB of one carve shape. Mirrors DCBrushStore's own private shape_bounds
// so the split peels around exactly the box the store used to decide which
// brushes a carve TOUCHED — if these two ever disagree, a brush gets split around
// a box that does not contain the carve that selected it.
inline void carve_bounds(const TerrainEdit& e, CSG4Vec3& lo, CSG4Vec3& hi) {
float ex, ey, ez;
if (e.shape == TerrainEdit::BOX) {
ex = e.half_ext[0]; ey = e.half_ext[1]; ez = e.half_ext[2];
} else {
ex = ey = ez = e.radius; // sphere / mesh / convex: bounding sphere
}
lo = {e.x - ex, e.y - ey, e.z - ez};
hi = {e.x + ex, e.y + ey, e.z + ez};
}
inline void union_aabb(CSG4Vec3& lo, CSG4Vec3& hi, const CSG4Vec3& l, const CSG4Vec3& h) {
lo.x = std::min(lo.x, l.x); lo.y = std::min(lo.y, l.y); lo.z = std::min(lo.z, l.z);
hi.x = std::max(hi.x, h.x); hi.y = std::max(hi.y, h.y); hi.z = std::max(hi.z, h.z);
}
// The pocket's sampling volume: the carve AABB inflated by a margin, clamped to
// the brush. Inflated deliberately — it costs a slightly larger pocket and keeps
// the DC surface away from the sampling boundary, where a vertex landing exactly
// on the edge is where seam artifacts come from. The exact pieces are peeled
// with the SAME inflated box, so the two still meet exactly.
inline void inflate(CSG4Vec3& lo, CSG4Vec3& hi, float margin) {
lo.x -= margin; lo.y -= margin; lo.z -= margin;
hi.x += margin; hi.y += margin; hi.z += margin;
}
} // namespace dc_brush_split
#endif // DC_BRUSH_SPLIT_H