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)