| … | … | @@ -395,7 +395,7 @@ const DEFAULTS: Dictionary = { |
| 395 | 395 | "builder.add_cable": {"label": "Cable (place points on surfaces; [ ] slack hangs each span)"}, |
| 396 | 396 | "builder.edit_curve": {"label": "Edit curve (the aimed point: move, Tab, X delete, I insert)"}, |
| 397 | 397 | "builder.assign_generator": {"label": "Generator… (stairs, tunnel … on this shape; it stays the passage)"}, |
| 398 | "builder.add_modifier": {"label": "Add modifier… (mirror, array, grid, radial, curve, chamfer, dice on this shape)"}, | |
| 398 | "builder.add_modifier": {"key": KEY_M, "ctrl": true, "shift": true, "label": "Add modifier… (mirror, array, grid, radial, curve, chamfer, dice on this shape)"}, | |
| 399 | 399 | # Menu-only, as in Blender (the outliner's / X menu's Delete Hierarchy): X keeps children. |
| 400 | 400 | "builder.delete_hierarchy": {"label": "Delete hierarchy (the objects and everything under them)"}, |
| 401 | 401 | "builder.extrude": {"key": KEY_E, "label": "Extrude the aimed face"}, |
| … | … | @@ -61,16 +61,25 @@ func open_sections(sections: Array, at: Vector2) -> void: |
| 61 | 61 | ## CHOICES rather than actions: rows = [[text, Callable], ...] under one title — a list |
| 62 | 62 | ## the keymap cannot know in advance (M's "move to which collection"). Picking runs the |
| 63 | 63 | ## Callable once the popup has closed, like pick(). |
| 64 | ## A row may name a KEY as its third element (#120 L1 §12.5: Shift+Ctrl+M, then a letter | |
| 65 | ## adds that modifier): shown as the row's accelerator, and that key, unmodified, picks it. | |
| 66 | var _choice_keys: Dictionary = {} # keycode -> choice id | |
| 67 | ||
| 68 | ||
| 64 | 69 | func open_choices(title: String, rows: Array, at: Vector2) -> void: |
| 65 | 70 | _menu.clear() |
| 66 | 71 | _actions.clear() |
| 67 | 72 | _calls.clear() |
| 73 | _choice_keys.clear() | |
| 68 | 74 | _menu.add_separator(title) |
| 69 | 75 | for r: Array in rows: |
| 70 | 76 | _actions.append("") |
| 71 | 77 | var id := _actions.size() - 1 |
| 72 | 78 | _calls[id] = r[1] |
| 73 | 79 | _menu.add_item(String(r[0]), id) |
| 80 | if r.size() > 2 and int(r[2]) != 0: | |
| 81 | _choice_keys[int(r[2])] = id | |
| 82 | _menu.set_item_accelerator(_menu.item_count - 1, int(r[2]) as Key) | |
| 74 | 83 | Keymap.push_context("menu") |
| 75 | 84 | _menu.popup(Rect2i(Vector2i(at), Vector2i.ZERO)) |
| 76 | 85 | |
| … | … | @@ -196,6 +205,11 @@ func _on_window_input(ev: InputEvent) -> void: |
| 196 | 205 | return |
| 197 | 206 | var k := ev as InputEventKey |
| 198 | 207 | var code := int(k.physical_keycode if k.physical_keycode != 0 else k.keycode) |
| 208 | # A choice's own letter (open_choices' third element), unmodified. | |
| 209 | if _choice_keys.has(code) and not (k.shift_pressed or k.ctrl_pressed or k.alt_pressed): | |
| 210 | _menu.set_input_as_handled() | |
| 211 | choose(int(_choice_keys[code])) | |
| 212 | return | |
| 199 | 213 | for a: String in _actions: |
| 200 | 214 | var b: Dictionary = Keymap.binding_of(a) |
| 201 | 215 | if b.has("mouse") or b.is_empty(): |
| … | … | @@ -4344,7 +4344,7 @@ func _builder_key(event: InputEvent) -> void: |
| 4344 | 4344 | elif _q_menu != null: |
| 4345 | 4345 | var rows: Array = [] |
| 4346 | 4346 | for k: String in MOD_DEFAULTS: |
| 4347 | rows.append([k.capitalize(), func() -> void: add_modifier(k)]) | |
| 4347 | rows.append([k.capitalize(), func() -> void: add_modifier(k), MOD_KEYS.get(k, 0)]) | |
| 4348 | 4348 | _q_menu.open_choices("Add modifier", rows, get_viewport().get_mouse_position()) |
| 4349 | 4349 | return |
| 4350 | 4350 | if Keymap.tapped(event, "builder.assign_generator"): |
| … | … | @@ -5613,6 +5613,10 @@ const MOD_DEFAULTS := { |
| 5613 | 5613 | "curve": {"count": 0}, |
| 5614 | 5614 | "chamfer": {"width": 0.1, "segments": 1, "angle": 30.0}, |
| 5615 | 5615 | } |
| 5616 | ## The letter that picks each kind in Add modifier (Shift+Ctrl+M, then the letter; §12.5). | |
| 5617 | ## C is curve, so chamfer takes B (BoxCutter's bevel key). | |
| 5618 | const MOD_KEYS := {"mirror": KEY_M, "array": KEY_A, "grid": KEY_G, "radial": KEY_R, | |
| 5619 | "curve": KEY_C, "chamfer": KEY_B, "dice": KEY_D} | |
| 5616 | 5620 | |
| 5617 | 5621 | |
| 5618 | 5622 | func add_modifier(kind: String, shape: String = "", params: Dictionary = {}) -> String: |
| … | … | @@ -0,0 +1,96 @@ |
| 1 | extends Node | |
| 2 | ## GATE — MODIFIER KEYS (#120 L1 §12.5; the menu + hotkey rule). | |
| 3 | ## K1 Shift+Ctrl+M on a shape opens Add modifier | |
| 4 | ## K2 ... its rows show their letters (M A G R C B D) | |
| 5 | ## K3 G picks Grid: the shape gains a grid modifier, the menu closes | |
| 6 | ## K4 Shift+Ctrl+M then D adds a Dice | |
| 7 | ## | |
| 8 | ## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_mod_keys.tscn | |
| 9 | ||
| 10 | var fails := 0 | |
| 11 | var checks := 0 | |
| 12 | var map: Node3D | |
| 13 | var persist: Node | |
| 14 | ||
| 15 | ||
| 16 | func _ok(what: String, cond: bool, note: Variant = "") -> void: | |
| 17 | checks += 1 | |
| 18 | if not cond: | |
| 19 | fails += 1 | |
| 20 | print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)]) | |
| 21 | ||
| 22 | ||
| 23 | func _ready() -> void: | |
| 24 | get_tree().root.size = Vector2i(1920, 1080) | |
| 25 | var gs: Node = get_tree().root.get_node_or_null("/root/GameState") | |
| 26 | gs.route("world:level:*", 1, "peer") | |
| 27 | var keet: Node = get_tree().root.get_node_or_null("/root/KeetService") | |
| 28 | if keet != null: | |
| 29 | keet.current_member_id = "gate-author" | |
| 30 | get_tree().root.get_node("/root/GameMode").set_mode("build") | |
| 31 | map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate() | |
| 32 | add_child(map) | |
| 33 | for i in 6: | |
| 34 | await get_tree().process_frame | |
| 35 | await _run() | |
| 36 | get_tree().root.get_node("/root/GameMode").set_mode("play") | |
| 37 | if checks < 5: | |
| 38 | print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks) | |
| 39 | fails += 1 | |
| 40 | print("\nGATE mod_keys: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails]) | |
| 41 | get_tree().quit(0 if fails == 0 else 1) | |
| 42 | ||
| 43 | ||
| 44 | func _key(code: Key, shift := false, ctrl := false) -> InputEventKey: | |
| 45 | var e := InputEventKey.new() | |
| 46 | e.keycode = code | |
| 47 | e.physical_keycode = code | |
| 48 | e.shift_pressed = shift | |
| 49 | e.ctrl_pressed = ctrl | |
| 50 | e.pressed = true | |
| 51 | return e | |
| 52 | ||
| 53 | ||
| 54 | func _kinds(shape: String) -> Array: | |
| 55 | var out: Array = [] | |
| 56 | for m: Dictionary in (map.modifier_stack(shape) as Array): | |
| 57 | out.append(String(m["kind"])) | |
| 58 | return out | |
| 59 | ||
| 60 | ||
| 61 | func _run() -> void: | |
| 62 | persist = map.get("_persist") | |
| 63 | var s: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {}, | |
| 64 | "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(140, 0, 140))), | |
| 65 | "size": [1, 1, 1], "material_id": 2, "level_id": 0}) | |
| 66 | persist.refold() | |
| 67 | map.select_shape(s) | |
| 68 | await get_tree().process_frame | |
| 69 | var q: Node = map.get("_q_menu") | |
| 70 | var menu: PopupMenu = q.get("_menu") | |
| 71 | ||
| 72 | # ---- K1 / K2 ---- | |
| 73 | get_viewport().push_input(_key(KEY_M, true, true)) | |
| 74 | await get_tree().process_frame | |
| 75 | _ok("K1 Shift+Ctrl+M opens Add modifier", q.is_open()) | |
| 76 | var shown: Array = [] | |
| 77 | for i in menu.item_count: | |
| 78 | if menu.get_item_accelerator(i) != 0: | |
| 79 | shown.append(OS.get_keycode_string(menu.get_item_accelerator(i))) | |
| 80 | shown.sort() | |
| 81 | _ok("K2 its rows show their letters", shown == ["A", "B", "C", "D", "G", "M", "R"], shown) | |
| 82 | ||
| 83 | # ---- K3 ---- | |
| 84 | q.call("_on_window_input", _key(KEY_G)) | |
| 85 | for i in 3: | |
| 86 | await get_tree().process_frame | |
| 87 | _ok("K3 G adds a Grid", _kinds(s).has("grid"), _kinds(s)) | |
| 88 | _ok("K3 ... and the menu closed", not q.is_open()) | |
| 89 | ||
| 90 | # ---- K4 ---- | |
| 91 | get_viewport().push_input(_key(KEY_M, true, true)) | |
| 92 | await get_tree().process_frame | |
| 93 | q.call("_on_window_input", _key(KEY_D)) | |
| 94 | for i in 3: | |
| 95 | await get_tree().process_frame | |
| 96 | _ok("K4 Shift+Ctrl+M, D adds a Dice", _kinds(s).has("dice"), _kinds(s)) |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | [gd_scene load_steps=2 format=3] | |
| 2 | ||
| 3 | [ext_resource type="Script" path="res://scenes/test/gate_mod_keys.gd" id="1"] | |
| 4 | ||
| 5 | [node name="ModKeysGate" type="Node"] | |
| 6 | script = ExtResource("1") |