#ifndef DC_LEVEL_H
#define DC_LEVEL_H
// ---------------------------------------------------------------------------
// DCLevel — the Godot-side node for the EXACT brush level.
//
// docs/level-mesh-implementation.md §2 (module map), §12 step 3.
// Owns a DCBrushStore (durable state) and derives everything else from it:
//
// BrushStore --csg4--> visible skin --dc_level_mesh--> ArrayMesh
// \--unclipped hulls------------------------> convex colliders
//
// This node is NOT the octree clipmap and shares no code with it. The world
// (procedural, destructible-everywhere) keeps DCTerrain; the level (authored,
// finite, sharp, low-poly) gets this. That separation IS the pivot.
//
// Attribute conventions match DCTerrain's chunk meshes so the SAME terrain
// shader works unchanged:
// ARRAY_COLOR R = material_id/255, A = 1 on textured faces (route ②)
// ARRAY_CUSTOM0 RG_FLOAT = two per-face projection candidates
// On the exact mesh both candidates are the SAME face — a fragment's face is
// known exactly, so the shader's crease-pick becomes a no-op rather than a
// heuristic. That is the material-quantization problem disappearing by
// construction, not being solved.
// ---------------------------------------------------------------------------
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/mesh_instance3d.hpp>
#include <godot_cpp/classes/array_mesh.hpp>
#include <godot_cpp/classes/physics_server3d.hpp>
#include <godot_cpp/variant/packed_float32_array.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/vector3.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/array.hpp>
#include <cstring>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "dc_brush_store.h"
#include "dc_csg4.h"
#include "dc_level_mesh.h"
#include "dc_pocket.h"
#include "dc_brush_split.h"
#include "dc_level_journal.h"
#include "dc_pick.h"
using namespace godot;
class DCLevel : public Node3D {
GDCLASS(DCLevel, Node3D)
public:
DCLevel();
~DCLevel();
void _ready() override;
void _exit_tree() override;
// ---- import ----
// `planes` is flat (nx,ny,nz,d) quads — the SAME layout the Quake importer
// already passes to DCTerrain::edit_terrain_convex, so it can drive either.
// `plane_proj` is parallel per-face (1-based; empty = none).
// `sky` marks a Quake sky brush: renders and CLIPS normally, but gets no
// collider (jump through it) and is the authored outside-wall marker.
// `key` is the brush's DURABLE name — the dot of the op that created it. Pass
// "" for an IMPORT, which is named "i:<level_id>:<n>" deterministically on
// every peer. Returns 0 when the plane set encloses nothing, or when a brush
// with this key already exists (in which case it is that brush's id: replay is
// not once-only, so a create must be idempotent).
// `clearance` marks AIR THE LEVEL OWNS rather than geometry — the headroom
// over a flight of stairs, the bore of an archway driven into a hillside. It
// renders nothing, collides with nothing and clips nothing; its only effect
// is that build_shell encloses it, so the surrounding DCTerrain has the
// passage subtracted from it. See DCBrush::clearance (hyper-games #25 §2).
// `group` names the operator invocation this brush came from — every
// voussoir and the clearance of one arch share it, so the whole thing moves
// and dies together. See DCBrush::group.
int add_brush(const PackedFloat32Array &planes, int material_id,
const PackedInt32Array &plane_proj, int level_id, bool sky,
const String &key, bool clearance = false,
const String &group = String());
// Brush ids of every live sky brush — the level's authored boundary, for the
// level<->world seam (§5a). The map author marked it by texturing it sky,
// which beats inferring "outside" from visible-skin membership.
PackedInt32Array get_sky_brush_ids() const;
// Brush ids of every live clearance brush. Nothing draws them — that is what
// they are — so a view that wants to SHOW them (the editor's clearance
// toggle) has to ask, and so does anything wanting to aim at one.
PackedInt32Array get_clearance_brush_ids() const;
// Every live brush id, in STORE ORDER — which is import order, and therefore
// the same on every peer. The outliner needs to enumerate what is in the
// level and there was no way to: get_brush(id) answers about one id, and the
// only lists were the two flag-specific ones above. Walking 1..N and probing
// for gaps works until a tombstone leaves one, which it always does.
PackedInt32Array get_live_brush_ids() const;
// The group key of one brush, "" when it belongs to none (every imported
// brush, and anything made before groups existed).
String get_brush_group(int brush_id) const;
// Every live brush carrying `group`, in store order. The outliner's group row
// is this call — DERIVED, so it cannot disagree with the geometry.
PackedInt32Array get_group_brush_ids(const String &group) const;
// Every distinct group in the level, in first-appearance order.
PackedStringArray get_group_keys() const;
// ---- HIDING (the outliner's eye — hyper-games #28) ----------------------
//
// VIEW STATE, NOT GEOMETRY, and the placement of it says so: the hidden set
// lives on this NODE and not in DCBrushStore. That is structural rather than
// careful. A `hidden` field on DCBrush would sit one line away from `sky` and
// `clearance`, inside the object every view iterates, and the first mistake
// anyone makes with it is the expensive one — dropping hidden brushes from
// shell_view. Hide a boundary wall, the flood fill escapes, the shell stops
// sealing and the hillside fills every room. Out here it cannot reach the
// store's views or the journal at all.
//
// Hiding filters at MESH EMIT, after the clip, so csg4 sees the level
// unchanged. Dropping hidden brushes from its INPUT instead would re-clip
// their neighbours — faces buried behind a wall become visible the moment you
// hide it — so unhiding would not restore what you had, and every toggle
// would re-clip the whole level. Filtering the faces keeps hiding a view.
//
// Colliders go with it, so you can fly through what you hid. NOT the pocket
// trimeshes of a carved brush yet — see the note in rebuild().
//
// Sets dirty_ but does NOT rebuild: hiding a collection is N calls and one
// rebuild, the same reason CreateBoxOperator batches a flight of stairs.
void set_brush_hidden(int brush_id, bool hidden);
bool is_brush_hidden(int brush_id) const;
PackedInt32Array get_hidden_brush_ids() const;
void clear_hidden();
/// The level's SEALED VOLUME as a closed mesh, for one subtractive
/// TerrainEdit::MESH on the surrounding DCTerrain — level-mesh-
/// implementation.md §5a.2. Replaces the inflated-AABB moat, which subtracts
/// 324622 m^3 where the sealed volume is ~38500 and the solid is 12600.
///
/// Returns { vertices: PackedVector3Array, indices: PackedInt32Array,
/// center: Vector3, triangles, volume, area, snapped, unmoved }.
///
/// `center` is the mesh's own centroid and MUST be what the caller passes as
/// stamp_mesh_arrays' `position`: that function re-centres the vertices on
/// their centroid and treats `position` as where the centroid lands, so
/// anything else silently translates the whole shell.
///
/// Requires the level to have been imported WITH SKY BRUSHES. A sky brush is
/// solid and only its rendering is special; without them the boundary has
/// holes, the flood fill leaks out through the ceiling and the result is an
/// empty or wildly wrong volume — check `volume` against the level's size
/// rather than trusting a non-empty mesh.
Dictionary build_shell(float cell = 0.25f, float clamp_cells = 0.5f) const;
/// The same shell, off the caller's thread — because a 2.2 s freeze on every
/// edit is what this costs when the editor re-derives it live.
///
/// THE SNAPSHOT IS THE WHOLE POINT. build_shell's first act is to copy the
/// brush view out of the store, and everything after that reads only the copy.
/// So `start` takes that copy HERE, on the caller's thread, and the flood runs
/// on a worker against memory nothing else can touch. Handing a Godot thread
/// the whole of build_shell instead would race every edit made during the
/// build, which is precisely what the build is being moved off the frame to
/// allow.
///
/// The worker touches no engine type: it fills a POD ShellMesh, and the
/// Dictionary is packed in `take`, on whichever thread calls it.
///
/// if (level.shell_ready()) apply(level.build_shell_take())
/// elif not level.shell_building(): level.build_shell_start()
///
/// `start` returns false if a build is already running or the level is empty.
/// `take` returns an empty Dictionary unless a build has finished, and hands
/// ownership over: the next `start` begins a fresh one. Nothing here cancels —
/// a build whose answer is already stale is cheaper to let finish and discard
/// than to make interruptible, and the caller compares its own cache key.
bool build_shell_start(float cell = 0.25f, float clamp_cells = 0.5f);
bool shell_building() const;
bool shell_ready() const;
Dictionary build_shell_take();
// ---- lifecycle (tombstones — there is no durable delete path) ----
bool remove_brush(int brush_id);
bool restore_brush(int brush_id);
int remove_level(int level_id);
int restore_level(int level_id);
void clear();
// ---- transform + shape editing (stays a brush; no model involved) ----
bool translate_brush(int brush_id, const Vector3 &delta);
// AXIS-ALIGNED SCALE about `pivot` — exact on half-spaces, see
// DCBrushStore::scale_brush. Refused (false, nothing changed) on a zero
// factor or if it would leave the brush enclosing nothing.
//
// The pivot is the CALLER'S, not the brush's own centre: a group scales
// about one shared point or its pieces fly apart, and the clearance volume
// of an arch has to move with the ring it belongs to.
bool scale_brush(int brush_id, const Vector3 &scale, const Vector3 &pivot);
// TRUE VERTEX / EDGE DRAG — move named CORNERS and rebuild the solid from its
// points, so a corner moves without dragging its neighbours. Everything else
// here moves planes, which is exact and is precisely why it cannot do this.
// `from` names corners by position; a position that is not a corner is refused.
bool move_brush_points(int brush_id, const PackedVector3Array &from,
const Vector3 &delta, const String &key);
// CSG. Planes are flattened 4-per-plane (nx, ny, nz, d), the same shape add_brush
// takes, so a plane set read back from get_brush can be handed straight over.
// subtract returns the piece count, 0 if the cutter swallowed the brush, or -1 if
// it missed and nothing changed.
int subtract_brush(int brush_id, const PackedFloat32Array &cutter, const String &key);
bool intersect_brush(int brush_id, const PackedFloat32Array &other, const String &key);
bool merge_brushes(const PackedInt32Array &brush_ids, const String &key);
bool set_face_plane(int brush_id, int face, const Vector3 &normal, float d);
// `key` is the CRDT dot of the op creating this face ("<replica>:<counter>").
// Pass "" only for local geometry that is never journalled — two peers cannot
// agree which face an add refers to without it. Re-adding a dot the brush
// already carries is refused, so replay is idempotent.
int add_face(int brush_id, const Vector3 &normal, float d, int proj,
const String &key);
bool remove_face(int brush_id, int face);
// CLIP / SPLIT — the operation that turns "adjust a level" into "author one".
// add_face can only SHRINK a convex solid, so removing material from the middle
// of a wall was unreachable; a doorway is now split, split, remove the middle.
// The original keeps the front half (n.x <= d); the back half is a new brush
// named `key` (the dot of the split record). Returns its id, or 0 if the plane
// does not divide the solid, in which case nothing changed.
int split_brush(int brush_id, const Vector3 &normal, float d, const String &key);
// Durable name -> local handle, 0 when unknown. Anything that outlives a
// single call — a journal record, a selection held across a re-fold — must hold
// the key and resolve it here, because the id is local to this peer.
int brush_by_key(const String &key) const;
String get_brush_key(int brush_id) const;
// ---- carves (derived-and-reversible; the brush stays authoritative) ----
// `key` is the carve's durable name (the dot of the op). Pass "" for a carve
// that is never journalled. The id-taking undo/redo below are LOCAL-ONLY: a
// replaying peer mints its own ids, so undoing by another peer's number undoes
// whichever carve happens to hold it here — which is exactly the defect that
// shipped (hyper-games #22 §5 item 4). Cross-peer code uses the _by_key forms.
int apply_carve_sphere(const Vector3 ¢er, float radius, const String &key);
int apply_carve_box(const Vector3 ¢er, const Vector3 &half_extents,
const String &key);
bool undo_carve(int carve_id);
bool redo_carve(int carve_id);
bool undo_carve_by_key(const String &key);
bool redo_carve_by_key(const String &key);
// ---- the journal (dc_journal::fold — see libdc/include/dc_level_journal.h) --
//
// THE JOURNAL IS THE STATE AND THIS NODE IS A DERIVED CACHE. Hand it every
// live record and it rewinds to the imported base and replays them in
// (seq, author) order. Undo is a record marked `undone` plus another call —
// there are no inverse ops, and both this UI and blender-bridge get the same
// undo because the fold is in libdc rather than in either of them.
//
// Each record is a Dictionary:
// author String the memberId that authored it
// seq int its counter; (seq, author) is the total order
// kind String "brush.create" | "brush.remove" | "brush.translate"
// | "face.set" | "face.add" | "face.remove" | "carve"
// undone bool a tombstone VALUE — erase has no durable path
// brush_key String what it targets (ignored by brush.create)
// face_key String what it targets (face.set / face.remove)
// planes PackedFloat32Array, proj PackedInt32Array, material_id,
// level_id, sky (brush.create)
// n Vector3, d float, face_proj int (face.set / face.add)
// delta Vector3 (brush.translate)
// carve Dictionary {type:"sphere"|"box", pos:Vector3,
// radius:float, half:Vector3} (carve)
//
// Returns {applied, skipped, undone, records}. `skipped` is not noise: it
// counts live records whose target is gone or that the store refused, i.e.
// places where the journal and this peer's geometry disagree.
Dictionary apply_journal(const Array &records);
// Order-independent hash of live brush geometry, over DURABLE KEYS — so two
// peers can be compared directly rather than each against its own past.
// Convergence is the property that matters; determinism alone is not enough.
int64_t fingerprint() const;
// ---- derive ----
void rebuild(); // full: csg4 -> mesh (+ collision) -> pockets
void set_build_collision(bool v);
bool get_build_collision() const;
// Voxel size for carved pockets. Defaults to the level import's grid-aligned
// 0.224 (8 Quake units x world_scale) so a pocket lands on the same lattice as
// everything else. Exposed because this is the Nyquist knob: Quake's 8-16 unit
// walls are 0.224-0.448m, i.e. 1-2 voxels here, so a carved THIN wall can hole
// at the default and wants a finer pocket. Finer is affordable precisely
// because it applies only to the carved region.
void set_pocket_voxel_size(float v);
float get_pocket_voxel_size() const;
// ---- picking (editor) ----
// Resolve a raycast hit back to the brush that produced it. A Godot ray
// result gives {collider, shape}; `shape` indexes into THIS body's shape
// list, which is NOT the brush index — build_colliders skips any brush
// whose hull has <4 points, so the mapping is RECORDED as shapes are added
// rather than inferred. Returns -1 when out of range.
int get_brush_id_from_shape(int shape_index) const;
// Read a brush back. Face-level editing needs this for two things the API
// cannot otherwise supply: an editor has to know what it is editing, and
// set_face_plane has NO INVERSE unless the caller recorded the plane it is
// replacing — so undo is built on this, not on a snapshot of the whole store.
// `generation` is included because it is what a replicated op-log compares to
// decide whether it is talking about the brush it thinks it is.
// Empty Dictionary if the id is unknown.
Dictionary get_brush(int brush_id) const;
// Which face did the player click? A raycast gives a point on the surface;
// this turns that into the face index the edit operations take. -1 if the
// brush is unknown or bounds nothing.
int pick_face(int brush_id, const Vector3 &point) const;
// WHAT AM I POINTING AT — with no physics server involved.
//
// The pair above (get_brush_id_from_shape + pick_face) needs a Godot collision
// shape index, so the editor's most basic query required the physics server to
// have been fed, stepped and queried. blender-bridge has none, which made this
// a blocker for a second consumer rather than a convenience. A brush is an
// intersection of half-spaces, so the query is a convex slab clip against its
// own planes (dc_pick.h) and needs nothing else.
//
// `from`/`to` match PhysicsRayQueryParameters3D.create, so a caller can swap
// one for the other. Returns {} on a miss, else:
// brush int, face int, brush_key String, face_key String,
// position Vector3, normal Vector3, distance float, inside bool
// The DURABLE names are included because anything that outlives the call — a
// journal record, a selection across a re-fold — must hold those, not the ids.
//
// Differs from the physics path in two ways, deliberately: SKY brushes are
// pickable (they have no collider, so a physics ray goes through the boundary
// the author marked), and a CARVED brush is hit at its hull (the pocket is a
// derived cache; the brush is the editable thing).
//
// CLEARANCE brushes are NOT hit unless `include_clearance` says so. One
// encloses the shape it belongs to, so a pickable clearance volume would
// intercept every ray aimed at the stairs inside it and the editor would be
// unable to select the geometry it is there to serve. The editor's clearance
// view passes true, which is what makes their extents draggable with the
// ordinary face tools rather than needing a parallel set of their own.
Dictionary raycast(const Vector3 &from, const Vector3 &to,
bool include_clearance = false) const;
// Face INDICES are positions and remove_face renumbers them. Anything that
// outlives a single call — an editor selection, a replicated op-log — must
// hold the dot from get_brush()["plane_keys"] and resolve it here.
// -1 if that face is gone.
int face_by_key(int brush_id, const String &key) const;
// Is this face a SURFACE, or a dormant plane? A plane that bounds nothing
// stays in the set — that is what makes topology change reversible — so
// "face" and "plane" are not the same thing and an editor must distinguish
// them. get_brush()["live_faces"] is the count; this asks per face.
bool face_is_live(int brush_id, int face) const;
// The selected face as a polygon, world space, wound around its normal.
// Empty if the brush or face is unknown. An editor needs this to SHOW what
// is selected; every gizmo past a highlight needs the same outline.
PackedVector3Array get_face_winding(int brush_id, int face) const;
// ---- the wireframe ----
// Every live brush's outline as LINE PAIRS, ready for PRIMITIVE_LINES. The level
// renders as one merged surface, so without this the structure being edited is
// invisible — and a split, which produces two brushes that tile the original
// exactly, looks like nothing happened at all. Dormant planes are skipped: they
// bound nothing, so drawing them would show an edge that is not there.
PackedVector3Array get_brush_edges() const;
// The clearance volumes, on their own, for the editor's clearance view.
// Deliberately not folded into get_brush_edges: they are a different kind of
// thing from a wall and drawing them in the same colour says they are not.
PackedVector3Array get_clearance_edges() const;
PackedVector3Array get_brush_edges_of(int brush_id) const;
// The DISTINCT corners of one brush — the handles an editor lets you aim at.
// Deduped in the store so every consumer gets the same answer and there is one
// epsilon to be careful about rather than one per caller.
PackedVector3Array get_brush_vertices_of(int brush_id) const;
// The polygon a split by this plane WOULD create — the operator preview. Empty
// when the plane does not divide the solid, which is the same condition
// split_brush refuses on: a preview that shows nothing is telling the truth.
PackedVector3Array get_cut_preview(int brush_id, const Vector3 &normal, float d) const;
// ---- introspection ----
int get_brush_count() const; // live (non-tombstoned)
int get_triangle_count() const;
int get_vertex_count() const;
int get_collider_count() const;
int get_carve_count() const;
int get_pocket_count() const;
Dictionary get_stats() const;
protected:
static void _bind_methods();
private:
// The in-flight shell build. Defined in the .cpp so this header does not have
// to pull in <thread>/<atomic> or the shell builder; ~DCLevel is out of line,
// which is what an incomplete type in a unique_ptr needs. Its destructor joins,
// so a level freed mid-build waits for its own worker rather than outliving it.
struct ShellJob;
std::unique_ptr<ShellJob> shell_job_;
// Hidden brush ids. Runtime only: never journalled, never persisted, never
// seen by DCBrushStore. See set_brush_hidden.
std::unordered_set<uint32_t> hidden_;
void _upload_mesh(const DCLevelMeshData &m);
void _upload_collision(const std::vector<DCConvexCollider> &cols);
void _free_collision();
void _free_all_shapes();
// One pocket per connected component of carved brushes. Runs AFTER
// _upload_collision, because pocket trimeshes are added to the body it
// creates — a carved brush leaves the CLEAN view, so it loses its convex
// collider too, and without this you fall through a wall you carved.
void _build_pockets();
void _compute_pocket_boxes();
uint64_t _pocket_key(size_t cluster_index) const;
struct PocketCache;
void _add_pocket_collision(PocketCache &pc, const std::vector<uint32_t> &comp);
// A pocket's built output, keyed by the identity of its inputs. One carve
// dirties one cluster; the rest are reused instead of re-running DC, which
// is what took ms_pocket from 5.9ms on the first carve to 422ms on the 15th.
struct PocketCache {
uint64_t key = 0;
Array arrays; // ready for add_surface_from_arrays
PackedVector3Array faces; // collision trimesh
uint32_t tris = 0;
RID shape; // collision RID, reused with the mesh
};
std::vector<PocketCache> pocket_cache_;
static CSG4Brush _piece_view(const dc_brush_split::Piece &p);
// Voxels of margin added to the carve box before peeling.
//
// ZERO, and that is a correction, not an omission. It was 2.0 on the
// reasoning that the DC surface needs to stay off the sampling boundary —
// but dc_pocket::build_bounded ALREADY provides exactly that margin, pulling
// the grid origin back by cfg.padding voxels and sizing the grid
// +2*padding. So the inflate was applying the same margin a second time, to
// the SPLIT box, where it does nothing for the seam and everything for cost:
// it turned a 0.6m carve's 1.2m box into 2.1m, a 75% larger extent and ~5x
// the volume, which is enough to swallow an ordinary Quake brush whole.
//
// Measured in-game at 0.826.0 with exactly that outcome: split_swallowed 3,
// split_pieces 0 — every carved brush consumed entirely, no exact remainder.
static constexpr float POCKET_INFLATE_VOXELS = 0.0f;
// Recomputed every rebuild, then SHARED between the split and the pocket.
// They must agree: the exact pieces are peeled with the same box the pocket
// samples, and that shared plane is what makes the two meet exactly.
std::vector<std::vector<uint32_t>> pocket_comps_;
std::vector<std::pair<CSG4Vec3, CSG4Vec3>> pocket_boxes_;
DCBrushStore store_;
MeshInstance3D *mesh_node_ = nullptr;
Ref<ArrayMesh> mesh_;
// Separate node from the exact mesh: pockets are voxel geometry with their
// own material needs and their own rebuild cadence, and keeping them apart
// means a carve never re-uploads the 12,747-triangle exact mesh.
MeshInstance3D *pocket_node_ = nullptr;
Ref<ArrayMesh> pocket_mesh_;
RID body_; // one static body, N convex shapes
std::vector<RID> shapes_; // per-rebuild ORDER; the caches own the RIDs
// Convex shape RIDs by point-set hash. On a carve ~1170 of ~1172 brushes are
// untouched, so recreating them all was thousands of PhysicsServer calls per
// keypress for geometry that did not move.
std::unordered_map<uint64_t, RID> shape_cache_;
std::vector<uint32_t> shape_brush_ids_; // parallel to shapes_; ray shape idx -> brush id
bool build_collision_ = true;
bool dirty_ = false;
float pocket_voxel_size_ = 0.224f;
// last rebuild stats
uint32_t last_pocket_tris_ = 0, last_pockets_ = 0;
uint32_t last_split_pieces_ = 0, last_split_swallowed_ = 0;
uint32_t last_pockets_rebuilt_ = 0; // vs reused from cache
uint32_t last_tris_ = 0, last_verts_ = 0, last_faces_ = 0;
uint32_t last_removed_ = 0, last_fragmented_ = 0;
double last_area_ = 0.0;
float last_normal_dev_ = 0.0f;
double last_build_ms_ = 0.0;
// Per-phase breakdown of the last rebuild. A single build_ms cannot say
// WHICH phase makes an interactive carve hitch, and the three candidates
// want different fixes.
double last_ms_split_ = 0.0, last_ms_clip_ = 0.0, last_ms_mesh_ = 0.0;
double last_ms_upload_ = 0.0, last_ms_collide_ = 0.0, last_ms_pocket_ = 0.0;
};
#endif // DC_LEVEL_H