extends Node
## HEADLESS GATE — the operator contract.
##
## The properties an immediate-mutation binding cannot have, asserted directly:
##
## 1. NOTHING IS WRITTEN UNTIL CONFIRM. A cancelled operator leaves the journal
## untouched — no record, so no peer ever saw it and there is nothing to undo.
## In a shared room a stray undo is not an annoyance, it is an edit someone else
## watched happen.
## 2. ESCAPE IS FIRST-WINS. An active operator eats it; otherwise the caller keeps
## it and Escape goes on meaning release-mouse / menu.
## 3. SWITCHING TOOLS CANCELS, never commits. Picking up another tool is not
## agreement to commit what you were part-way through.
## 4. The preview is the cut that WOULD happen — same polygon, from the same code
## path that performs it.
##
## Run: "$HYPER_TPS_GODOT" --headless res://scenes/test/operator_gate.tscn
var fails := 0
func _ok(what: String, cond: bool) -> void:
if not cond:
fails += 1
print(" %-58s %s" % [what, "OK" if cond else "**FAIL**"])
func _eqi(what: String, got: int, want: int) -> void:
var c := got == want
if not c:
fails += 1
print(" %-58s %-6d want %-6d %s" % [what, got, want, "OK" if c else "**FAIL**"])
func _ready() -> void:
# A REAL window: headless is 64x64, and the picks below click the viewport's centre,
# which must be inside the 3D view's free rectangle (#124 B1: every point outside it
# belongs to an editor area, and a 64x64 window is nearly all area).
get_tree().root.size = Vector2i(1920, 1080)
await get_tree().process_frame
print("=== Operator gate ===\n")
# SNAP PINNED ON for the whole gate. Maps restore snap from the player's pref and
# this gate shares the installed game's user:// — with the player's snap OFF every
# step became 1u and three geometry checks failed on a clean tree. Restored at the end.
var pref_dock: Node = get_tree().root.get_node_or_null("/root/BuildDock")
var saved_snap: Variant = pref_dock.pref("snap", true) if pref_dock != null else null
if pref_dock != null:
pref_dock.set_pref("snap", true)
var stack := OperatorStack.new()
add_child(stack)
# A stub operator, so the CONTRACT is tested rather than the knife's geometry.
var op := LevelOperator.new()
add_child(op)
print("-- activate / cancel --")
_ok("nothing is active to begin with", stack.active == null)
_ok("activate", stack.activate(op))
_ok("it is active", stack.active == op)
_ok("escape is consumed by the active operator", stack.handle_escape())
_ok("and it is no longer active", stack.active == null)
_ok("a second escape is NOT consumed — it belongs to the caller",
not stack.handle_escape())
print("\n-- an operator that refuses to start never becomes active --")
var picky := RefusingOperator.new()
add_child(picky)
_ok("activation is refused", not stack.activate(picky))
_ok("so nothing is active", stack.active == null)
print("\n-- switching tools CANCELS the first, it does not commit it --")
var a := CountingOperator.new()
var b := CountingOperator.new()
add_child(a)
add_child(b)
stack.activate(a)
stack.activate(b)
_ok("the second is active", stack.active == b)
_eqi("the first was never confirmed", a.confirms, 0)
_eqi("it was deactivated", a.deactivations, 1)
print("\n-- confirm commits exactly once, then leaves --")
_ok("confirm reports that it wrote", stack.confirm())
_eqi("confirmed once", b.confirms, 1)
_ok("and the operator is no longer active", stack.active == null)
_ok("confirming again does nothing", not stack.confirm())
_eqi("still once", b.confirms, 1)
print("\n-- the status line names the tool and its setting --")
stack.activate(b)
_ok("the HUD line carries the operator name",
stack.status_line().to_lower().contains("counting"))
stack.cancel()
_ok("and is empty when nothing is active", stack.status_line() == "")
print("\n-- THE REAL OPERATORS, against a real store --")
# The contract above is tested with stubs; this is the geometry. A stub map
# supplies exactly what an operator reaches for — the aim, the grid, the journal —
# so the operators are exercised without a level scene, a .MAP or a camera.
var lvl: Node3D = ClassDB.instantiate("DCLevel")
add_child(lvl)
await get_tree().process_frame
var stub := StubMap.new()
stub.level = lvl
add_child(stub)
var wall: int = lvl.add_brush(_box_planes(Vector3(0, 0, 0), Vector3(8, 4, 1)),
7, PackedInt32Array(), 0, false, "")
lvl.rebuild()
stub.aim_brush = wall
stub.aim_point = Vector3(4, 2, 1) # on the +z face
stub.aim_normal = Vector3.BACK
print("\n move --")
var mv := MoveOperator.new()
mv.level = lvl
mv.map = stub
add_child(mv)
_ok("activates on a brush", stack.activate(mv))
_ok("does nothing with no steps yet", not mv.confirm())
mv.handle_key(_key(KEY_BRACKETRIGHT))
mv.handle_key(_key(KEY_BRACKETRIGHT))
_ok("status names the axis and the offset", mv.status().contains("+2"))
var before_x: float = _min_x(lvl, wall)
_ok("confirm moves it", mv.confirm())
var moved: float = _min_x(lvl, wall) - before_x
_ok("by two grid steps on X (%.3f)" % moved, absf(moved - 2.0 * stub.step()) < 1e-3)
_eqi("and wrote exactly one record", stub.records.size(), 1)
_ok("of the right kind", String(stub.records[0]["kind"]) == "brush.translate")
# Texture lock is ON by default (#94 P1 D5) and RIDES THE RECORD, so a peer's
# fold moves the texture exactly as the author's local move did.
_ok("the move is texture-locked, in the record", bool(stub.records[0].get("tex_lock", false)))
print("\n extrude --")
stub.records.clear()
var ex := ExtrudeOperator.new()
ex.level = lvl
ex.map = stub
add_child(ex)
stub.aim_point = Vector3(6, 2, 1) # still the +z face, after the move
_ok("activates on a face", stack.activate(ex))
ex.handle_key(_key(KEY_BRACKETRIGHT))
var faces_before: int = int((lvl.get_brush(wall) as Dictionary)["live_faces"])
_ok("confirm extrudes", ex.confirm())
_eqi("the brush still has six faces", int((lvl.get_brush(wall) as Dictionary)["live_faces"]), faces_before)
_eqi("one record", stub.records.size(), 1)
_ok("a face.set", String(stub.records[0]["kind"]) == "face.set")
print("\n create box --")
stub.records.clear()
var bx := CreateBoxOperator.new()
bx.level = lvl
bx.map = stub
add_child(bx)
var count_before: int = lvl.get_brush_count()
_ok("activates while aiming at a surface", stack.activate(bx))
# PARAMETRIC (parametric-shapes-design §13): the commit is ONE shape.create; the
# fold makes the brush, named by the record and grouped under its dot.
_ok("confirm creates", bx.confirm())
_eqi("one more brush", lvl.get_brush_count(), count_before + 1)
_eqi("one record", stub.records.size(), 1)
_ok("a shape.create of type box", String(stub.records[0]["kind"]) == "shape.create"
and String(stub.records[0].get("type", "")) == "box")
var box_ids: PackedInt32Array = lvl.get_group_brush_ids(String(stub.records[0]["dot"]))
_eqi("its one piece is grouped under the record's dot", box_ids.size(), 1)
print("\n a new box is TEXTURED (#94 P1 gate 6, §13.3) --")
# The record carries the map's CURRENT texture — and the brush the fold made carries
# it on every plane, or the author sees grey while every peer sees the texture.
var rec_tex: Dictionary = stub.records[0].get("tex", {})
_ok("the record names the current texture", String(rec_tex.get("tex", "")) == StubMap.CURRENT_TEX
and int(rec_tex.get("mode", 0)) == 1)
var box_id: int = box_ids[0] if box_ids.size() > 0 else -1
var loc_ft: Array = lvl.get_brush(box_id).get("face_tex", []) if box_id >= 0 else []
var rec_planes: int = (lvl.get_brush(box_id).get("planes", PackedFloat32Array()) as PackedFloat32Array).size() / 4 if box_id >= 0 else 0
var loc_ok := loc_ft.size() == rec_planes and rec_planes >= 6
for t: Dictionary in loc_ft:
loc_ok = loc_ok and String(t.get("tex", "")) == StubMap.CURRENT_TEX and int(t.get("mode", 0)) == 1
_ok("the brush carries it on every plane", loc_ok)
print("\n where the box LANDS — TrenchBroom's moveDeltaForBounds rules --")
# Grid.cpp:180. Three rules, none of which we had, all of which decide whether a
# box lands where you were pointing or somewhere defensible-but-wrong.
var step: float = float(StubMap.GRID_UNITS[stub._grid_idx]) * StubMap.WORLD_SCALE
# RULE 1 — an AXIAL surface is treated as on-grid along its own normal. TB's
# reason: "pasting on top of 1 unit thick floor detail on grid 8". Ours is
# stronger — the lattice origin is the map's recentre offset and map_dc_world
# LIFTS the level, so a floor being off-lattice is the normal case, not the
# exception. Rounding the normal axis buries the box in the floor or floats it.
# aim_brush stays as it is: the stub returns NO HIT while it is negative, and
# update_preview() then leaves _origin at its previous value — so the first
# draft of this section was asserting against the box from the test before it.
stub.aim_normal = Vector3.UP
stub.aim_point = Vector3(0.0, 0.4 * step, 0.0)
stub.aim_ray(Vector3(0.0, 5.0, 0.0), Vector3.DOWN)
bx.update_preview()
var lo: Vector3 = (bx._corners() as Array)[0]
_ok("a box on an off-lattice floor rests ON it (%.4f, floor %.4f)"
% [lo.y, stub.aim_point.y],
absf(lo.y - stub.aim_point.y) < 1e-5)
# RULE 2 — WHICH SIDE. The two axes across the normal put the box on the far
# side of the hit from the camera, so the corner you are pointing at is the one
# nearest you. Aimed exactly ON a lattice line, so snapping is a no-op and this
# measures the side and nothing else: standing at +X, the box must be in -X.
stub.aim_point = Vector3.ZERO
stub.aim_ray(Vector3(5.0, 5.0, 0.0), Vector3(-1, -1, 0).normalized())
bx.update_preview()
var hi2: Vector3 = (bx._corners() as Array)[1]
_ok("standing at +X, the box is on the -X side of the aim (max %.4f)" % hi2.x,
absf(hi2.x) < 1e-5)
# RULE 3 — WHICH LATTICE LINE. The snap goes TOWARDS the viewer, not to the
# nearest line. Aimed at 0.4 of a step, the two answers differ: nearest rounds
# DOWN to 0 (away from the camera at +X), towards-the-viewer takes the line at
# one full step. That is the difference between a box placed where you were
# pointing and one placed a grid square behind it.
stub.aim_point = Vector3(0.4 * step, 0.0, 0.0)
stub.aim_ray(Vector3(5.0, 5.0, 0.0), Vector3(-1, -1, 0).normalized())
bx.update_preview()
var hi3: Vector3 = (bx._corners() as Array)[1]
_ok("snapped to the lattice line towards the viewer (%.4f, want %.4f)"
% [hi3.x, step],
absf(hi3.x - step) < 1e-5)
# AND STILL ON THE LATTICE where it matters: the across-axes are snapped, so a
# box placed on a floor still lines up with everything else on that floor.
_ok("the across-axis corner is on the lattice (%.4f)" % hi3.x,
absf(hi3.x / step - roundf(hi3.x / step)) < 1e-4)
print("\n stairs and arch — one placement, more than one solid --")
# #31's last ready item. TrenchBroom dispatches seven parametric shapes from ONE
# tool and one bounds; these assert the two we added fill those bounds the way its
# generators do, and that the bounds themselves are still decided by the rules
# just asserted above.
#
# Aimed at an on-lattice floor from +Z, so the run/tunnel axis is Z and nothing
# here is measuring the snap a second time.
stub.aim_normal = Vector3.UP
stub.aim_point = Vector3.ZERO
stub.aim_ray(Vector3(0.0, 5.0, 10.0), Vector3(0, -1, -1).normalized())
bx._size_steps = 4
bx._shape = CreateBoxOperator.Shape.STAIRS
bx.update_preview()
var lo_s: Vector3 = (bx._corners() as Array)[0]
var hi_s: Vector3 = (bx._corners() as Array)[1]
var steps: Array = bx._pieces()
_eqi("a flight is one solid per step", steps.size(), 4)
var tops: Array = []
var on_floor := true
for pc: Dictionary in steps:
var sb: Array = _plane_box(pc["planes"])
tops.append((sb[1] as Vector3).y)
if absf((sb[0] as Vector3).y - lo_s.y) > 1e-5:
on_floor = false
# THE PROPERTY THAT MAKES A FLIGHT CLIMBABLE. TrenchBroom's step i is a solid from
# the floor up to its own tread, not a slab at tread height; a stack of hovering
# shelves looks identical from the front and is not stairs.
_ok("every step is a solid from the floor up, not a floating slab", on_floor)
var rising := true
for i in range(1, tops.size()):
if float(tops[i]) <= float(tops[i - 1]) + 1e-6:
rising = false
_ok("each tread is higher than the one before", rising)
_ok("the top tread reaches the top of the bounds (%.4f, want %.4f)"
% [float(tops[tops.size() - 1]), hi_s.y],
absf(float(tops[tops.size() - 1]) - hi_s.y) < 1e-5)
# Ascending AWAY from the viewer: standing at +Z, the lowest step is the near one.
_ok("the flight ascends away from you",
(_plane_box(steps[0]["planes"])[1] as Vector3).z
> (_plane_box(steps[steps.size() - 1]["planes"])[1] as Vector3).z)
bx._shape = CreateBoxOperator.Shape.ARCH
bx.update_preview()
var vou: Array = bx._pieces()
_eqi("an arch is a ring of voussoirs", vou.size(), CreateBoxOperator.ARCH_SEGMENTS)
var six := true
for pc: Dictionary in vou:
if (pc["planes"] as PackedFloat32Array).size() != 24:
six = false
_ok("each voussoir is bounded by six planes", six)
# THE HOLE IS THE POINT. A ring and a filled half-cylinder have the same outline,
# the same brush count and the same bounds; the only thing that tells them apart
# is whether anything is under the arc. Both probes are taken away from a segment
# boundary, where a point lies on two voussoirs at once by construction.
var cs: float = (lo_s.x + hi_s.x) * 0.5
var wm: float = (lo_s.z + hi_s.z) * 0.5
var arch_h: float = hi_s.y - lo_s.y
var open_pt := Vector3(cs, lo_s.y + arch_h * 0.2, wm)
var t_mid: float = PI * 4.5 / float(CreateBoxOperator.ARCH_SEGMENTS)
var rs: float = (hi_s.x - lo_s.x) * 0.5
var ring_pt := Vector3(
cs + (rs - step * 0.5) * cos(t_mid),
lo_s.y + (arch_h - step * 0.5) * sin(t_mid),
wm)
var in_open := 0
var in_ring := 0
for pc: Dictionary in vou:
if _inside(pc["planes"], open_pt):
in_open += 1
if _inside(pc["planes"], ring_pt):
in_ring += 1
_eqi("nothing fills the opening — a point under the arc is in no voussoir", in_open, 0)
_eqi("and the ring is solid where it should be", in_ring, 1)
var ymin := INF
var ymax := -INF
for pc: Dictionary in vou:
for p: Vector3 in (pc["edges"] as PackedVector3Array):
ymin = minf(ymin, p.y)
ymax = maxf(ymax, p.y)
_ok("it stands on the springing line and fills the bounds (%.4f .. %.4f)" % [ymin, ymax],
absf(ymin - lo_s.y) < 1e-5 and absf(ymax - hi_s.y) < 1e-5)
# AND THE STORE MUST TAKE THEM. This is the assertion that catches a plane written
# the wrong way round: area and closure are unchanged by a flip, so a flipped
# voussoir looks correct everywhere except here, where it bounds nothing.
stub.records.clear()
var before_arch: int = lvl.get_brush_count()
_ok("confirm creates the whole arch", bx.confirm())
# ONE VOUSSOIR EACH, PLUS THE CLEARANCE — the bore, so the arch can be driven
# into a hillside and walked through instead of being a ring full of dirt
# (hyper-games #25 §2). Asserted as a COMPOSITION rather than as a bumped
# total: "9 brushes" is equally true of eight voussoirs and a stray solid.
_eqi("one brush per voussoir, plus one clearance",
lvl.get_brush_count() - before_arch, CreateBoxOperator.ARCH_SEGMENTS + 1)
# ONE record — the tunnel's shape.create; the fold makes the pieces. Every peer
# folds the same record into the same clearance, so it replicates by construction.
_eqi("and ONE record, a tunnel shape.create", stub.records.size(), 1)
_ok("of type tunnel", String(stub.records[0].get("type", "")) == "tunnel")
var arch_ids: PackedInt32Array = lvl.get_group_brush_ids(String(stub.records[0]["dot"]))
var n_clear := 0
var tex_ok := true
var solid := true
for id: int in arch_ids:
var br: Dictionary = lvl.get_brush(id)
var rft: Array = br.get("face_tex", [])
if bool(br.get("clearance", false)):
n_clear += 1
# The store lists a default entry per plane for an untextured brush (tex "").
for ft: Dictionary in rft:
tex_ok = tex_ok and String(ft.get("tex", "")) == ""
else:
tex_ok = tex_ok and not rft.is_empty() and String((rft[0] as Dictionary).get("tex", "")) == StubMap.CURRENT_TEX
if (lvl.get_brush_vertices_of(id) as PackedVector3Array).size() < 4:
solid = false
_eqi("exactly one of its pieces is the clearance", n_clear, 1)
# Voussoirs textured, the clearance NOT: it is air, never drawn, and a texture
# on it would only load a layer for nothing.
_ok("every voussoir textured, the clearance volume not", tex_ok)
_ok("every voussoir is a real solid, not an empty plane set", solid)
# AT THE DEFAULT SIZE TOO, which is where this shipped broken. Thickness was a
# fixed grid step and the span radius is _size_steps/2 of them, so at 2 the ring
# was as thick as its own radius, the degenerate branch fired, and an arch came
# out a cube — while still reporting "created arch". Caught in a play session,
# not here, because every assertion above ran at 4.
bx._size_steps = 2
bx.update_preview()
_eqi("an arch at the DEFAULT size is still a ring", (bx._pieces() as Array).size(),
CreateBoxOperator.ARCH_SEGMENTS)
# The old "it says how thick it is" guarded a ring that silently became a CUBE. The
# tunnel cannot: its wall is clamped under half the bore (param_tunnel) and its
# Wall width is shown in the tool's settings. What stays worth asserting is that
# the status line is readable — no format specifiers leaked into it.
_ok("and the status line reads cleanly (%s)" % bx.status(), not bx.status().contains("%"))
var wall_row := false
for d: Dictionary in bx.tool_settings():
wall_row = wall_row or String(d.get("id", "")) == "p.wall"
_ok("and the wall thickness is a visible setting", wall_row)
bx._shape = CreateBoxOperator.Shape.BOX
stub.clear_ray()
print("\n-- AIM-FOLLOWS: the crosshair IS the pointer --")
# `[` and `]` are a stepper, not a drag: you cannot see where a face is going while
# you tap it there. Blender's G follows the pointer; here the mouse IS the camera,
# so the pointer is the crosshair ray and the value is where that ray comes closest
# to the constraint axis.
var probe := LevelOperator.new()
probe.level = lvl
probe.map = stub
add_child(probe)
print("\n the closest-approach value itself --")
stub.aim_ray(Vector3(0, 0, 10), Vector3(0, 0, -1))
_ok("a ray through the grab point reads zero — no jump on activation",
absf(probe.aim_along(Vector3.ZERO, Vector3.RIGHT)) < 1e-6)
stub.aim_ray(Vector3(5, 0, 10), Vector3(0, 0, -1))
_ok("look 5 m along X and the value is 5 (%.4f)"
% probe.aim_along(Vector3.ZERO, Vector3.RIGHT),
absf(probe.aim_along(Vector3.ZERO, Vector3.RIGHT) - 5.0) < 1e-5)
_ok("it is measured from the GRAB point, not the world origin",
absf(probe.aim_along(Vector3(2, 0, 0), Vector3.RIGHT) - 3.0) < 1e-5)
stub.aim_ray(Vector3(-10, 0, 0), Vector3(1, 0, 0))
_ok("looking ALONG the axis is NAN, not a huge number",
is_nan(probe.aim_along(Vector3.ZERO, Vector3.RIGHT)))
_ok("and aim_steps holds the caller's value there",
probe.aim_steps(Vector3.ZERO, Vector3.RIGHT, 7) == 7)
print("\n move follows the aim --")
stub.records.clear()
var drag_slab: int = _slab(lvl, 100.0)
stub.aim_brush = drag_slab
stub.aim_point = Vector3(104, 2, 1)
stub.aim_normal = Vector3.BACK
stub.clear_ray()
var mv2 := MoveOperator.new()
mv2.level = lvl
mv2.map = stub
add_child(mv2)
_ok("activates", stack.activate(mv2))
mv2.update_preview()
_ok("nothing moves while you are still looking where you grabbed",
mv2.status().contains("+0"))
# Look a metre and a half further along X. 1.5 m is 6.7 grid steps at 8 units, so
# it lands on 7 — the drag is continuous, the RESULT is on the lattice.
stub.aim_ray(Vector3(104, 2, 11) + Vector3(1.5, 0, 0), Vector3(0, 0, -1))
mv2.update_preview()
var want_steps: int = roundi(1.5 / (8.0 * 0.028))
_ok("the brush follows the aim, snapped to %d steps (%s)" % [want_steps, mv2.status()],
mv2.status().contains("%+d" % want_steps))
var x0: float = _min_x(lvl, drag_slab)
_ok("confirm moves it there", mv2.confirm())
_ok("by the distance we were looking at (%.3f)" % (_min_x(lvl, drag_slab) - x0),
absf((_min_x(lvl, drag_slab) - x0) - float(want_steps) * stub.step()) < 1e-3)
_eqi("one record", stub.records.size(), 1)
print("\n `[` and `]` still ride on top — the fallback for the parallel case --")
stub.records.clear()
stub.aim_point = Vector3(104, 2, 1)
stub.clear_ray()
_ok("activates", stack.activate(mv2))
mv2.handle_key(_key(KEY_BRACKETRIGHT))
mv2.handle_key(_key(KEY_BRACKETRIGHT))
mv2.update_preview()
_ok("two keyed steps with the aim at zero (%s)" % mv2.status(),
mv2.status().contains("+2"))
stub.aim_ray(Vector3(104, 2, 11) + Vector3(1.5, 0, 0), Vector3(0, 0, -1))
mv2.update_preview()
_ok("and they ADD to the aim rather than replacing it",
mv2.status().contains("%+d" % (want_steps + 2)))
stack.cancel()
print("\n extrude follows the aim along the face normal --")
stub.records.clear()
var ex_slab: int = _slab(lvl, 120.0)
stub.aim_brush = ex_slab
stub.aim_point = Vector3(124, 2, 1) # the +z face
stub.aim_normal = Vector3.BACK
stub.clear_ray()
var ex2 := ExtrudeOperator.new()
ex2.level = lvl
ex2.map = stub
add_child(ex2)
_ok("activates on a face", stack.activate(ex2))
# HEAD ON is the degenerate case, and it is the ordinary way to look at a wall:
# the face normal points at your eye, so aiming carries no depth. It must say so
# rather than twitch.
ex2.update_preview()
_ok("looking straight at the face, the aim is refused (%s)" % ex2.status(),
ex2.status().to_lower().contains("across"))
_ok("and `[` `]` still work there", ex2.handle_key(_key(KEY_BRACKETRIGHT)))
# From the side, where there IS depth information, it follows.
stub.aim_ray(Vector3(124, 2, 1) + Vector3(0, 10, 0.7), Vector3(0, -1, 0))
ex2.update_preview()
_ok("from an oblique angle it follows the aim (%s)" % ex2.status(),
not ex2.status().to_lower().contains("across"))
stack.cancel()
print("\n knife: AXIS mode — the control, unchanged --")
stub.records.clear()
var kn := KnifeOperator.new()
kn.level = lvl
kn.map = stub
add_child(kn)
var slab: int = _slab(lvl, 20.0)
stub.aim_brush = slab
stub.aim_point = Vector3(24, 2, 1)
stub.aim_normal = Vector3.BACK
var n_before: int = lvl.get_brush_count()
_ok("activates on a brush", stack.activate(kn))
_ok("no points placed yet, so it is in axis mode", kn.points().is_empty())
_ok("confirm cuts", kn.confirm())
_eqi("one more brush", lvl.get_brush_count(), n_before + 1)
_eqi("one record", stub.records.size(), 1)
_ok("a brush.split", String(stub.records[0]["kind"]) == "brush.split")
_ok("on a world axis", _rec_n(stub.records[0]) == Vector3.RIGHT)
stack.cancel()
print("\n knife: THREE placed points define the plane --")
# The capability the axis knife does not have: a plane that is not axis-aligned.
# Points on DIFFERENT faces, because three points on one face are coplanar with it
# and define a cut that does not divide the solid.
stub.records.clear()
var slab3: int = _slab(lvl, 40.0)
stub.aim_brush = slab3
stub.aim_point = Vector3(41, 1, 1)
stub.aim_normal = Vector3.BACK
_ok("activates", stack.activate(kn))
_ok("place point 1", kn.handle_key(_key(KEY_P)))
stub.aim_point = Vector3(46, 1, 1)
_ok("place point 2", kn.handle_key(_key(KEY_P)))
stub.aim_point = Vector3(43, 4, 0.5)
stub.aim_normal = Vector3.UP # the top face
_ok("place point 3", kn.handle_key(_key(KEY_P)))
_eqi("three points", kn.points().size(), 3)
_ok("the status says so", kn.status().contains("3"))
# THE AIM MOVES AWAY. A free plane comes from the POINTS; if it still tracked the
# crosshair the cut would change under you between placing and confirming.
stub.aim_point = Vector3(47.9, 3.9, 0.9)
kn.update_preview()
var n3: int = lvl.get_brush_count()
_ok("confirm cuts", kn.confirm())
_eqi("one more brush", lvl.get_brush_count(), n3 + 1)
_eqi("one record", stub.records.size(), 1)
_ok("a brush.split", String(stub.records[0]["kind"]) == "brush.split")
_ok("NOT axis-aligned — this is the new capability",
_axis_aligned(_rec_n(stub.records[0])) == false)
_ok("the durable plane passes through all three points (%.6f)"
% _max_off_plane(stub.records[0], kn.points()),
_max_off_plane(stub.records[0], kn.points()) < 1e-4)
stack.cancel()
print("\n knife: TWO points + the help axis — a wedge IS a box with a diagonal cut --")
# TrenchBroom's rule, from the checkout: with two points the third is inferred
# along a help vector, the dominant axis of the face the points were placed on
# (ClipToolController.cpp:236-248 selectHelpVectors, ClipTool.cpp:163-174
# computeThirdPoint). Two points on a wall therefore cut it perpendicular to that
# wall — which is the cut a ramp needs.
stub.records.clear()
var slab2: int = _slab(lvl, 60.0)
stub.aim_brush = slab2
stub.aim_normal = Vector3.BACK
stub.aim_point = Vector3(60.5, 2.2, 1)
_ok("activates", stack.activate(kn))
_ok("place point 1", kn.handle_key(_key(KEY_P)))
stub.aim_point = Vector3(64.5, 0.6, 1)
_ok("place point 2", kn.handle_key(_key(KEY_P)))
_eqi("two points", kn.points().size(), 2)
_ok("confirm cuts", kn.confirm())
_eqi("one record", stub.records.size(), 1)
_ok("the plane passes through both points",
_max_off_plane(stub.records[0], kn.points()) < 1e-4)
_ok("and is parallel to the help axis (n.z = %.6f)" % _rec_n(stub.records[0]).z,
absf(_rec_n(stub.records[0]).z) < 1e-5)
# THE RAMP, measured: a corner cut leaves a triangular prism (5 faces) and a
# pentagonal one (7). Six-and-six would mean the cut ran wall to wall instead.
var halves: Array = _split_face_counts(lvl, slab2, String(stub.records[0]["dot"]))
_eqi("the halves are a wedge and its complement (low)", int(halves[0]), 5)
_eqi(" (high)", int(halves[1]), 7)
stack.cancel()
print("\n knife: a plane that cannot exist is REFUSED, and writes nothing --")
stub.records.clear()
var slab4: int = _slab(lvl, 80.0)
stub.aim_brush = slab4
stub.aim_normal = Vector3.BACK
stub.aim_point = Vector3(84, 2, 1)
_ok("activates", stack.activate(kn))
_ok("place point 1", kn.handle_key(_key(KEY_P)))
# The KEY is still the knife's — a refused point must not fall through to whatever
# else P means. What is refused is the POINT.
_ok("a duplicate press is still claimed", kn.handle_key(_key(KEY_P)))
_eqi("but no second point was placed", kn.points().size(), 1)
# Stacked along the help axis: the inferred third point is colinear with the other
# two, so there is no plane through them at all.
stub.aim_point = Vector3(84, 2, 0)
stub.aim_normal = Vector3.FORWARD # the back face, same dominant axis
_ok("place point 2, straight through the slab", kn.handle_key(_key(KEY_P)))
var n4: int = lvl.get_brush_count()
_ok("confirm refuses", not kn.confirm())
_eqi("no record", stub.records.size(), 0)
_eqi("and no brush", lvl.get_brush_count(), n4)
_ok("the status says why", kn.status().to_lower().contains("cannot"))
stack.cancel()
print("\n knife: Backspace takes back the last point --")
stub.aim_point = Vector3(84, 2, 1)
stub.aim_normal = Vector3.BACK
stack.activate(kn)
kn.handle_key(_key(KEY_P))
_eqi("one point", kn.points().size(), 1)
_ok("backspace is claimed", kn.handle_key(_key(KEY_BACKSPACE)))
_eqi("and it is gone", kn.points().size(), 0)
_ok("backspace with no points is not claimed — it belongs to the caller",
not kn.handle_key(_key(KEY_BACKSPACE)))
stack.cancel()
_ok("cancelling clears the points, so the next cut starts clean",
kn.points().is_empty())
print("\n CANCEL WRITES NOTHING — the property that matters --")
stub.records.clear()
var count2: int = lvl.get_brush_count()
stack.activate(bx)
stack.cancel()
_eqi("no record after a cancelled create", stub.records.size(), 0)
_eqi("and no brush", lvl.get_brush_count(), count2)
stack.activate(mv)
mv.handle_key(_key(KEY_BRACKETRIGHT))
var x_before: float = _min_x(lvl, wall)
stack.cancel()
_eqi("no record after a cancelled move", stub.records.size(), 0)
_ok("and the brush did not move", absf(_min_x(lvl, wall) - x_before) < 1e-6)
print("\n-- GRAB: dragging the SELECTED vertex/edge/face, end to end --")
# END TO END, and deliberately so: a real MapDCLevel in the tree, a real camera
# supplying the aim, a real selection, and the map's own apply_element_move. The
# stub proves an operator does the right thing when told; only this proves the
# selection routes to it, the camera reaches it, and the planes actually move.
Keymap.push_context("builder")
# The editor's tools are build-mode only since the 2026-10-01 key audit
# (`builder` requires `build_mode`), so this enters build mode as a user does.
Keymap.push_context("build_mode")
var map_script: GDScript = load("res://scenes/maps/map_dc_level.gd")
# NO DCLevel child when it enters the tree, so _ready takes its "no level" branch
# and never starts an import. The level is handed over afterwards.
var m: Node = map_script.new()
m.name = "GateMap"
add_child(m)
m.level = lvl
var rec := RecordingPersist.new()
m._persist = rec # NOT add_child: in the tree it would subscribe to
# GameState and re-fold the level under the gate
var chain := OperatorStack.new()
m._ops = chain
m.add_child(chain)
var grab_op := ElementOperator.new()
grab_op.level = lvl
grab_op.map = m
m.add_child(grab_op)
m._grab_op = grab_op
m._move = MoveOperator.new()
m._move.level = lvl
m._move.map = m
m.add_child(m._move)
var cam := Camera3D.new()
add_child(cam)
cam.current = true
# A face of a fresh slab, selected the way _pick_element leaves it.
var sel_slab: int = _slab(lvl, 140.0)
var sel_keys: PackedStringArray = lvl.get_brush(sel_slab).get("plane_keys", PackedStringArray())
m._sel_brush = sel_slab
m._sel_keys = PackedStringArray([sel_keys[1]]) # +x, at 148
m._sel_point = Vector3(148, 2, 0.5)
m._sel_kind = "face"
# Look at the face from off to one side, so the aim carries information along the
# +x normal we are about to drag.
cam.global_position = Vector3(148, 2, 12)
cam.look_at(Vector3(148, 2, 0.5), Vector3.UP)
await get_tree().process_frame
m._unhandled_key_input(_key(KEY_G))
_ok("G with a selection takes up GRAB, not Move",
chain.active != null and chain.active.op_name() == "grab")
grab_op.update_preview()
_ok("and it reads zero where you grabbed (%s)" % grab_op.status(),
grab_op.status().contains("+0"))
# Now look 1.5 m further out along +x. Same drag as the Move test, on one plane
# of a selection rather than a whole brush.
cam.global_position = Vector3(148 + 1.5, 2, 12)
cam.look_at(Vector3(148 + 1.5, 2, 0.5), Vector3.UP)
await get_tree().process_frame
grab_op.update_preview()
var want2: int = roundi(1.5 / (8.0 * 0.028))
_ok("the face follows the aim, %d steps (%s)" % [want2, grab_op.status()],
grab_op.status().contains("%+d" % want2))
var max_x: float = _max_x(lvl, sel_slab)
rec.written.clear()
_ok("confirm moves it", chain.confirm())
_ok("by the distance we were looking at (%.3f)" % (_max_x(lvl, sel_slab) - max_x),
absf((_max_x(lvl, sel_slab) - max_x) - float(want2) * 8.0 * 0.028) < 1e-3)
_eqi("one face.set record for the one selected plane", rec.written.size(), 1)
_ok("of the right kind", String(rec.written[0]["kind"]) == "face.set")
print("\n nothing selected -> G is NOT the element drag --")
m._sel_brush = -1
m._sel_keys = PackedStringArray()
m._unhandled_key_input(_key(KEY_G))
# Asserted as "not grab" rather than "is move": with no selection G routes to the
# brush move, which refuses unless the crosshair is on geometry — and whether a
# physics ray happens to hit here is not what this is testing.
_ok("G routes away from grab when nothing is selected",
chain.active == null or chain.active.op_name() != "grab")
chain.cancel()
print("\n a cancelled drag writes nothing, and moves nothing --")
m._sel_brush = sel_slab
m._sel_keys = PackedStringArray([sel_keys[1]])
m._sel_point = Vector3(_max_x(lvl, sel_slab), 2, 0.5)
rec.written.clear()
var held_x: float = _max_x(lvl, sel_slab)
m._unhandled_key_input(_key(KEY_G))
grab_op.update_preview()
m._unhandled_key_input(_key(KEY_BRACKETRIGHT))
chain.cancel()
_eqi("no record", rec.written.size(), 0)
_ok("and the face did not move", absf(_max_x(lvl, sel_slab) - held_x) < 1e-6)
print("\n VERTEX drag moves ONE corner, through the real map --")
# The C++ proves the store does this; this proves the editor asks it to. A vertex
# selection must reach move_brush_points and write a brush.reshape, NOT move the
# three incident planes — which is what every "vertex mode moves the whole brush"
# report was actually seeing.
var vslab: int = _slab(lvl, 240.0)
var vcorners: PackedVector3Array = lvl.get_brush_vertices_of(vslab)
_eqi("the slab has eight corners", vcorners.size(), 8)
var pick: Vector3 = vcorners[0]
m._sel_brush = vslab
m._sel_kind = "vertex"
m._sel_point = pick
# THE THREE INCIDENT PLANES, as a real pick produces — not all six. All six sum
# to zero outward direction (a box's normals cancel) and the operator rightly
# refuses that, which is what a wrong selection SHOULD do.
var vkeys: PackedStringArray = lvl.get_brush(vslab).get("plane_keys", PackedStringArray())
var vsel := PackedStringArray()
for pi: int in m._planes_through(vslab, pick, 1e-3):
if pi < vkeys.size():
vsel.append(vkeys[pi])
m._sel_keys = vsel
_eqi("a corner is three incident planes", vsel.size(), 3)
rec.written.clear()
_ok("the map offers that corner as a drag point",
m.element_drag_points().size() == 1)
cam.global_position = pick + Vector3(0, 0, 9)
cam.look_at(pick, Vector3.UP)
await get_tree().process_frame
m._unhandled_key_input(_key(KEY_G))
_ok("grab takes it", chain.active != null and chain.active.op_name() == "grab")
chain.active.handle_key(_key(KEY_X)) # constrain to X so the result is checkable
chain.active.handle_key(_key(KEY_BRACKETRIGHT))
_ok("confirm reshapes", chain.confirm())
_eqi("one record", rec.written.size(), 1)
_ok("and it is a brush.reshape, not a face.set",
String(rec.written[0]["kind"]) == "brush.reshape")
var vafter: PackedVector3Array = lvl.get_brush_vertices_of(vslab)
_eqi("still eight corners", vafter.size(), 8)
# THE ASSERTION. Seven of the original corners must be exactly where they were.
var kept := 0
for c: Vector3 in vcorners:
for d: Vector3 in vafter:
if c.distance_to(d) < 1e-3:
kept += 1
break
_eqi("SEVEN of the eight are untouched — only the dragged one moved", kept, 7)
print("\n left click confirms, right click cancels --")
# The gesture your hand is already making. Enter and Escape still work; these are
# what every editor ends a modal transform with.
m._sel_brush = sel_slab
m._sel_keys = PackedStringArray([sel_keys[1]])
m._sel_point = Vector3(_max_x(lvl, sel_slab), 2, 0.5)
cam.global_position = Vector3(_max_x(lvl, sel_slab) + 1.0, 2, 12)
cam.look_at(Vector3(_max_x(lvl, sel_slab) + 1.0, 2, 0.5), Vector3.UP)
await get_tree().process_frame
rec.written.clear()
m._unhandled_key_input(_key(KEY_G))
grab_op.update_preview()
_ok("a tool is up", chain.active != null)
m._unhandled_input(_click(MOUSE_BUTTON_RIGHT))
_ok("right click cancels it", chain.active == null)
_eqi("and wrote nothing", rec.written.size(), 0)
m._unhandled_key_input(_key(KEY_G))
grab_op.update_preview()
var pre_x: float = _max_x(lvl, sel_slab)
m._unhandled_input(_click(MOUSE_BUTTON_LEFT))
_ok("left click confirms and leaves", chain.active == null)
_ok("the face moved", absf(_max_x(lvl, sel_slab) - pre_x) > 1e-4)
_eqi("one record", rec.written.size(), 1)
# WITH NO TOOL UP both buttons mean exactly what they always meant — nothing is
# taken away from play.
rec.written.clear()
var idle_x: float = _max_x(lvl, sel_slab)
m._unhandled_input(_click(MOUSE_BUTTON_LEFT))
_eqi("a click with no tool up writes nothing", rec.written.size(), 0)
_ok("and changes nothing", absf(_max_x(lvl, sel_slab) - idle_x) < 1e-6)
print("\n left click SELECTS when no tool is up --")
GameMode.set_mode(GameMode.BUILD)
m._sel_brush = -1
m._sel_keys = PackedStringArray()
m._sel_mode = "face"
cam.global_position = Vector3(_max_x(lvl, sel_slab) - 4.0, 2, 12)
cam.look_at(Vector3(_max_x(lvl, sel_slab) - 4.0, 2, 0.5), Vector3.UP)
await get_tree().process_frame
m._unhandled_input(_click(MOUSE_BUTTON_LEFT))
_ok("a click with nothing selected picks an element", m._sel_brush >= 0)
_ok("and it is the mode's kind, not a guess", m._sel_kind == "face")
GameMode.set_mode(GameMode.PLAY)
m._sel_brush = -1
m._unhandled_input(_click(MOUSE_BUTTON_LEFT))
_ok("but NOT while playing — there a left click is the grapple",
m._sel_brush < 0)
GameMode.set_mode(GameMode.BUILD)
print("\n G moves what is SELECTED, not what is aimed --")
# Select one brush, aim at ANOTHER, press G. It must move the selected one.
var other: int = _slab(lvl, 200.0)
m._sel_brush = other
m._sel_keys = lvl.get_brush(other).get("plane_keys", PackedStringArray())
m._sel_point = Vector3(204, 2, 0.5)
m._sel_kind = "brush"
cam.global_position = Vector3(_max_x(lvl, sel_slab) - 4.0, 2, 12)
cam.look_at(Vector3(_max_x(lvl, sel_slab) - 4.0, 2, 0.5), Vector3.UP)
await get_tree().process_frame
m._unhandled_key_input(_key(KEY_G))
_ok("G takes up move in brush mode",
chain.active != null and chain.active.op_name() == "move")
_ok("on the SELECTED brush, not the one under the crosshair",
chain.active._brush == other)
chain.cancel()
print("\n the selection is a SET with an ACTIVE member --")
# Issue #23 item 4, and the prerequisite for every CSG op: "subtract THIS from
# THAT" cannot be expressed with one selected brush.
GameMode.set_mode(GameMode.BUILD)
m._sel_mode = "brush"
m._clear_face_selection()
var s1: int = _slab(lvl, 300.0)
var s2: int = _slab(lvl, 320.0)
var s3: int = _slab(lvl, 340.0)
m._note_selected(s1, false)
_eqi("a plain pick selects one", m._sel_brushes.size(), 1)
_ok("and it is active", m._sel_brush == s1)
m._note_selected(s2, true)
m._note_selected(s3, true)
_eqi("shift extends", m._sel_brushes.size(), 3)
_ok("the LAST picked is active — the only rule to remember", m._sel_brush == s3)
m._note_selected(s2, true)
_eqi("shift-clicking a selected brush removes it", m._sel_brushes.size(), 2)
_ok("active is untouched when another is removed", m._sel_brush == s3)
m._note_selected(s3, true)
_eqi("removing the ACTIVE one leaves the rest", m._sel_brushes.size(), 1)
_ok("and hands active to what remains", m._sel_brush == s1)
m._note_selected(s2, false)
_eqi("a plain pick REPLACES the whole set", m._sel_brushes.size(), 1)
_ok("with the new one active", m._sel_brush == s2)
m._clear_face_selection()
_eqi("clearing empties the set", m._sel_brushes.size(), 0)
_ok("and there is no active brush", m._sel_brush == -1)
print("\n CSG: the ACTIVE brush against the others --")
# Blender's model, which is what the selection set exists for: last clicked is the
# survivor and the target, the rest are operands and are consumed.
m._sel_mode = "brush"
m._clear_face_selection()
var cw: int = _slab(lvl, 400.0) # the wall 400..408
var cc: int = lvl.add_brush(_box_planes(Vector3(403, -1, -1), Vector3(405, 3, 2)),
7, PackedInt32Array(), 0, false, "")
lvl.rebuild()
var before_n: int = lvl.get_brush_count()
m._note_selected(cc, false) # the cutter first
m._note_selected(cw, true) # the WALL last, so active
_ok("the wall is active, the cutter is an operand", m._sel_brush == cw)
rec.written.clear()
m._csg("subtract")
_ok("the doorway is open", not _solid_at(lvl, Vector3(404, 1, 0.5)))
_ok("and the wall either side is not",
_solid_at(lvl, Vector3(401, 1, 0.5)) and _solid_at(lvl, Vector3(407, 1, 0.5)))
_ok("the cutter was consumed", not _solid_at(lvl, Vector3(404, 1, 1.5)))
_ok("records were written", rec.written.size() >= 2)
var kinds := ""
for r2: Dictionary in rec.written:
kinds += String(r2["kind"]) + " "
_ok("including a brush.subtract (%s)" % kinds.strip_edges(),
kinds.contains("brush.subtract"))
_ok("and a brush.remove for the consumed cutter", kinds.contains("brush.remove"))
_ok("the selection was cleared — it names geometry that is gone",
m._sel_brushes.is_empty())
_ok("more brushes than before, because a doorway is pieces",
lvl.get_brush_count() > before_n - 1)
print("\n CSG: discarding the cutter is an OPTION (menu: Q / RMB / F3) --")
# User 2026-10-05: "discard should be an option, there are times when you do and do
# not want to discard". Kept, the cutter is still level geometry — it stands in the
# hole it cut until you move it, which is why it comes back SELECTED.
# MENU-ONLY since 2026-10-12 (Alt+H is Blender's unhide all, cutting-and-operations
# §9c): a pick arrives from Keymap.run as an InputEventAction, through _unhandled_input.
var csg_dock: Node = get_tree().root.get_node_or_null("/root/BuildDock")
var saved_discard: Variant = csg_dock.pref("csg_discard", true) if csg_dock != null else null
m.csg_discard = true
Keymap.push_context("build_mode")
var discard_pick := InputEventAction.new()
discard_pick.action = "build_mode.csg_discard"
discard_pick.pressed = true
m._unhandled_input(discard_pick)
_ok("the menu's discard toggle turns discarding OFF", not m.csg_discard)
_ok("and it is remembered as a pref",
csg_dock == null or not bool(csg_dock.pref("csg_discard", true)))
rec.written.clear()
m._unhandled_key_input(_key(KEY_H))
_eqi("plain H is hide now, not the toggle (nothing selected = no records)",
rec.written.size(), 0)
_ok("and did not flip the option back", not m.csg_discard)
Keymap.pop_context("build_mode")
m._clear_face_selection()
var kw: int = _slab(lvl, 440.0) # the wall 440..448
var kc: int = lvl.add_brush(_box_planes(Vector3(443, -1, -1), Vector3(445, 3, 2)),
7, PackedInt32Array(), 0, false, "")
lvl.rebuild()
m._note_selected(kc, false)
m._note_selected(kw, true)
rec.written.clear()
m._csg("subtract")
_ok("the wall is cut where the cutter was",
not _solid_at(lvl, Vector3(444, 1, 0.5), kc))
_ok("and the wall either side is not",
_solid_at(lvl, Vector3(441, 1, 0.5)) and _solid_at(lvl, Vector3(447, 1, 0.5)))
_ok("the cutter is KEPT", _solid_at(lvl, Vector3(444, 1, 1.5)))
var kinds_k := ""
for r3: Dictionary in rec.written:
kinds_k += String(r3["kind"]) + " "
_ok("a brush.subtract and NO brush.remove (%s)" % kinds_k.strip_edges(),
kinds_k.contains("brush.subtract") and not kinds_k.contains("brush.remove"))
_ok("the kept cutter is what is selected now, ready to move",
m._sel_brushes.size() == 1 and m._sel_brush == kc)
m._clear_face_selection()
var iw: int = _slab(lvl, 460.0) # intersect keeps its operand too
var ic: int = lvl.add_brush(_box_planes(Vector3(463, -1, -1), Vector3(465, 3, 2)),
7, PackedInt32Array(), 0, false, "")
lvl.rebuild()
m._note_selected(ic, false)
m._note_selected(iw, true)
m._csg("intersect")
_ok("intersect narrowed the active brush", not _solid_at(lvl, Vector3(461, 1, 0.5)))
_ok("and kept the operand", _solid_at(lvl, Vector3(464, 1, 1.5)))
m.csg_discard = true
if csg_dock != null:
csg_dock.set_pref("csg_discard", saved_discard)
print("\n CSG refuses a selection it cannot act on --")
m._clear_face_selection()
rec.written.clear()
m._note_selected(cw, false)
m._csg("subtract")
_eqi("one selected brush is not a CSG operation", rec.written.size(), 0)
print("\n select modes: 1 / 2 / 3 / 4 --")
Keymap.push_context("build_mode")
for pair: Array in [[KEY_1, "vertex"], [KEY_2, "edge"], [KEY_3, "face"], [KEY_4, "brush"]]:
m._unhandled_key_input(_key(int(pair[0])))
_ok("%s selects %s mode" % [OS.get_keycode_string(int(pair[0])), String(pair[1])],
m._sel_mode == String(pair[1]))
_ok("switching mode clears the selection — it is not silently converted",
m._sel_brush < 0)
# The number row is only free BECAUSE build_mode is exclusive. Assert the thing
# that makes it safe, not just that the binding exists.
_ok("the weapon row is suppressed while building",
not Keymap.is_context_active("player"))
Keymap.pop_context("build_mode")
_ok("and OUT of build mode the mode keys are inert again",
not Keymap.tapped(_key(KEY_1), "build_mode.sel_vertex"))
Keymap.push_context("build_mode") # back in: the tools below are build-mode only
print("\n-- FIRST REFUSAL, against the REAL map dispatch --")
# Every operator adjusts itself with `[` and `]`, and every one of them was tested
# by calling handle_key DIRECTLY — which is exactly the seam issue #23 named: the
# gate proved the operator does the right thing WHEN TOLD, never that it gets told.
# The map is what tells it, so the map is what this drives.
var ck := CountingKeyOperator.new()
m.add_child(ck)
_ok("the operator is active", chain.activate(ck))
m._unhandled_key_input(_key(KEY_BRACKETRIGHT))
_eqi("`]` reached the active operator", ck.keys, 1)
m._unhandled_key_input(_key(KEY_BRACKETLEFT))
_eqi("`[` too", ck.keys, 2)
# The other half of the contract: what an operator does NOT claim must still fall
# through, or taking up a tool would strand you.
var grid_before: int = m._grid_idx
# The grid is a remembered per-player pref since E2b and this gate shares the
# installed game's user:// — put it back afterwards (it leaked a 16u grid).
var dock: Node = get_tree().root.get_node_or_null("/root/BuildDock")
var saved_grid: Variant = dock.pref("grid", 5) if dock != null else null
m._unhandled_key_input(_key(KEY_EQUAL))
_ok("an unclaimed key still falls through to the map",
m._grid_idx != grid_before)
chain.cancel()
var idx_after: int = m._grid_idx
m._unhandled_key_input(_key(KEY_BRACKETRIGHT))
_eqi("with no operator up, `]` is the selection nudge again — not the operator",
ck.keys, 2)
_ok("and the grid was not touched", m._grid_idx == idx_after)
if dock != null:
dock.set_pref("grid", saved_grid)
m.free()
rec.free()
cam.free()
Keymap.pop_context("build_mode")
Keymap.pop_context("builder")
if pref_dock != null:
pref_dock.set_pref("snap", saved_snap)
print("\n%s" % ("FAILED" if fails else "PASS"))
get_tree().quit(1 if fails else 0)
func _key(code: int) -> InputEventKey:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.pressed = true
return e
## Is any live brush solid at this point? The honest way to check a CSG result: a
## face count says the shape changed, not that it changed into the right shape.
## `skip`: a brush to ignore — a KEPT cutter fills the hole it cut, so "is the wall cut
## here" has to ask about everything but the cutter.
func _solid_at(lvl: Node, p: Vector3, skip: int = -1) -> bool:
for i in range(0, 4000):
if i == skip:
continue
var b: Dictionary = lvl.get_brush(i)
if b.is_empty():
continue
# TOMBSTONED BRUSHES ARE NOT SOLID, and get_brush returns them anyway — a
# tombstone is a value, not a deletion, precisely so undo can bring it back.
# Counting them as solid made a consumed cutter look like it was still there
# and a cut doorway look filled.
if bool(b.get("tombstoned", false)):
continue
var pl: PackedFloat32Array = b.get("planes", PackedFloat32Array())
if pl.is_empty():
continue
var inside := true
for f in range(pl.size() / 4):
var n := Vector3(pl[f * 4], pl[f * 4 + 1], pl[f * 4 + 2])
if n.dot(p) > pl[f * 4 + 3] + 1e-4:
inside = false
break
if inside:
return true
return false
func _click(button: int) -> InputEventMouseButton:
var e := InputEventMouseButton.new()
e.button_index = button
e.pressed = true
# WHERE the click happened, because in build mode that is what the pick aims
# along. A synthetic event leaves this at (0,0) — the top-left corner — so the
# ray left the camera sideways and hit nothing, and "a click picks an element"
# failed on a fixture that never aimed at anything. The centre of the viewport
# is what every camera in this gate is pointed at.
var vp := get_viewport()
if vp != null:
e.position = vp.get_visible_rect().size * 0.5
return e
## Is a point inside the solid a plane set bounds? The convention is `n · x <= d`,
## so a point outside ANY plane is outside the solid.
func _inside(planes: PackedFloat32Array, p: Vector3, eps: float = 1.0e-4) -> bool:
for i in range(0, planes.size(), 4):
var n := Vector3(planes[i], planes[i + 1], planes[i + 2])
if n.dot(p) > planes[i + 3] + eps:
return false
return true
## Recover [lo, hi] from an AXIS-ALIGNED plane set, so a step can be measured
## without reaching into the operator for numbers it has already turned into planes.
func _plane_box(planes: PackedFloat32Array) -> Array:
var lo := Vector3.ZERO
var hi := Vector3.ZERO
for i in range(0, planes.size(), 4):
var n := Vector3(planes[i], planes[i + 1], planes[i + 2])
var d: float = planes[i + 3]
for a in 3:
if absf(n[a]) > 0.999:
if n[a] < 0.0:
lo[a] = -d
else:
hi[a] = d
return [lo, hi]
func _box_planes(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
return PackedFloat32Array([
-1, 0, 0, -lo.x, 1, 0, 0, hi.x,
0, -1, 0, -lo.y, 0, 1, 0, hi.y,
0, 0, -1, -lo.z, 0, 0, 1, hi.z])
## A fresh 8 x 4 x 1 box at x0, so each cut's face counts belong to that cut alone.
func _slab(lvl: Node, x0: float) -> int:
var id: int = lvl.add_brush(_box_planes(Vector3(x0, 0, 0), Vector3(x0 + 8.0, 4, 1)),
7, PackedInt32Array(), 0, false, "")
lvl.rebuild()
return id
## The plane a record actually carries — the durable thing that replicates, rather
## than whatever the operator happens to hold in memory.
func _rec_n(rec: Dictionary) -> Vector3:
var n: Array = rec["n"]
return Vector3(float(n[0]), float(n[1]), float(n[2]))
func _axis_aligned(n: Vector3) -> bool:
var nonzero := 0
for a in 3:
if absf(n[a]) > 1e-5:
nonzero += 1
return nonzero == 1
## How far the furthest placed point sits off the plane that was committed. Zero is
## the whole claim of a placed-point cut: the plane goes THROUGH the points.
func _max_off_plane(rec: Dictionary, pts: PackedVector3Array) -> float:
var n := _rec_n(rec)
var d := float(rec["d"])
var worst := 0.0
for p: Vector3 in pts:
worst = maxf(worst, absf(n.dot(p) - d))
return worst
## The two halves of a split, by live face count, low first.
func _split_face_counts(lvl: Node, original: int, dot: String) -> Array:
var made: int = lvl.brush_by_key(dot)
var a: int = int((lvl.get_brush(original) as Dictionary).get("live_faces", 0))
var b: int = int((lvl.get_brush(made) as Dictionary).get("live_faces", 0))
return [mini(a, b), maxi(a, b)]
func _max_x(lvl: Node, brush: int) -> float:
var hi := -1e30
for p: Vector3 in lvl.get_brush_edges_of(brush):
hi = maxf(hi, p.x)
return hi
func _min_x(lvl: Node, brush: int) -> float:
var lo := 1e30
for p: Vector3 in lvl.get_brush_edges_of(brush):
lo = minf(lo, p.x)
return lo
## Exactly what an operator reaches for, and nothing else — which is itself a useful
## check on the framework: if this stub has to grow, an operator is reaching too far
## into the map.
class StubMap extends Node:
const GRID_UNITS: Array[int] = [1, 2, 4, 8, 16, 32, 64]
const WORLD_SCALE := 0.028
var _grid_idx := 3
var _map_center := Vector3.ZERO
var _aligned_offset := Vector3.ZERO
var level: Node = null
var aim_brush := -1
var aim_point := Vector3.ZERO
var aim_normal := Vector3.UP
# The selection, because G now moves what is SELECTED rather than what is aimed.
# The stub growing is the signal it is meant to be: an operator reached further
# into the map, and that is worth seeing rather than hiding behind a duck-type.
var _sel_brush := -1
var _sel_keys := PackedStringArray()
var _sel_point := Vector3.ZERO
var _sel_kind := ""
var records: Array = []
var _persist: StubPersist = null
# NOT the pack default, so the gate proves the operator asks the map.
const CURRENT_TEX := "gate/current"
func current_face_tex() -> Dictionary:
return {"tex": CURRENT_TEX, "mode": 1}
func _ensure_persistence() -> void:
pass
func _init() -> void:
_persist = StubPersist.new()
_persist.owner_map = self
func step() -> float:
return float(GRID_UNITS[_grid_idx]) * WORLD_SCALE
func _crosshair_hit() -> Dictionary:
if aim_brush < 0:
return {}
return {"position": aim_point, "normal": aim_normal, "shape": 0}
# THE RAY, which is what an aim-following operator actually reads. Unset, it
# points straight at aim_point from 10 m away — the "you have not looked away
# yet" case, where every drag must read zero.
var ray_from := Vector3.ZERO
var ray_dir := Vector3.FORWARD
var _ray_set := false
func aim_ray(from: Vector3, dir: Vector3) -> void:
ray_from = from
ray_dir = dir.normalized()
_ray_set = true
func clear_ray() -> void:
_ray_set = false
func _crosshair_ray() -> Dictionary:
if _ray_set:
return {"from": ray_from, "dir": ray_dir}
return {"from": aim_point + Vector3(0, 0, 10), "dir": Vector3(0, 0, -1)}
func _brush_from_hit(hit: Dictionary) -> int:
return aim_brush if not hit.is_empty() else -1
func _refresh_brush_view() -> void:
pass
static func snap_to_map_grid(d: float, _n: Vector3, units: int,
_mc: Vector3, _ao: Vector3) -> float:
var s: float = float(units) * WORLD_SCALE
return roundf(d / s) * s
class StubPersist extends RefCounted:
var owner_map = null
var _n := 0
func reserve_dot() -> String:
_n += 1
return "gate:%d" % _n
func record(kind: String, payload: Dictionary) -> String:
var dot := reserve_dot()
record_at(dot, kind, payload)
return dot
func record_at(dot: String, kind: String, payload: Dictionary) -> bool:
var r: Dictionary = payload.duplicate()
r["kind"] = kind
r["dot"] = dot
owner_map.records.append(r)
return true
func begin_gesture(_label: String = "") -> void:
pass
func end_gesture() -> void:
pass
# THE CREATE TOOL WRITES shape.create (parametric-shapes-design §13). Refold runs the
# REAL ShapeFold on each one not yet built and adds its pieces to the REAL DCLevel by
# the fold's own names, so the store still has to accept every generated plane set.
var _built: Dictionary = {}
func refold() -> void:
var ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
for r: Dictionary in owner_map.records:
var dot := String(r["dot"])
if String(r["kind"]) != "shape.create" or _built.has(dot):
continue
_built[dot] = true
var sz: Array = r.get("size", [1, 1, 1])
var tex: Variant = r.get("tex", {})
var rec := {"author": dot.get_slice(":", 0), "seq": int(dot.get_slice(":", 1)), "kind": "shape.create",
"type": r["type"], "gv": r["gv"], "params": r["params"],
"frame": SceneObjects.xform_from(r["frame"]), "size": Vector3(sz[0], sz[1], sz[2]),
"material_id": r.get("material_id", 0), "level_id": r.get("level_id", 0),
"tex": LevelPersistence._tex_rec(tex) if tex is Dictionary and not (tex as Dictionary).is_empty() else {}}
for b: Dictionary in ShapeFold.expand([rec]):
owner_map.level.add_brush(b["planes"], int(b["material_id"]), PackedInt32Array(), int(b["level_id"]),
false, "%s:%d" % [b["author"], b["seq"]], bool(b["clearance"]), String(b["group"]), b["face_tex"])
owner_map.level.rebuild()
## A real LevelPersistence, with the durable write replaced by a list. The map calls
## it through the same `record` / `record_at` the game does, so what the gate sees is
## the payload that WOULD have replicated.
class RecordingPersist extends LevelPersistence:
var written: Array = []
var _n := 0
func reserve_dot() -> String:
_n += 1
return "gate:%d" % _n
func record(kind: String, payload: Dictionary) -> String:
var dot := reserve_dot()
record_at(dot, kind, payload)
return dot
func record_at(dot: String, kind: String, payload: Dictionary) -> bool:
var r: Dictionary = payload.duplicate()
r["kind"] = kind
r["dot"] = dot
written.append(r)
return true
func is_applying() -> bool:
return false
class RefusingOperator extends LevelOperator:
func on_activate() -> bool:
return false
## Claims `[` and `]` and counts them — the two keys every real operator adjusts
## itself with, and therefore the two the map must hand it before anything else.
class CountingKeyOperator extends LevelOperator:
var keys := 0
func op_name() -> String:
return "countkeys"
func handle_key(event: InputEvent) -> bool:
if not (event is InputEventKey) or not (event as InputEventKey).pressed:
return false
var kc: int = (event as InputEventKey).keycode
if kc != KEY_BRACKETLEFT and kc != KEY_BRACKETRIGHT:
return false
keys += 1
return true
class CountingOperator extends LevelOperator:
var confirms := 0
var deactivations := 0
func op_name() -> String:
return "counting"
func on_deactivate() -> void:
deactivations += 1
func confirm() -> bool:
confirms += 1
return true