OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / ca733ba

#120 L2 step 8: stack scroll preview (§13.5)
Ctrl+Alt+[ / ] shows the selected subject as if only its first k cuts existed — HardOps' modifier_scroll as a LOCAL view: the skipped op dots go to LevelPersistence.set_preview_off, which the cut expansion filters and which forces the next refold to run in full (the brush fold's "nothing changed" skip would otherwise keep the old cuts). Nothing is journaled. A selection change off the subject or a tool starting clears it. gate_q_stack S1–S6 (19); refold_skip, keymap, box_cutter, op_journal, q_menu, op_inset green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ca733bacf2ab83e0339256e1c6b4849d7d4b0a45
by plankatron <fleeky@gmail.com> · 2026-10-19T01:32:35.000Z

Changed files (5) +112 -1

autoloads/keymap.gd +3 -0
……@@ -433,6 +433,9 @@ const DEFAULTS: Dictionary = {
433433 "builder.change_cut": {"key": KEY_X, "alt": true, "label": "Change cut… (subtract / slice / intersect / inset / outset)"},
434434 "builder.extract_cut": {"key": KEY_Y, "alt": true, "label": "Extract… (a cut's target ∩ cutter becomes new shapes)"},
435435 "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)"},
436 # Stack scroll (HardOps' modifier_scroll): a LOCAL preview of the first k cuts.
437 "builder.stack_scroll_back": {"key": KEY_BRACKETLEFT, "ctrl": true, "alt": true, "label": "Stack scroll: preview one cut fewer"},
438 "builder.stack_scroll_fwd": {"key": KEY_BRACKETRIGHT, "ctrl": true, "alt": true, "label": "Stack scroll: preview one cut more"},
436439 "builder.adjust_last": {"key": KEY_F6, "label": "Adjust Last (the last operation's settings)"},
437440 "builder.history": {"key": KEY_Z, "ctrl": true, "alt": true, "label": "Undo History"},
438441 "builder.repeat_last": {"key": KEY_R, "shift": true, "label": "Repeat the last command (starts the tool again)"},
scenes/maps/level_persistence.gd +20 -0
……@@ -787,6 +787,15 @@ func refold() -> Dictionary:
787787 # LIVE CUTS: one brush.cut per live op, sorting after everything above; a
788788 # target's copies get its cutters copied the same way (§3.3).
789789 var cuts: Array = OpFold.expand(op_recs, shaped)
790 # STACK SCROLL (#120 L2 §13.5): a LOCAL preview skips the ops after row k —
791 # never journaled; StackOps.scroll sets it, a selection change clears it.
792 var off: Dictionary = get_meta("preview_off", {})
793 if not off.is_empty():
794 var kept: Array = []
795 for c: Dictionary in cuts:
796 if not off.has("%s:%d" % [String(c["author"]), int(c["seq"]) - OpFold.OP_SEQ_BASE]):
797 kept.append(c)
798 cuts = kept
790799 ModFold.copy_cutters(mod_recs, cuts, shape_states, curves)
791800 brush_recs.append_array(cuts)
792801 # CHAMFERS last: they must see the cut pieces' planes (§3.3).
……@@ -1191,6 +1200,17 @@ func records(with_objects: bool = true) -> Array:
11911200
11921201## The room's collection tree (CollectionFold.tree): {"cols": {dot: {name, parent,
11931202## children, members}}, "ignored": [...]}.
1203## THE STACK SCROLL PREVIEW (#120 L2 §13.5): the op dots the fold skips, LOCALLY. It
1204## changes no record, so the brush fold's "nothing changed" skip must be told: the next
1205## refold runs in full. {} ends the preview.
1206func set_preview_off(off: Dictionary) -> void:
1207 if off.is_empty():
1208 remove_meta("preview_off")
1209 else:
1210 set_meta("preview_off", off)
1211 _brush_ver_folded = -1
1212
1213
11941214func collection_tree() -> Dictionary:
11951215 _ensure_index()
11961216 var recs: Array = []
scenes/maps/map_dc_level.gd +6 -0
……@@ -3027,6 +3027,9 @@ func _ensure_ops() -> OperatorStack:
30273027 _extrude = _make_op(ExtrudeOperator.new(), "Extrude")
30283028 _box = _make_op(CreateBoxOperator.new(), "CreateBox")
30293029 _make_op(BoxCutterOperator.new(), "BoxCutter") # #120 L2 §13: found by name ("BoxCutter")
3030 # The stack scroll preview ends when the selection moves off its subject or a tool starts.
3031 selection_changed.connect(func() -> void: preload("res://scenes/maps/stack_ops.gd").clear_if_moved(self))
3032 _ops.changed.connect(func(op: LevelOperator) -> void: if op != null: preload("res://scenes/maps/stack_ops.gd").scroll_clear(self))
30303033 # The HUD shows which operator is live and what it is set to, so a modal tool is
30313034 # never a mode you are in without being told.
30323035 _ops.changed.connect(func(_op: LevelOperator) -> void: _update_op_hud())
……@@ -4358,6 +4361,9 @@ func _builder_key(event: InputEvent) -> void:
43584361 rows.append([k.capitalize(), func() -> void: add_modifier(k), MOD_KEYS.get(k, 0)])
43594362 _q_menu.open_choices("Add modifier", rows, get_viewport().get_mouse_position())
43604363 return
4364 if Keymap.tapped(event, "builder.stack_scroll_back") or Keymap.tapped(event, "builder.stack_scroll_fwd"):
4365 preload("res://scenes/maps/stack_ops.gd").scroll(self, -1 if Keymap.tapped(event, "builder.stack_scroll_back") else 1)
4366 return
43614367 # #120 L2 §13.4–13.5: Change cut / Extract / Apply up to here — each picks one of the
43624368 # subject's cuts from a list (StackOps).
43634369 for sa: Array in [["builder.change_cut", "Change cut"], ["builder.extract_cut", "Extract"],
scenes/maps/stack_ops.gd +47 -0
……@@ -138,6 +138,53 @@ static func extract(map: Node, _subject: String, op_dot: String) -> Array[String
138138 return made
139139
140140
141# ---- STACK SCROLL (HardOps' modifier_scroll, as a VIEW) ---------------------------------
142#
143# Ctrl+Alt+[ / ] shows the subject as if only its first k cuts existed. LOCAL: the ops
144# after row k go in the persistence node's "preview_off" meta, which the fold skips when
145# it expands the cuts; nothing is journaled, so no peer ever sees it. A selection change
146# or a tool starting clears it (scroll_clear / clear_if_moved).
147
148## Step the preview of the selected subject's stack by `delta` rows (−1 = one cut fewer).
149static func scroll(map: Node, delta: int) -> void:
150 map._ensure_persistence()
151 var p: Node = map.get("_persist")
152 var subject: String = map.stack_subject()
153 var ops: Array = map.op_stack(subject)
154 if subject == "" or ops.is_empty():
155 return
156 var st: Dictionary = map.get_meta("stack_scroll", {})
157 var k := ops.size() if String(st.get("subject", "")) != subject else int(st["k"])
158 k = clampi(k + delta, 0, ops.size())
159 var off := {}
160 for i in range(k, ops.size()):
161 off[String(ops[i]["dot"])] = true
162 map.set_meta("stack_scroll", {"subject": subject, "k": k})
163 p.set_preview_off(off)
164 p.refold()
165 if map.has_method("_refresh_brush_view"):
166 map._refresh_brush_view()
167 ModalControls.summary("Stack: %d of %d cut(s) shown%s" % [k, ops.size(), "" if k == ops.size() else " (preview)"])
168
169
170## End the preview: every cut shows again.
171static func scroll_clear(map: Node) -> void:
172 var p: Variant = map.get("_persist")
173 if not map.has_meta("stack_scroll") or p == null:
174 return
175 map.remove_meta("stack_scroll")
176 (p as Node).set_preview_off({})
177 (p as Node).refold()
178 if map.has_method("_refresh_brush_view"):
179 map._refresh_brush_view()
180
181
182## The selection moved off the previewed subject: end the preview.
183static func clear_if_moved(map: Node) -> void:
184 if map.has_meta("stack_scroll") and String((map.get_meta("stack_scroll") as Dictionary)["subject"]) != String(map.stack_subject()):
185 scroll_clear(map)
186
187
141188# ---- APPLY UP TO HERE ------------------------------------------------------------------
142189
143190## Apply up to here ▸ <op> on the SHAPE `target`: bake it as folded with only the ops up to
scenes/test/gate_q_stack.gd +36 -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 < 13:
42 if checks < 19:
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])
……@@ -183,3 +183,38 @@ func _run() -> void:
183183 var nops2 := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size()
184184 _ok("Q5 Y extracts the drawn shape ∩ the target, and cuts nothing", nops2 == nops and _count(t5, Vector3(342, 1, 2)) == 1
185185 and (_col_named("Extract").size() >= 2), [nops2 - nops, _count(t5, Vector3(342, 1, 2)), _col_named("Extract").size()])
186
187 # ---- S: the stack scroll preview (§13.5, step 8) — LOCAL, never journaled ----
188 var t8 := _box(Vector3(360, 0, 0), Vector3(6, 2, 4))
189 var c8a := _box(Vector3(360.5, -1, 1), Vector3(1, 4, 2))
190 var c8b := _box(Vector3(364.5, -1, 1), Vector3(1, 4, 2))
191 _cut("subtract", c8a, t8)
192 _cut("subtract", c8b, t8)
193 map.select_shape(t8)
194 var nrec := (persist.records(false) as Array).size()
195 StackOps.scroll(map, -1)
196 _ok("S1 scroll back one: the first cut shows, the second does not",
197 _count(t8, Vector3(361, 1, 2)) == 0 and _count(t8, Vector3(365, 1, 2)) == 1)
198 StackOps.scroll(map, -1)
199 _ok("S2 back again: no cuts show", _count(t8, Vector3(361, 1, 2)) == 1 and _count(t8, Vector3(365, 1, 2)) == 1)
200 _ok("S3 nothing was journaled", (persist.records(false) as Array).size() == nrec, (persist.records(false) as Array).size() - nrec)
201 StackOps.scroll(map, 1)
202 StackOps.scroll(map, 1)
203 _ok("S4 forward twice: both cuts again", _count(t8, Vector3(361, 1, 2)) == 0 and _count(t8, Vector3(365, 1, 2)) == 0)
204 StackOps.scroll(map, -1)
205 map.select_brushes(PackedInt32Array())
206 await get_tree().process_frame
207 _ok("S5 a selection change clears the preview", _count(t8, Vector3(365, 1, 2)) == 0
208 and (not persist.has_meta("preview_off") or (persist.get_meta("preview_off") as Dictionary).is_empty()))
209 map.select_shape(t8)
210 await get_tree().process_frame
211 var kb := InputEventKey.new()
212 kb.keycode = KEY_BRACKETLEFT
213 kb.physical_keycode = KEY_BRACKETLEFT
214 kb.ctrl_pressed = true
215 kb.alt_pressed = true
216 kb.pressed = true
217 get_viewport().push_input(kb)
218 await get_tree().process_frame
219 _ok("S6 Ctrl+Alt+[ scrolls the selected shape's stack back", _count(t8, Vector3(365, 1, 2)) == 1)
220 StackOps.scroll_clear(map)