| … | … | @@ -433,6 +433,9 @@ const DEFAULTS: Dictionary = { |
| 433 | 433 | "builder.change_cut": {"key": KEY_X, "alt": true, "label": "Change cut… (subtract / slice / intersect / inset / outset)"}, |
| 434 | 434 | "builder.extract_cut": {"key": KEY_Y, "alt": true, "label": "Extract… (a cut's target ∩ cutter becomes new shapes)"}, |
| 435 | 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)"}, |
| 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"}, | |
| 436 | 439 | "builder.adjust_last": {"key": KEY_F6, "label": "Adjust Last (the last operation's settings)"}, |
| 437 | 440 | "builder.history": {"key": KEY_Z, "ctrl": true, "alt": true, "label": "Undo History"}, |
| 438 | 441 | "builder.repeat_last": {"key": KEY_R, "shift": true, "label": "Repeat the last command (starts the tool again)"}, |
| … | … | @@ -787,6 +787,15 @@ func refold() -> Dictionary: |
| 787 | 787 | # LIVE CUTS: one brush.cut per live op, sorting after everything above; a |
| 788 | 788 | # target's copies get its cutters copied the same way (§3.3). |
| 789 | 789 | 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 | |
| 790 | 799 | ModFold.copy_cutters(mod_recs, cuts, shape_states, curves) |
| 791 | 800 | brush_recs.append_array(cuts) |
| 792 | 801 | # CHAMFERS last: they must see the cut pieces' planes (§3.3). |
| … | … | @@ -1191,6 +1200,17 @@ func records(with_objects: bool = true) -> Array: |
| 1191 | 1200 | |
| 1192 | 1201 | ## The room's collection tree (CollectionFold.tree): {"cols": {dot: {name, parent, |
| 1193 | 1202 | ## 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. | |
| 1206 | func 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 | ||
| 1194 | 1214 | func collection_tree() -> Dictionary: |
| 1195 | 1215 | _ensure_index() |
| 1196 | 1216 | var recs: Array = [] |
| … | … | @@ -3027,6 +3027,9 @@ func _ensure_ops() -> OperatorStack: |
| 3027 | 3027 | _extrude = _make_op(ExtrudeOperator.new(), "Extrude") |
| 3028 | 3028 | _box = _make_op(CreateBoxOperator.new(), "CreateBox") |
| 3029 | 3029 | _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)) | |
| 3030 | 3033 | # The HUD shows which operator is live and what it is set to, so a modal tool is |
| 3031 | 3034 | # never a mode you are in without being told. |
| 3032 | 3035 | _ops.changed.connect(func(_op: LevelOperator) -> void: _update_op_hud()) |
| … | … | @@ -4358,6 +4361,9 @@ func _builder_key(event: InputEvent) -> void: |
| 4358 | 4361 | rows.append([k.capitalize(), func() -> void: add_modifier(k), MOD_KEYS.get(k, 0)]) |
| 4359 | 4362 | _q_menu.open_choices("Add modifier", rows, get_viewport().get_mouse_position()) |
| 4360 | 4363 | 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 | |
| 4361 | 4367 | # #120 L2 §13.4–13.5: Change cut / Extract / Apply up to here — each picks one of the |
| 4362 | 4368 | # subject's cuts from a list (StackOps). |
| 4363 | 4369 | for sa: Array in [["builder.change_cut", "Change cut"], ["builder.extract_cut", "Extract"], |
| … | … | @@ -138,6 +138,53 @@ static func extract(map: Node, _subject: String, op_dot: String) -> Array[String |
| 138 | 138 | return made |
| 139 | 139 | |
| 140 | 140 | |
| 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). | |
| 149 | static 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. | |
| 171 | static 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. | |
| 183 | static 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 | ||
| 141 | 188 | # ---- APPLY UP TO HERE ------------------------------------------------------------------ |
| 142 | 189 | |
| 143 | 190 | ## Apply up to here ▸ <op> on the SHAPE `target`: bake it as folded with only the ops up to |
| … | … | @@ -39,7 +39,7 @@ func _ready() -> void: |
| 39 | 39 | await get_tree().process_frame |
| 40 | 40 | await _run() |
| 41 | 41 | get_tree().root.get_node("/root/GameMode").set_mode("play") |
| 42 | if checks < 13: | |
| 42 | if checks < 19: | |
| 43 | 43 | print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks) |
| 44 | 44 | fails += 1 |
| 45 | 45 | print("\nGATE q_stack: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails]) |
| … | … | @@ -183,3 +183,38 @@ func _run() -> void: |
| 183 | 183 | var nops2 := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size() |
| 184 | 184 | _ok("Q5 Y extracts the drawn shape ∩ the target, and cuts nothing", nops2 == nops and _count(t5, Vector3(342, 1, 2)) == 1 |
| 185 | 185 | 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) |