OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / operator_gate.gd

1338 lines · gd ref:
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