OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_keymap_blender.gd

173 lines · gd ref:
extends SceneTree
## GATE — our build-mode keys vs BLENDER 2.7x (left-select), command by command
## (docs/keymap-blender-279.md §5; keymap step 2).
##
## The reference is GENERATED, not remembered: tools/keymap/blender_279_left.json is Blender's
## own blender_default.py run with the Blender_27x.py params (tools/keymap/blender_dump.py).
## tools/keymap/command_map.json says which Blender command each of our actions IS.
##
##   match      our key must be one of Blender's keys for that command
##   exception  our key must still DIFFER (an exception that now matches is stale — remove
##              it), and it carries its reason and the step that resolves it
## Plus, reported and NOT failed: our keys that Blender uses for a DIFFERENT command
## (collisions = step-3 work), and Blender keys for mapped commands we lack.
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_keymap_blender.gd

const REF := "res://tools/keymap/blender_279_left.json"
const MAP := "res://tools/keymap/command_map.json"
const COLLIDE_IN := ["Window", "Screen", "3D View", "Object Mode", "Mesh"]

var ok := true
var checks := 0


func _p(cond: bool, what: String, note: Variant = "") -> void:
	checks += 1
	if not cond:
		ok = false
	print("  %-58s %s  %s" % [what, "OK" if cond else "**FAIL**", str(note)])


var _frame := 0


## Not _initialize: the Keymap autoload does not exist yet there.
func _process(_d: float) -> bool:
	_frame += 1
	if _frame < 3:
		return false
	_run()
	if checks < 30:
		_p(false, "only %d checks ran" % checks)
	print("\nGATE keymap_blender: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
	quit(0 if ok else 1)
	return true


## Blender event type -> {"key": keycode} or {"mouse": button}; {} = not representable here.
static func _ev(t: String) -> Dictionary:
	if t.length() == 1 and t >= "A" and t <= "Z":
		return {"key": KEY_A + (t.unicode_at(0) - 65)}
	if t.begins_with("NUMPAD_") and t.length() == 8 and t[7] >= "0" and t[7] <= "9":
		return {"key": KEY_KP_0 + (t.unicode_at(7) - 48)}
	if t.begins_with("F") and t.substr(1).is_valid_int():
		return {"key": KEY_F1 + int(t.substr(1)) - 1}
	var named := {"NUMPAD_PERIOD": KEY_KP_PERIOD, "NUMPAD_MINUS": KEY_KP_SUBTRACT,
		"NUMPAD_PLUS": KEY_KP_ADD, "NUMPAD_ASTERIX": KEY_KP_MULTIPLY, "NUMPAD_SLASH": KEY_KP_DIVIDE,
		"HOME": KEY_HOME, "END": KEY_END, "DEL": KEY_DELETE, "TAB": KEY_TAB, "SPACE": KEY_SPACE,
		"RET": KEY_ENTER, "ESC": KEY_ESCAPE, "COMMA": KEY_COMMA, "PERIOD": KEY_PERIOD,
		"MINUS": KEY_MINUS, "EQUAL": KEY_EQUAL, "ONE": KEY_1, "TWO": KEY_2, "THREE": KEY_3,
		"FOUR": KEY_4, "ACCENT_GRAVE": KEY_QUOTELEFT}
	if named.has(t):
		return {"key": named[t]}
	var mice := {"LEFTMOUSE": MOUSE_BUTTON_LEFT, "RIGHTMOUSE": MOUSE_BUTTON_RIGHT, "MIDDLEMOUSE": MOUSE_BUTTON_MIDDLE}
	if mice.has(t):
		return {"mouse": mice[t]}
	return {}


## Does one Blender row fire on our binding `b`?
static func _same(row: Dictionary, b: Dictionary) -> bool:
	if not (String(row["value"]) in ["PRESS", "CLICK"]):
		return false
	var e := _ev(String(row["type"]))
	if e.is_empty():
		return false
	if e.has("key") != b.has("key") or e.has("mouse") != b.has("mouse"):
		return false
	if e.has("key") and int(e["key"]) != int(b["key"]):
		return false
	if e.has("mouse") and int(e["mouse"]) != int(b["mouse"]):
		return false
	return bool(row["shift"]) == bool(b.get("shift", false)) and bool(row["ctrl"]) == bool(b.get("ctrl", false)) \
		and bool(row["alt"]) == bool(b.get("alt", false))


static func _props_match(row: Dictionary, want: Dictionary) -> bool:
	var have: Dictionary = row.get("props", {})
	for k: String in want:
		if not have.has(k):
			return false
		var a: Variant = have[k]
		var w: Variant = want[k]
		if (typeof(a) == TYPE_FLOAT or typeof(w) == TYPE_FLOAT) and absf(float(a) - float(w)) < 1e-6:
			continue
		if str(a) != str(w):
			return false
	return true


static func _keys(row: Dictionary) -> String:
	var s := ""
	for m: String in ["ctrl", "shift", "alt"]:
		if bool(row[m]):
			s += m.capitalize() + "+"
	return s + String(row["type"])


func _run() -> void:
	var ref: Variant = JSON.parse_string(FileAccess.get_file_as_string(REF))
	var cmap: Variant = JSON.parse_string(FileAccess.get_file_as_string(MAP))
	_p(ref is Dictionary and (ref as Dictionary).has("keymaps"), "the generated Blender 2.7x table loads", REF)
	_p(cmap is Dictionary and (cmap as Dictionary).has("map"), "the command map loads", MAP)
	if not (ref is Dictionary) or not (cmap is Dictionary):
		return
	var kms: Dictionary = ref["keymaps"]
	_p(int((kms.get("3D View", []) as Array).size()) == 136 and int((kms.get("Object Mode", []) as Array).size()) == 69,
		"the table is the measured one (3D View 136, Object Mode 69)",
		[(kms.get("3D View", []) as Array).size(), (kms.get("Object Mode", []) as Array).size()])
	var km: Node = root.get_node("/root/Keymap")
	var mapped_ops := {}
	var exceptions: Array = []
	for action: String in (cmap["map"] as Dictionary):
		var m: Dictionary = cmap["map"][action]
		var b: Dictionary = km.binding_of(action)
		if b.is_empty():
			_p(false, "%s exists in our Keymap" % action)
			continue
		var rows: Array = []
		for r: Dictionary in (kms.get(String(m["keymap"]), []) as Array):
			if String(r["op"]) == String(m["op"]) and _props_match(r, m.get("props", {})):
				rows.append(r)
		if rows.is_empty():
			_p(false, "%s: Blender has %s in %s" % [action, m["op"], m["keymap"]], "table drift?")
			continue
		mapped_ops["%s|%s" % [m["op"], str(m.get("props", {}))]] = action
		var hit := false
		var theirs := PackedStringArray()
		for r2: Dictionary in rows:
			theirs.append(_keys(r2))
			if _same(r2, b):
				hit = true
		if String(m["status"]) == "match":
			_p(hit, "%s = Blender's %s" % [action, m["op"]], ", ".join(theirs))
		else:
			_p(not hit, "%s: exception still differs (else remove it)" % action, ", ".join(theirs))
			exceptions.append("%s -> %s [step %s] %s" % [action, ", ".join(theirs), str(m.get("step", "")), String(m.get("why", ""))])

	print("\n-- exceptions (allowed, each with its reason) --")
	for e: String in exceptions:
		print("   " + e)

	# COLLISIONS (reported, never failed): one of our build-mode keys is a DIFFERENT Blender
	# command. This is the step-3 worklist.
	print("\n-- collisions: our key is a different Blender command (step-3 worklist) --")
	var n := 0
	for ctx: String in ["builder", "build_mode"]:
		for action2: String in (km.DEFAULTS.get(ctx, {}) as Dictionary):
			var b2: Dictionary = km.binding_of(action2)
			if b2.is_empty() or not (b2.has("key") or b2.has("mouse")):
				continue
			var own: Dictionary = (cmap["map"] as Dictionary).get(action2, {})
			for kname: String in COLLIDE_IN:
				for r3: Dictionary in (kms.get(kname, []) as Array):
					if _same(r3, b2) and String(r3["op"]) != String(own.get("op", "")):
						n += 1
						var kept: String = String((cmap.get("kept_collisions", {}) as Dictionary).get(action2, ""))
						print("   %-26s %-14s Blender %s: %s %s%s" % [action2, _keys(r3), kname, r3["op"],
							str(r3.get("props", {}).get("name", r3.get("props", {}).get("data_path", ""))),
							("   [kept: %s]" % kept) if kept != "" else ""])
						break
	print("   (%d)" % n)