OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / operators / cable_edges_operator.gd

165 lines · gd ref:
extends LevelOperator
## CABLES FROM EDGES (#120 L3, docs/node-tree-and-stacks.md §14.5): the edge set (§14.4) ->
## one cable per chain (EdgeCables). The preview draws the cables' curves; Enter writes ONE
## gesture (a curve + a cable per chain, map.cables_from_edges). Adjust Last re-runs it.
## Keys (Cablerator's from-edges where the convention allows): P points at vertices /
## centres, C corners round / sharp (Cablerator's H is every tool's help here), [ ] / Tab
## the offset and the radius.

const EdgeCablesScript := preload("res://scenes/maps/curves/edge_cables.gd")
const POINTS_AT := ["Vertices", "Centres"]
const CORNERS := ["Round", "Sharp"]

var centres := false
var sharp := false
var offset := 0.05                    # metres off the edge, along its outward normal
var radius := 0.05
var _edges: Array = []                # selected_edges() at activation (Adjust Last: stored)
var _frozen := false


func op_name() -> String:
	return "cable from edges"


## 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 from edges"


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 cables"
	m.table([["P", "points at", func() -> String: return POINTS_AT[1 if centres else 0]],
		["C", "corners", func() -> String: return CORNERS[1 if sharp else 0]],
		["", "chains", func() -> String: return "%d" % EdgeCablesScript.chains(_edges).size()]])
	m.bind("offset", "off the edge", func() -> float: return offset,
		func(v: float) -> void:
			offset = maxf(0.0, v)
			update_preview(),
		0.01, " m", 0.002)
	m.bind("radius", "radius", func() -> float: return radius,
		func(v: float) -> void:
			radius = clampf(v, 0.005, 2.0)
			update_preview(),
		0.01, " m", 0.002)


func status() -> String:
	return modal().status_text()


func on_activate() -> bool:
	if not _frozen:
		_edges = map.selected_edges() if map != null and map.has_method("selected_edges") else []
	if _edges.is_empty():
		print("[cable from edges] select edges first (edge mode, Shift-click to add)")
		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_P:
			centres = not centres
			update_preview()
			return true
		KEY_C:
			sharp = not sharp
			update_preview()
			return true
	return modal().handle_key(event)


func update_preview() -> void:
	clear_preview()
	var lines := PackedVector3Array()
	for ch: Dictionary in EdgeCablesScript.chains(_edges):
		var pts: Array = EdgeCablesScript.curve_points(ch, centres, offset, sharp)
		if pts.size() < 2:
			continue
		var closed := bool(ch["closed"])
		var fs := CurveMath.sample(pts, closed, 64)
		for i in range(1, fs.size()):
			lines.append(fs[i - 1]["p"])
			lines.append(fs[i]["p"])
		if closed and fs.size() > 2:
			lines.append(fs[fs.size() - 1]["p"])
			lines.append(fs[0]["p"])
	draw_lines(lines)


func confirm() -> bool:
	var dots: Array = map.cables_from_edges(_edges, centres, offset, sharp, {"radius": radius})
	if dots.is_empty():
		return false
	ModalControls.summary("Added %d cable%s from %d edge%s" % [dots.size(), "" if dots.size() == 1 else "s",
		_edges.size(), "" if _edges.size() == 1 else "s"])
	return true


func tool_settings() -> Array:
	return [
		{"id": "points_at", "label": "Points at", "type": "enum", "value": 1 if centres else 0,
			"choices": POINTS_AT, "tip": "P: the chain's vertices, or its edge centres (rounds corners off)"},
		{"id": "corners", "label": "Corners", "type": "enum", "value": 1 if sharp else 0,
			"choices": CORNERS, "tip": "C: round (auto handles) or sharp (vector)"},
		{"id": "offset", "label": "Offset", "type": "float", "value": offset, "min": 0.0, "max": 2.0,
			"step": 0.01, "tip": "Off the edge along its outward normal (the radius sits the cable on the edge)"},
		{"id": "radius", "label": "Radius", "type": "float", "value": radius, "min": 0.005, "max": 2.0,
			"step": 0.01},
	]


func set_tool_setting(sid: String, value: Variant) -> bool:
	match sid:
		"points_at":
			centres = int(value) == 1
		"corners":
			sharp = int(value) == 1
		"offset":
			offset = maxf(0.0, float(value))
		"radius":
			radius = clampf(float(value), 0.005, 2.0)
		_:
			return false
	if not _frozen:
		update_preview()
	return true


func adjust_ids() -> Array:
	return ["points_at", "corners", "offset", "radius"]


func adjust_snapshot() -> Dictionary:
	return {"edges": _edges.duplicate(true), "centres": centres, "sharp": sharp, "offset": offset, "radius": radius}


func adjust_restore(snap: Dictionary) -> void:
	_edges = (snap.get("edges", []) as Array).duplicate(true)
	centres = bool(snap.get("centres", centres))
	sharp = bool(snap.get("sharp", sharp))
	offset = float(snap.get("offset", offset))
	radius = float(snap.get("radius", radius))
	_frozen = true


func adjust_end() -> void:
	_frozen = false
	clear_preview()