extends Node
## GATE — MAKE PARAMETRIC (#120 F2, docs/node-tree-and-stacks.md §3.1): any brush becomes
## a parametric shape that generates it.
## M1 an axis-aligned box brush -> a `box` shape, the same corners, the brush gone;
## one gesture, one Ctrl+Z brings the brush back
## M2 a wedge (a box with a slanted face) -> a `hull` shape with the same corners
## M3 ... each face keeps its own texture
## M4 resizing the hull shape stretches it (size is a real knob)
## M5 a brush in a collection: its shape is in that collection
## M6 a shape's piece is refused (nothing written)
## M7 the Q menu offers it for a plain brush, and the menu action (Keymap.run's path for
## a menu-only action) converts it
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_make_parametric.tscn
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
get_tree().root.get_node("/root/GameMode").set_mode("build")
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await _run()
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 15:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE make_parametric: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
## Two corner sets equal within 1 mm (order-free).
func _same_corners(a: PackedVector3Array, b: PackedVector3Array) -> bool:
if a.size() != b.size():
return false
for v: Vector3 in a:
var hit := false
for w: Vector3 in b:
if v.distance_to(w) < 1.0e-3:
hit = true
break
if not hit:
return false
return true
func _corners_of_group(g: String) -> PackedVector3Array:
var out := PackedVector3Array()
for b: int in (level.get_group_brush_ids(g) as PackedInt32Array):
out.append_array(FaceKit.corners(level.get_brush(b)["planes"]))
return out
func _new_brush(planes: Array, face_tex: Array) -> int:
persist.begin_gesture("Add")
var dot: String = persist.record("brush.create", {"planes": planes, "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": face_tex})
persist.end_gesture()
persist.refold()
return int(level.brush_by_key(dot))
func _tex(name: String) -> Dictionary:
return {"tex": name, "mode": 0}
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
persist.refold()
# ---- M1 ----
var box: Array = Array(ShapeKit.box_piece(Vector3(40, 0, 40), Vector3(43, 2, 44))["planes"])
var b1 := _new_brush(box, [])
var c1 := FaceKit.corners(level.get_brush(b1)["planes"])
var k1 := String(level.get_brush_key(b1))
map.select_brushes(PackedInt32Array([b1]))
var g0: int = (persist.gestures() as Array).size()
var made: Array = map.make_parametric()
_ok("M1 one shape made", made.size() == 1, made)
if made.is_empty():
return
var s1 := String(made[0])
_ok("M1 ... of type box", String(persist.shape_state(s1).get("type", "")) == "box", persist.shape_state(s1).get("type"))
_ok("M1 ... generating the same corners", _same_corners(_corners_of_group(s1), c1), _corners_of_group(s1))
_ok("M1 the brush is gone", int(level.brush_by_key(k1)) <= 0 or bool(level.get_brush(int(level.brush_by_key(k1))).get("tombstoned", false))
or not (level.get_live_brush_ids() as PackedInt32Array).has(int(level.brush_by_key(k1))))
_ok("M1 one gesture", (persist.gestures() as Array).size() == g0 + 1)
persist.undo_last()
persist.refold()
_ok("M1 one Ctrl+Z: the brush is back, the shape gone", (level.get_live_brush_ids() as PackedInt32Array).has(int(level.brush_by_key(k1)))
and not (persist.live_shapes() as Dictionary).has(s1))
# ---- M2 / M3 ----
var wedge: Array = Array(ShapeKit.box_piece(Vector3(50, 0, 50), Vector3(54, 3, 52))["planes"])
var sn := Vector3(1, 1, 0).normalized()
wedge.append_array([sn.x, sn.y, sn.z, sn.dot(Vector3(53, 3, 50))])
var texs: Array = []
for i in wedge.size() / 4:
texs.append(_tex("t%d" % i))
var b2 := _new_brush(wedge, texs)
var c2 := FaceKit.corners(level.get_brush(b2)["planes"])
var pl2: PackedFloat32Array = level.get_brush(b2)["planes"]
map.select_brushes(PackedInt32Array([b2]))
var s2 := String(map.make_parametric()[0])
_ok("M2 a wedge becomes a hull", String(persist.shape_state(s2).get("type", "")) == "hull")
_ok("M2 ... with the same corners", _same_corners(_corners_of_group(s2), c2), [c2.size(), _corners_of_group(s2).size()])
var piece: int = (level.get_group_brush_ids(s2) as PackedInt32Array)[0]
var pp: PackedFloat32Array = level.get_brush(piece)["planes"]
var pft: Array = level.get_brush(piece)["face_tex"]
var tex_ok := pp.size() == pl2.size()
for i in pl2.size() / 4:
var n := Vector3(pl2[i * 4], pl2[i * 4 + 1], pl2[i * 4 + 2])
for j in pp.size() / 4:
if Vector3(pp[j * 4], pp[j * 4 + 1], pp[j * 4 + 2]).dot(n) > 0.9999:
tex_ok = tex_ok and String((pft[j] as Dictionary).get("tex", "")) == "t%d" % i
_ok("M3 each face keeps its own texture", tex_ok, pft.map(func(t: Dictionary) -> String: return String(t.get("tex", ""))))
# ---- M4 ----
var sz: Vector3 = persist.shape_state(s2)["size"]
persist.begin_gesture("Size")
persist.record("shape.size", {"target": s2, "size": [sz.x * 2.0, sz.y, sz.z]})
persist.end_gesture()
persist.refold()
var c4 := _corners_of_group(s2)
var span := 0.0
var lo := INF
var hi := -INF
for v: Vector3 in c4:
lo = minf(lo, v.x)
hi = maxf(hi, v.x)
span = hi - lo
_ok("M4 doubling the size doubles the hull along x", absf(span - 8.0) < 1.0e-3, span)
# ---- M5 ----
var b5 := _new_brush(Array(ShapeKit.box_piece(Vector3(60, 0, 60), Vector3(61, 1, 61))["planes"]), [])
map.select_brushes(PackedInt32Array([b5]))
var col := CollectionOps.create_from_selection(map, "Kept")
persist.refold()
map.select_brushes(PackedInt32Array([int(level.brush_by_key(String(level.get_brush_key(b5))))]))
var s5 := String(map.make_parametric()[0])
var members: Array = persist.collection_tree()["cols"][col]["members"]
_ok("M5 the shape REPLACES the brush in its collection", members == ["g:" + s5], members)
# ---- M6 ----
map.select_brushes(PackedInt32Array([piece]))
var g6: int = (persist.gestures() as Array).size()
_ok("M6 a shape's piece is refused", (map.make_parametric() as Array).is_empty())
_ok("M6 ... nothing written", (persist.gestures() as Array).size() == g6 or persist.gestures()[0]["count"] == 0)
# ---- M7 ----
var b7 := _new_brush(Array(ShapeKit.box_piece(Vector3(70, 0, 70), Vector3(71, 1, 71))["planes"]), [])
map.select_brushes(PackedInt32Array([b7]))
var rows7: Array = []
for sec: Array in map.q_sections():
for r: Array in sec[1]:
rows7.append(String(r[0]))
_ok("M7 the Q menu offers Make parametric", rows7.has("builder.make_parametric"), rows7)
var n7: int = (persist.live_shapes() as Dictionary).size()
var ev := InputEventAction.new()
ev.action = "builder.make_parametric"
ev.pressed = true
map._unhandled_input(ev)
persist.refold()
_ok("M7 the menu action converts it", (persist.live_shapes() as Dictionary).size() == n7 + 1)