OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_cable_tool.gd

180 lines · gd ref:
extends Node
## GATE — THE CABLE TOOLS (#120 L3, docs/node-tree-and-stacks.md §11.3).
##   K1  the Add menu lists Cable; builder.add_cable starts the curve tool in cable mode
##   K2  Enter with two placed points: ONE gesture, a curve and a cable shape on it
##   K3  slack 1 m on a 10 m span: 3 interior points, the middle one d = sqrt(3·10·1/8)
##       below the chord; 320 m out its pieces are still clean hulls (<= 18 planes)
##   K4  G on a cable moves its CURVE (one curve.set), and the pieces with it
##   K5  R on a cable turns its curve about the pivot
##   K6  X on a cable removes its curve too; Ctrl+Z brings both back
##   K7  X on a cable whose curve a follow-curve also reads keeps the curve
##   K8  the Shape panel sets the caps: Start cap, End cap, Cap size, Cap length
##
## Points are handed to the tool directly: aiming is gate_curve_tool's (the same code).
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_cable_tool.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var ops: 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 < 14:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE cable_tool: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _cables() -> Array:
	return (persist.live_shapes() as Dictionary).keys().filter(func(d: String) -> bool:
		return String(persist.shape_state(d)["type"]) == "cable")


func _curve_of(dot: String) -> String:
	return String((persist.shape_state(dot)["params"] as Dictionary).get("curve", ""))


func _pieces_aabb(dot: String) -> AABB:
	var bb := AABB()
	var first := true
	for b: int in (level.get_group_brush_ids(dot) as PackedInt32Array):
		for c: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]):
			bb = AABB(c, Vector3.ZERO) if first else bb.expand(c)
			first = false
	return bb


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	ops = map._ensure_ops()
	persist.refold()

	# ---- K1 ----
	var rows: Array = []
	for sec: Array in map.add_sections():
		for r: Array in sec[1]:
			rows.append(r[0])
	_ok("K1 the Add menu lists Cable", rows.has("builder.add_cable"), rows)
	var ev := InputEventAction.new()
	ev.action = "builder.add_cable"
	ev.pressed = true
	map._unhandled_input(ev)
	var op: Node = map.get("_cable_op")
	_ok("K1 builder.add_cable: the curve tool, in cable mode", ops.get("active") == op and bool(op.get("as_cable")),
		ops.get("active"))

	# ---- K2 ----
	op.set("_pts", [CurveMath.point(Vector3(300, 0.5, 300), Vector3.ZERO), CurveMath.point(Vector3(310, 0.5, 300), Vector3.ZERO)])
	var n0: int = (persist.records(false) as Array).size()
	ops.confirm()
	var cs := _cables()
	_ok("K2 Enter: one cable shape", cs.size() == 1, cs)
	_ok("K2 ... two records (curve + shape)", (persist.records(false) as Array).size() == n0 + 2)
	var c: String = cs[0] if cs.size() == 1 else ""
	_ok("K2 ... on its curve, with pieces", c != "" and persist.curve_states().has(_curve_of(c))
		and (level.get_group_brush_ids(c) as PackedInt32Array).size() > 0)

	# ---- K3 ----
	map._unhandled_input(ev)
	op.set("slack", 1.0)
	op.set("_pts", [CurveMath.point(Vector3(320, 3.0, 300), Vector3.ZERO), CurveMath.point(Vector3(330, 3.0, 300), Vector3.ZERO)])
	ops.confirm()
	op.set("slack", 0.0)
	var hung := ""
	for d: String in _cables():
		if d != c:
			hung = d
	var hp: Array = persist.curve_states().get(_curve_of(hung), {}).get("points", [])
	_ok("K3 slack 1 m: 2 + 3 interior points", hp.size() == 5, hp.size())
	if hp.size() == 5:
		var mid: Vector3 = (hp[2] as Dictionary)["p"]
		_ok("K3 ... the middle sags sqrt(3·10·1/8) = 1.936 m", absf((3.0 - mid.y) - sqrt(30.0 / 8.0)) < 1.0e-3, mid)
	# 320 m out: each piece is still a clean hull of two octagons (Jolt rejected sliver hulls).
	var worst := [0, 0]
	for b: int in (level.get_group_brush_ids(hung) as PackedInt32Array):
		var pl: PackedFloat32Array = level.get_brush(b)["planes"]
		worst = [maxi(worst[0], pl.size() / 4), maxi(worst[1], FaceKit.corners(pl).size())]
	_ok("K3 ... at 320 m every piece <= 18 planes, <= 16 corners", worst[0] > 0 and worst[0] <= 18
		and worst[1] <= 16, worst)

	# ---- K4 ----
	var bb0 := _pieces_aabb(c)
	var nk: int = (persist.records(false) as Array).size()
	persist.begin_gesture("Move")
	map.translate_units([{"kind": "shape", "shape": c, "w": 1.0}], Vector3(0, 0, 5))
	persist.end_gesture()
	persist.refold()
	var recs: Array = persist.records(false)
	_ok("K4 G on a cable: ONE curve.set, no shape.xform", recs.size() == nk + 1
		and String((recs[recs.size() - 1] as Dictionary)["kind"]) == "curve.set", recs.slice(nk).map(func(r: Dictionary) -> String: return r["kind"]))
	var p0: Vector3 = (persist.curve_states()[_curve_of(c)]["points"][0] as Dictionary)["p"]
	_ok("K4 ... the curve moved +5 z", p0.distance_to(Vector3(300, 0.5, 305)) < 1.0e-3, p0)
	_ok("K4 ... and the pieces with it", absf(_pieces_aabb(c).position.z - (bb0.position.z + 5.0)) < 1.0e-3, [bb0, _pieces_aabb(c)])

	# ---- K5 ----
	persist.begin_gesture("Rotate")
	map.rotate_brushes(Array(level.get_group_brush_ids(c)), Vector3.UP, 90.0, Vector3(300, 0.5, 305))
	persist.end_gesture()
	persist.refold()
	var p1: Vector3 = (persist.curve_states()[_curve_of(c)]["points"][1] as Dictionary)["p"]
	_ok("K5 R 90° about the start: the end swings to -z", p1.distance_to(Vector3(300, 0.5, 295)) < 1.0e-3, p1)

	# ---- K6 ----
	var cd := _curve_of(c)
	map.delete_brushes(Array(level.get_group_brush_ids(c)))
	_ok("K6 X on the cable: the shape is gone", not (persist.live_shapes() as Dictionary).has(c))
	_ok("K6 ... and its curve", not persist.curve_states().has(cd))
	persist.undo_last()
	persist.refold()
	_ok("K6 Ctrl+Z: both back", (persist.live_shapes() as Dictionary).has(c) and persist.curve_states().has(cd))

	# ---- K7 ----
	var box: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(340, 0, 340))),
		"size": [1.0, 1.0, 1.0], "material_id": 2, "level_id": 0})
	persist.refold()
	map.add_modifier("curve", box, {"curve": cd, "count": 2})
	map.delete_brushes(Array(level.get_group_brush_ids(c)))
	_ok("K7 a follow-curve reads the curve: X on the cable keeps it", not (persist.live_shapes() as Dictionary).has(c)
		and persist.curve_states().has(cd))

	# ---- K8: caps from the Shape panel (user 2026-10-18) ----
	var c8: String = map.add_cable([CurveMath.point(Vector3(360, 0.5, 300)), CurveMath.point(Vector3(366, 0.5, 300))])
	await get_tree().process_frame
	var sp: Node = map.get("_shape_panel")
	var ids8: Array = sp.setting_ids() if sp != null else []
	_ok("K8 the selected cable's panel has Start cap, End cap, Cap size, Cap length",
		ids8.has("p.start_cap") and ids8.has("p.end_cap") and ids8.has("p.cap_size") and ids8.has("p.cap_length"), ids8)
	_ok("K8 ... and not the old Connectors switch", not ids8.has("p.connectors"), ids8)
	var n8: int = (level.get_group_brush_ids(c8) as PackedInt32Array).size()
	sp.apply("p.end_cap", 2)                     # none / plug / FLANGE / socket
	persist.refold()
	_ok("K8 End cap = Flange: one shape.set, two more pieces", String(persist.shape_state(c8)["params"].get("end_cap", ""))
		== "flange" and (level.get_group_brush_ids(c8) as PackedInt32Array).size() == n8 + 2,
		[persist.shape_state(c8)["params"].get("end_cap"), n8, (level.get_group_brush_ids(c8) as PackedInt32Array).size()])