OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / 82895c2

#120 L2 step 6: BoxCutter housekeeping (§13.5)
The cut goes into the aimed object AND every selected object (never a cutter); through-depth reaches through all of them. Every cutter is linked into one room collection "Cutters" (created on the first cut, in the same gesture). Shift+R repeats BoxCutter with the last cut's modifiers. Doc: the selection rule is BoxCutter's only (Ctrl+numpad CSG unchanged); Cutters is not hidden (its wires are the Shift+Alt+C overlay's). gate_box_cutter H1–H3 (37 total); op_editor, collection_focus green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82895c2879387b21409addb3d3367005a45b23e2
by plankatron <fleeky@gmail.com> · 2026-10-19T01:16:23.000Z

Changed files (4) +113 -15

docs/node-tree-and-stacks.md +6 -4
……@@ -821,13 +821,15 @@ collisions.
821821
822822### 13.5 Housekeeping
823823
824- **Targets = the whole selection.** `_live_cut` takes every selected object's group (not
825 only the active one), excluding cutters. `op.create.targets` already takes many.
824- **Targets = the whole selection, in BoxCutter.** Its cut goes into the aimed object and
825 every selected object's group, excluding cutters (`op.create.targets` takes many), and
826 through-depth reaches through all of them. `_live_cut` (the Ctrl+numpad CSG commands)
827 is UNCHANGED: there the other selected objects ARE the cutters.
826828- **Cutters collection.**
827829 - The first commit creates a room collection named "Cutters" (`col.create`, replicated
828830 like any collection) and links every new cutter into it.
829 - Its outliner rows start hidden. That is view state, kept per player.
830 - The Shift+Alt+C overlay still shows the wires.
831 - It is NOT hidden: hiding its rows would hide the cutters' wires, which belong to the
832 Shift+Alt+C overlay. It is organisation.
831833- **Q menu on a shape with cuts:**
832834 - **Change cut ▸** subtract / slice / intersect / inset / outset (`set_op`);
833835 - **Apply up to here ▸** <op>: `make_real` takes an op: it bakes the target as folded
scenes/maps/map_dc_level.gd +5 -1
……@@ -621,7 +621,7 @@ const ADD_SHAPES := ["box", "stairs", "spiral_stairs", "tunnel", "cylinder", "co
621621## What Shift+R repeats: the commands that start a tool or apply an edit (not views, not
622622## selection, not menus). Recorded as they fire (_note_repeatable).
623623const REPEATABLE := ["builder.move", "builder.rotate", "builder.scale", "builder.extrude",
624 "builder.duplicate", "builder.duplicate_linked", "builder.knife", "builder.create_box", "builder.texture_apply",
624 "builder.duplicate", "builder.duplicate_linked", "builder.knife", "builder.create_box", "builder.box_cutter", "builder.texture_apply",
625625 "build_mode.csg_subtract", "build_mode.csg_intersect", "build_mode.csg_slice",
626626 "build_mode.csg_merge", "builder.uv_hotspot", "builder.uv_fit", "builder.uv_align"]
627627var _last_repeatable := ""
……@@ -4166,6 +4166,10 @@ func _builder_key(event: InputEvent) -> void:
41664166 if _last_repeatable == "":
41674167 print("[MapDCLevel] repeat last: nothing to repeat yet")
41684168 else:
4169 # BoxCutter repeats with the last cut's modifiers (#120 L2 §13.5).
4170 var bcut := get_node_or_null("BoxCutter")
4171 if _last_repeatable == "builder.box_cutter" and bcut != null:
4172 bcut.restore_last_queue()
41694173 Keymap.run(_last_repeatable)
41704174 return
41714175 if Keymap.tapped(event, "builder.element_toggle"):
scenes/maps/operators/box_cutter_operator.gd +67 -9
……@@ -90,6 +90,56 @@ func _clear_queue() -> void:
9090 _live = true
9191
9292
93# ---- step 6: housekeeping (§13.5) ----
94
95## The last commit's queue, for Shift+R (restore_last_queue).
96var _last_queue: Dictionary = {}
97
98
99## Shift+R: the last cut's modifiers come back (the rest — mode, shape, depth — is sticky).
100func restore_last_queue() -> void:
101 if _last_queue.is_empty():
102 return
103 _q_chamfer = bool(_last_queue["chamfer"])
104 _q_array = String(_last_queue["array"])
105 _q_mirror = (_last_queue["mirror"] as Array).duplicate()
106 _q_grid = bool(_last_queue["grid"])
107
108
109func _remember_queue() -> void:
110 _last_queue = {"chamfer": _q_chamfer, "array": _q_array, "mirror": _q_mirror.duplicate(), "grid": _q_grid}
111
112
113## What the cut goes into: the aimed object, and every SELECTED object with it (each
114## brush's group, or its key when it has none), never a cutter.
115func _targets() -> Array:
116 var out: Array = []
117 if _target != "":
118 out.append(_target)
119 var sel: Variant = map.get("_sel_brushes")
120 for b: Variant in (sel as Array if sel is Array else []):
121 var bd: Dictionary = level.get_brush(int(b))
122 if bd.is_empty() or bool(bd.get("cutter", false)) or bool(bd.get("clearance", false)):
123 continue
124 var g := String(level.get_brush_group(int(b)))
125 var t := g if g != "" else String(level.get_brush_key(int(b)))
126 if t != "" and not out.has(t):
127 out.append(t)
128 return out
129
130
131## The room's "Cutters" collection, created (inside the caller's gesture) the first time.
132func _cutters_collection() -> String:
133 var t: Dictionary = map._persist.collection_tree()
134 var cols: Dictionary = t.get("cols", {})
135 var keys: Array = cols.keys()
136 keys.sort()
137 for cd: String in keys:
138 if String(cols[cd].get("name", "")) == "Cutters":
139 return cd
140 return String(map._persist.record("col.create", {"name": "Cutters", "parent": ""}))
141
142
93143## The target's centre: the middle of its solid pieces' corners.
94144func _target_centre() -> Vector3:
95145 var lo := Vector3(INF, INF, INF)
……@@ -452,14 +502,16 @@ func _depth() -> float:
452502
453503func _through() -> float:
454504 var pieces: Array = []
455 for b: int in (level.get_group_brush_ids(_target) as PackedInt32Array):
456 var bd: Dictionary = level.get_brush(b)
457 if not bool(bd.get("clearance", false)) and not bool(bd.get("cutter", false)):
458 pieces.append(bd["planes"])
459 if pieces.is_empty():
460 var id: int = level.brush_by_key(_target) if level.has_method("brush_by_key") else -1
461 if id >= 0:
462 pieces.append(level.get_brush(id)["planes"])
505 for t: String in _targets(): # through EVERY target
506 var before := pieces.size()
507 for b: int in (level.get_group_brush_ids(t) as PackedInt32Array):
508 var bd: Dictionary = level.get_brush(b)
509 if not bool(bd.get("clearance", false)) and not bool(bd.get("cutter", false)):
510 pieces.append(bd["planes"])
511 if pieces.size() == before:
512 var id: int = level.brush_by_key(t) if level.has_method("brush_by_key") else -1
513 if id >= 0:
514 pieces.append(level.get_brush(id)["planes"])
463515 return CutDepth.through(pieces, _o, _n, grid_step())
464516
465517
……@@ -563,7 +615,7 @@ func confirm() -> bool:
563615 "size": [size.x, size.y, size.z], "material_id": _material, "level_id": _level_id})
564616 var ok := dot != ""
565617 if ok:
566 var rec := {"op": mode(), "cutter": dot, "targets": [_target]}
618 var rec := {"op": mode(), "cutter": dot, "targets": _targets()}
567619 var odot: String = map._persist.record("op.create", rec)
568620 ok = odot != ""
569621 if ok and not _live:
……@@ -574,6 +626,11 @@ func confirm() -> bool:
574626 var p: Dictionary = (defaults.get(kp[0], {}) as Dictionary).duplicate(true)
575627 p.merge(kp[1] as Dictionary, true)
576628 map._persist.record("mod.create", {"target": dot, "mod": kp[0], "params": p})
629 # Every cutter in ONE "Cutters" collection (organisation; the wires are the
630 # cutters overlay's, Shift+Alt+C).
631 var col := _cutters_collection()
632 if col != "":
633 map._persist.record("col.link", {"target": col, "members": ["g:" + dot]})
577634 map._persist.end_gesture()
578635 if not ok:
579636 print("[boxcutter] the journal refused the cut")
……@@ -586,5 +643,6 @@ func confirm() -> bool:
586643 _phase = Phase.AIM
587644 _frozen = false
588645 _points.clear()
646 _remember_queue()
589647 _clear_queue()
590648 return true
scenes/test/gate_box_cutter.gd +35 -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 < 29:
42 if checks < 36:
4343 print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
4444 fails += 1
4545 print("\nGATE box_cutter: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
……@@ -263,3 +263,37 @@ func _run() -> void:
263263 and is_equal_approx(float(pp.get("shear_x", 0)), 0.25), pp)
264264 op.set_tool_setting("taper", 1.0)
265265 op.set_tool_setting("shear_x", 0.0)
266
267 # ---- step 6: housekeeping (§13.5) ----
268 # H1 the whole SELECTION is the target: two boxes selected, one rect across both.
269 var ha := _box(Vector3(220, 0, 0), Vector3(4, 2, 4))
270 var hb := _box(Vector3(226, 0, 0), Vector3(4, 2, 4))
271 var ids := PackedInt32Array()
272 ids.append_array(level.get_group_brush_ids(ha))
273 ids.append_array(level.get_group_brush_ids(hb))
274 map.select_brushes(ids)
275 op.place(Vector3(221, 2, 1), Vector3.UP, Vector3(221, 2, 1), Vector3(229, 2, 3), ha)
276 op.confirm()
277 var tg: Array = Array(_last("op.create").get("targets", []))
278 _ok("H1 both selected objects are targets", tg.has(ha) and tg.has(hb), tg)
279 _ok("H1 ... both are cut", _count(ha, Vector3(222, 1, 2)) == 0 and _count(hb, Vector3(228, 1, 2)) == 0)
280 map.select_brushes(PackedInt32Array())
281 # H2 every cutter is linked into ONE collection named "Cutters".
282 var cuts: Array = []
283 var tree: Dictionary = persist.collection_tree()
284 for cd: String in (tree.get("cols", {}) as Dictionary):
285 if String(tree["cols"][cd]["name"]) == "Cutters":
286 cuts.append(cd)
287 var hc := _last("shape.create")
288 var hcd := "%s:%d" % [hc["author"], int(hc["seq"])]
289 _ok("H2 one Cutters collection", cuts.size() == 1, cuts)
290 _ok("H2 ... holding the last cutter", cuts.size() == 1 and (tree["cols"][cuts[0]]["members"] as Array).has("g:" + hcd),
291 tree["cols"][cuts[0]]["members"] if cuts.size() == 1 else [])
292 # H3 Shift+R repeats with the last cut's modifiers: the queue comes back.
293 var tr3 := _box(Vector3(240, 0, 0), Vector3(4, 2, 4))
294 op.place(Vector3(241, 2, 1), Vector3.UP, Vector3(241, 2, 1), Vector3(243, 2, 3), tr3)
295 op.handle_key(_key(KEY_2))
296 op.confirm()
297 op.restore_last_queue()
298 _ok("H3 repeat restores the last queue (mirror y)", String(op._queue_text()) == "mirror y", op._queue_text())
299 _ok("H3 ... and Shift+R repeats the BoxCutter", (map.get("REPEATABLE") as Array).has("builder.box_cutter"))