OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / a1422a4

#120 L2 step 7: Q menu rows + keys for the stack operations
Q on a shape with cuts offers Change cut (Alt+X), Extract (Alt+Y) and, on a shape target, Apply up to here (Ctrl+Alt+A); each key opens the pick-a-cut list (StackOps). gate_q_stack Q6 (13); keymap_gate, q_menu green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a1422a4ad8a0704f76b22e83366d7b9528d25ace
by plankatron <fleeky@gmail.com> · 2026-10-19T01:21:36.000Z

Changed files (3) +52 -1

autoloads/keymap.gd +4 -0
……@@ -429,6 +429,10 @@ const DEFAULTS: Dictionary = {
429429 "builder.reset_panels": {"key": KEY_N, "ctrl": true, "alt": true, "label": "Reset panel layout (every panel back to its column)"},
430430 # #120 L2 §13: BoxCutter's own start key (its manual: "alt + w with cursor in 3d view").
431431 "builder.box_cutter": {"key": KEY_W, "alt": true, "label": "BoxCutter: draw a cutter on the aimed surface"},
432 # #120 L2 §13.4–13.5: the Q menu's stack operations (StackOps), each with a key.
433 "builder.change_cut": {"key": KEY_X, "alt": true, "label": "Change cut… (subtract / slice / intersect / inset / outset)"},
434 "builder.extract_cut": {"key": KEY_Y, "alt": true, "label": "Extract… (a cut's target ∩ cutter becomes new shapes)"},
435 "builder.apply_up_to": {"key": KEY_A, "ctrl": true, "alt": true, "label": "Apply up to here… (bake the cuts up to one, keep the rest live)"},
432436 "builder.adjust_last": {"key": KEY_F6, "label": "Adjust Last (the last operation's settings)"},
433437 "builder.history": {"key": KEY_Z, "ctrl": true, "alt": true, "label": "Undo History"},
434438 "builder.repeat_last": {"key": KEY_R, "shift": true, "label": "Repeat the last command (starts the tool again)"},
scenes/maps/map_dc_level.gd +26 -0
……@@ -610,6 +610,11 @@ func q_sections() -> Array:
610610 rows.append(["builder.make_parametric"])
611611 if not op_stack(subject).is_empty():
612612 rows.append(["builder.cuts_clear"])
613 # #120 L2: the stack operations (StackOps); Apply works on shapes, as Make Real does.
614 rows.append(["builder.change_cut"])
615 rows.append(["builder.extract_cut"])
616 if is_shape:
617 rows.append(["builder.apply_up_to"])
613618 rows.append_array([["builder.hide"], ["builder.hide_unselected"], ["builder.delete"]])
614619 return [["Shape" if is_shape else "Selection", rows], cursor_section()]
615620
……@@ -4353,6 +4358,27 @@ func _builder_key(event: InputEvent) -> void:
43534358 rows.append([k.capitalize(), func() -> void: add_modifier(k), MOD_KEYS.get(k, 0)])
43544359 _q_menu.open_choices("Add modifier", rows, get_viewport().get_mouse_position())
43554360 return
4361 # #120 L2 §13.4–13.5: Change cut / Extract / Apply up to here — each picks one of the
4362 # subject's cuts from a list (StackOps).
4363 for sa: Array in [["builder.change_cut", "Change cut"], ["builder.extract_cut", "Extract"],
4364 ["builder.apply_up_to", "Apply up to here"]]:
4365 if not Keymap.tapped(event, String(sa[0])):
4366 continue
4367 var so := preload("res://scenes/maps/stack_ops.gd")
4368 var subj := stack_subject()
4369 var srows: Array = []
4370 match String(sa[0]):
4371 "builder.change_cut":
4372 srows = so.change_rows(self, subj)
4373 "builder.extract_cut":
4374 srows = so.op_rows(self, subj, so.extract)
4375 _:
4376 srows = so.op_rows(self, subj, so.apply_up_to)
4377 if srows.is_empty():
4378 print("[MapDCLevel] %s: the selection has no cuts" % String(sa[1]).to_lower())
4379 elif _q_menu != null:
4380 _q_menu.open_choices(String(sa[1]), srows, get_viewport().get_mouse_position())
4381 return
43564382 if Keymap.tapped(event, "builder.assign_generator"):
43574383 if _sel_shape == "":
43584384 print("[MapDCLevel] assign generator: select a shape (Make parametric turns a brush into one)")
scenes/test/gate_q_stack.gd +22 -1
……@@ -39,7 +39,7 @@ func _ready() -> void:
3939 await get_tree().process_frame
4040 await _run()
4141 get_tree().root.get_node("/root/GameMode").set_mode("play")
42 if checks < 11:
42 if checks < 13:
4343 print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
4444 fails += 1
4545 print("\nGATE q_stack: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
……@@ -147,6 +147,27 @@ func _run() -> void:
147147 _ok("Q4 one Ctrl+Z: the shape is live again, both cuts live", (persist.live_shapes() as Dictionary).has(t3)
148148 and not bool(LevelPersistence.OpFold.resolve_states(persist.records(false))[oa]["applied"]))
149149
150 # ---- Q6 the Q menu and the keys ----
151 map.select_shape(t2)
152 await get_tree().process_frame
153 var acts: Array = []
154 for sec: Array in (map.q_sections() as Array):
155 for row: Array in (sec[1] as Array):
156 acts.append(String(row[0]))
157 _ok("Q6 Q on a shape with cuts offers Change cut, Extract, Apply up to here",
158 acts.has("builder.change_cut") and acts.has("builder.extract_cut") and acts.has("builder.apply_up_to"), acts)
159 var ax := InputEventKey.new()
160 ax.keycode = KEY_X
161 ax.physical_keycode = KEY_X
162 ax.alt_pressed = true
163 ax.pressed = true
164 get_viewport().push_input(ax)
165 await get_tree().process_frame
166 var q: Node = map.get("_q_menu")
167 _ok("Q6 ... Alt+X opens Change cut", q.is_open())
168 (q.get("_menu") as PopupMenu).hide()
169 map.select_brushes(PackedInt32Array())
170
150171 # ---- Q5 BoxCutter Y ----
151172 map._ensure_ops()
152173 var op: Node = map.get_node("BoxCutter")