extends SceneTree
## Gate — parametric shapes (docs/parametric-shapes-design.md §9).
##
## Generators (gate 7): every type, over a grid of parameters, yields pieces the REAL
## brush store accepts (it refuses a plane set that encloses nothing), with the
## expected piece counts.
## Fold (gates 1-3, 5): records -> synthetic brush.create records
## 1. deterministic: the same records expand to identical output
## 2. shape.set regenerates under the same shape name (steps 4 -> 8), undo -> 4
## 3. per-parameter last-write-wins by (seq, author)
## 4. shape.xform moves every piece
## 5. a live shape.detach removes the shape's pieces
## 6. names are "<author>/<i>:<seq>", group is the shape's dot
const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
const ParamShapes := preload("res://scenes/maps/shapes/param/param_shapes.gd")
var ok := true
var _ran := false
var _finished := false
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
_p(_finished, "the gate ran to the end (no script error aborted it)")
print("GATE param_shapes: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1)
return true
## Pieces into a fresh DCLevel as plain brush.create records; returns
## [accepted, skipped] from the store.
func _store_accepts(pieces: Array) -> Array:
var lvl: Node = ClassDB.instantiate(&"DCLevel")
root.add_child(lvl)
var recs: Array = []
var i := 0
for pc: Dictionary in pieces:
i += 1
recs.append({"author": "gate", "seq": i, "kind": "brush.create", "undone": false,
"planes": pc["planes"], "proj": PackedInt32Array(), "material_id": 0, "level_id": 0,
"sky": false, "clearance": false, "group": "", "face_tex": []})
var st: Dictionary = lvl.apply_journal(recs)
lvl.queue_free()
return [int(st.get("applied", 0)), int(st.get("skipped", 0))]
func _check_type(id: String, size: Vector3, params: Dictionary, want: int, what: String) -> void:
var t: Object = ParamShapes.get_type(id)
if t == null:
_p(false, "%s: type registered" % id)
return
var p: Dictionary = t.resolve(params)
var pcs: Array = t.local_pieces(size, p)
var r := _store_accepts(pcs)
_p(pcs.size() == want and r[0] == want and r[1] == 0,
"%s %s: %d pieces, store accepted %d, refused %d (want %d)" % [id, what, pcs.size(), r[0], r[1], want])
func _run() -> void:
_p(ClassDB.class_exists(&"DCLevel"), "DCLevel (dc-terrain) is loaded")
# CONTROL: the check below means something only if the store REFUSES a plane set
# that encloses nothing. An inside-out box (lo above hi) must be refused.
var bad: Dictionary = ShapeKit.box_piece(Vector3(1, 1, 1), Vector3(0, 0, 0))
var rc := _store_accepts([bad])
_p(rc[0] == 0 and rc[1] == 1, "control: an inside-out box is refused (accepted %d, refused %d)" % [rc[0], rc[1]])
# ---- generators -------------------------------------------------------
var s := Vector3(6, 3, 2) # 6 m run, 3 m rise, 2 m wide
_check_type("stairs", s, {"steps": 10}, 10, "10 solid steps")
_check_type("stairs", s, {"steps": 10, "tread_thickness": 0.1}, 10, "floating slabs")
_check_type("stairs", s, {"steps": 6, "gap": 0.2}, 6, "open risers")
_check_type("stairs", s, {"steps": 5, "flights": 2, "landing_depth": 1.0}, 11, "2 flights + 1 landing")
_check_type("stairs", Vector3(6, 3, 3), {"steps": 5, "flights": 2, "landing_depth": 1.0, "turn": "u"},
11, "U-turn, 2 flights + 1 landing")
_check_type("stairs", s, {"steps": 0}, 15, "auto steps (3 m / 0.2 m)")
var sp := Vector3(4, 3, 4)
_check_type("spiral_stairs", sp, {"steps": 12, "sweep_deg": 270.0}, 12, "12 treads over 270°")
_check_type("spiral_stairs", sp, {"steps": 4, "sweep_deg": 360.0}, 8, "4 wide treads split ≤ 45°")
_check_type("spiral_stairs", sp, {"steps": 12, "center_column": true, "column_facets": 8},
12 + 1, "with a centre column")
_check_type("spiral_stairs", sp, {"steps": 12, "tread_thickness": 0.15}, 12, "floating treads")
var tu := Vector3(8, 4, 4)
_check_type("tunnel", tu, {"profile": "round", "facets": 8}, 8, "round, 8 facets")
_check_type("tunnel", tu, {"profile": "round", "facets": 12, "wall": 0.5, "spring": 1.0}, 12 + 2, "round on 1 m walls")
_check_type("tunnel", tu, {"profile": "pointed", "facets": 10}, 10, "pointed, 10 facets")
_check_type("tunnel", tu, {"profile": "flat"}, 3, "flat (2 walls + roof)")
_check_type("tunnel", tu, {"profile": "circle", "facets": 16}, 16, "circle, 16 facets")
_check_type("tunnel", tu, {"profile": "round", "facets": 8, "floor": true}, 9, "round + floor slab")
for t: String in ["stairs", "spiral_stairs", "tunnel"]:
var ty: Object = ParamShapes.get_type(t)
if ty != null:
var cl: Array = ty.local_clearance(Vector3(6, 3, 4), ty.resolve({}))
_p(_store_accepts(cl)[1] == 0 and cl.size() >= 1, "%s: clearance accepted (%d)" % [t, cl.size()])
# ---- fold -------------------------------------------------------------
var create := {"author": "A", "seq": 100, "kind": "shape.create", "undone": false,
"type": "stairs", "gv": 1, "params": {"steps": 4}, "frame": Transform3D.IDENTITY,
"size": Vector3(4, 2, 2), "material_id": 3, "level_id": 0}
var out1: Array = ShapeFold.expand([create])
var out2: Array = ShapeFold.expand([create.duplicate(true)])
_p(var_to_str(out1) == var_to_str(out2), "1. deterministic")
var solids := out1.filter(func(r: Dictionary) -> bool: return not bool(r["clearance"]))
_p(solids.size() == 4, "2. 4 steps -> 4 solid pieces (got %d)" % solids.size())
_p(String(out1[0]["group"]) == "A:100" and String(out1[0]["author"]) == "A/0@100" and int(out1[0]["seq"]) == 100,
"6. piece 0 is author A/0@100 (index @ shape version) seq 100, group A:100")
_p(int(out1[0]["material_id"]) == 3, "6. the shape's material on its pieces")
var set8 := {"author": "A", "seq": 110, "kind": "shape.set", "undone": false,
"target": "A:100", "params": {"steps": 8}}
var s8 := ShapeFold.expand([create, set8]).filter(func(r: Dictionary) -> bool: return not bool(r["clearance"]))
_p(s8.size() == 8 and String(s8[0]["group"]) == "A:100", "2. shape.set steps 8 -> 8 pieces, same shape")
# DCLevel restores a name it already holds with its OLD planes, so an edited
# shape's pieces must get new names (shape_fold.gd header).
_p(String(s8[0]["author"]) == "A/0@110", "2. an edit gives the pieces fresh names (A/0@110, got %s)" % s8[0]["author"])
var set8u := set8.duplicate(true)
set8u["undone"] = true
var s8u := ShapeFold.expand([create, set8u]).filter(func(r: Dictionary) -> bool: return not bool(r["clearance"]))
_p(s8u.size() == 4, "2. undo the set -> 4 again")
var setB := {"author": "B", "seq": 111, "kind": "shape.set", "undone": false,
"target": "A:100", "params": {"tread_thickness": 0.1}}
var setA2 := {"author": "A", "seq": 112, "kind": "shape.set", "undone": false,
"target": "A:100", "params": {"steps": 6}}
var st3: Dictionary = ShapeFold.resolve_states([create, set8, setB, setA2])["A:100"]
_p(int(st3["params"]["steps"]) == 6 and is_equal_approx(float(st3["params"]["tread_thickness"]), 0.1),
"3. per-param LWW: steps=6 (later), tread_thickness=0.1 (other author) both hold")
var moved := Transform3D(Basis.IDENTITY, Vector3(10, 0, 0))
var xf := {"author": "A", "seq": 120, "kind": "shape.xform", "undone": false,
"target": "A:100", "frame": moved}
var om: Array = ShapeFold.expand([create, xf])
var shifted := true
for i in out1.size():
var a: PackedFloat32Array = out1[i]["planes"]
var b: PackedFloat32Array = om[i]["planes"]
for k in range(0, a.size(), 4):
var want: float = a[k + 3] + a[k] * 10.0 # d' = d + n·t, t = (10,0,0)
if absf(b[k + 3] - want) > 1.0e-4:
shifted = false
_p(shifted, "4. shape.xform moves every plane by the frame")
var det := {"author": "A", "seq": 130, "kind": "shape.detach", "undone": false, "target": "A:100"}
_p(ShapeFold.expand([create, det]).is_empty(), "5. a live detach removes the shape's pieces")
var detu := det.duplicate(true)
detu["undone"] = true
_p(ShapeFold.expand([create, detu]).size() == out1.size(), "5. undo the detach -> the shape is back")
_finished = true