extends LevelOperator
## A BUNDLE BETWEEN TWO FACE SETS (#120 L3, docs/node-tree-and-stacks.md §14.6):
## Cablerator's Create a Cable Mass on the face set — exactly two islands. The preview draws
## the cables; Enter writes ONE gesture (N curves + cables and a "Bundle" collection holding
## them, map.cable_bundle). Adjust Last re-runs it with the same seed.
## Keys (Cablerator's, tool-local): Z count, D tension, N tension jitter, S radius — each
## focuses its value, then [ ] steps it (Tab cycles, as everywhere); R random points / face
## centres; Q a new seed.
const BundleCablesScript := preload("res://scenes/maps/curves/bundle_cables.gd")
var count := 8
var tension := 1.0
var jitter := 0.3
var radius := 0.03
var slack := 0.0
var random := true
var rng_seed := 1
var _faces: Array = [] # _selected_face_polys() at activation (Adjust Last: stored)
var _frozen := false
func op_name() -> String:
return "cable bundle"
## The mouse drags the focused value (§17.7 Q2): this tool works on the selection.
func drags_value() -> bool:
return true
func gesture_label() -> String:
return "Cable bundle"
func preview_color() -> Color:
return Color(1.0, 0.85, 0.3, 0.9) # the curve view's amber
func setup_modal(m: ModalControls) -> void:
m.confirm_label = "add the bundle"
m.table([["R", "ends", func() -> String: return "random points" if random else "face centres"],
["Q", "new seed", func() -> String: return "%d" % rng_seed]])
m.bind("count", "cables (Z)", func() -> float: return float(count),
func(v: float) -> void:
count = clampi(roundi(v), 1, 64)
update_preview(),
1.0, "")
m.bind("tension", "tension (D)", func() -> float: return tension,
func(v: float) -> void:
tension = clampf(v, 0.0, 5.0)
update_preview(),
0.1, "", 0.02)
m.bind("jitter", "tension jitter (N)", func() -> float: return jitter,
func(v: float) -> void:
jitter = clampf(v, 0.0, 1.0)
update_preview(),
0.05, "", 0.01)
m.bind("radius", "radius (S)", func() -> float: return radius,
func(v: float) -> void:
radius = clampf(v, 0.005, 2.0)
update_preview(),
0.01, " m", 0.002)
m.bind("slack", "slack", func() -> float: return slack,
func(v: float) -> void:
slack = maxf(0.0, v)
update_preview(),
0.1, " m", 0.02)
func status() -> String:
return modal().status_text()
func _opts() -> Dictionary:
return {"count": count, "random": random, "seed": rng_seed, "tension": tension, "jitter": jitter, "slack": slack}
func on_activate() -> bool:
if not _frozen:
_faces = map._selected_face_polys() if map != null and map.has_method("_selected_face_polys") else []
var n: int = BundleCablesScript.islands(_faces).size()
if n != 2:
print("[bundle] select exactly 2 face islands (face mode, Shift-click), found %d" % n)
return false
update_preview()
return true
func on_deactivate() -> void:
clear_preview()
func handle_key(event: InputEvent) -> bool:
if not (event is InputEventKey) or not (event as InputEventKey).pressed:
return false
match KeyRules.key_of(event as InputEventKey):
KEY_Z:
return modal().focus("count")
KEY_D:
return modal().focus("tension")
KEY_N:
return modal().focus("jitter")
KEY_S:
return modal().focus("radius")
KEY_R:
random = not random
update_preview()
return true
KEY_Q:
rng_seed += 1
update_preview()
return true
return modal().handle_key(event)
func update_preview() -> void:
clear_preview()
var lines := PackedVector3Array()
for pts: Array in (BundleCablesScript.bundle(_faces, _opts())["cables"] as Array):
var fs := CurveMath.sample(pts, false, 48)
for i in range(1, fs.size()):
lines.append(fs[i - 1]["p"])
lines.append(fs[i]["p"])
draw_lines(lines)
func confirm() -> bool:
var dots: Array = map.cable_bundle(_faces, _opts(), {"radius": radius})
if dots.is_empty():
return false
ModalControls.summary("Added a bundle of %d cable%s" % [dots.size(), "" if dots.size() == 1 else "s"])
return true
func tool_settings() -> Array:
return [
{"id": "count", "label": "Cables", "type": "int", "value": count, "min": 1, "max": 64, "step": 1},
{"id": "tension", "label": "Tension", "type": "float", "value": tension, "min": 0.0, "max": 5.0, "step": 0.1,
"tip": "How far each end leaves its face along the normal, x chord / 3"},
{"id": "jitter", "label": "Tension jitter", "type": "float", "value": jitter, "min": 0.0, "max": 1.0, "step": 0.05},
{"id": "radius", "label": "Radius", "type": "float", "value": radius, "min": 0.005, "max": 2.0, "step": 0.01},
{"id": "slack", "label": "Slack", "type": "float", "value": slack, "min": 0.0, "max": 20.0, "step": 0.1,
"tip": "Extra length: each cable hangs"},
{"id": "random", "label": "Ends", "type": "enum", "value": 0 if random else 1,
"choices": ["Random points", "Face centres"], "tip": "R"},
{"id": "seed", "label": "Seed", "type": "int", "value": rng_seed, "min": 0, "max": 1000000, "step": 1, "tip": "Q: a new one"},
]
func set_tool_setting(sid: String, value: Variant) -> bool:
match sid:
"count":
count = clampi(int(value), 1, 64)
"tension":
tension = clampf(float(value), 0.0, 5.0)
"jitter":
jitter = clampf(float(value), 0.0, 1.0)
"radius":
radius = clampf(float(value), 0.005, 2.0)
"slack":
slack = maxf(0.0, float(value))
"random":
random = int(value) == 0
"seed":
rng_seed = maxi(0, int(value))
_:
return false
if not _frozen:
update_preview()
return true
func adjust_ids() -> Array:
return ["count", "tension", "jitter", "radius", "slack", "random", "seed"]
func adjust_snapshot() -> Dictionary:
var s := _opts()
s["faces"] = _faces.duplicate(true)
s["radius"] = radius
return s
func adjust_restore(snap: Dictionary) -> void:
_faces = (snap.get("faces", []) as Array).duplicate(true)
count = int(snap.get("count", count))
random = bool(snap.get("random", random))
rng_seed = int(snap.get("seed", rng_seed))
tension = float(snap.get("tension", tension))
jitter = float(snap.get("jitter", jitter))
slack = float(snap.get("slack", slack))
radius = float(snap.get("radius", radius))
_frozen = true
func adjust_end() -> void:
_frozen = false
clear_preview()