| … | … | @@ -228,6 +228,34 @@ func _open_menu() -> void: |
| 228 | 228 | q.open_choices("BoxCutter", rows, map.get_viewport().get_mouse_position()) |
| 229 | 229 | |
| 230 | 230 | |
| 231 | ## Y (BoxCutter's extract): the drawn shape ∩ the targets becomes new hull shapes (an | |
| 232 | ## "Extract" collection), and NOTHING is cut — no cutter, no op. One gesture. | |
| 233 | func _extract_now() -> void: | |
| 234 | var fs := _frame() | |
| 235 | if fs.is_empty() or _targets().is_empty(): | |
| 236 | print("[boxcutter] Y: draw a footprint on a target first") | |
| 237 | return | |
| 238 | var prism: Object = ParamShapes.get_type("prism") | |
| 239 | var params: Dictionary = prism.resolve({"poly": fs[2], "taper": _taper, "shear_x": _shear_x, "shear_z": _shear_z}) | |
| 240 | var cutters: Array = [] | |
| 241 | for pc: Dictionary in (prism.local_pieces(fs[1], params) as Array): | |
| 242 | cutters.append(ShapeFold.transform_planes(pc["planes"], fs[0])) | |
| 243 | map._ensure_persistence() | |
| 244 | map._persist.begin_gesture("BoxCutter extract") | |
| 245 | var made: Array[String] = StackOps.extract_planes(map, _targets(), cutters) | |
| 246 | map._persist.end_gesture() | |
| 247 | map._persist.refold() | |
| 248 | print("[boxcutter] extracted %d shape(s)" % made.size()) | |
| 249 | ModalControls.summary("BoxCutter extract · %d shape(s)" % made.size()) | |
| 250 | _phase = Phase.AIM | |
| 251 | _frozen = false | |
| 252 | _points.clear() | |
| 253 | ||
| 254 | ||
| 255 | const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd") | |
| 256 | const StackOps := preload("res://scenes/maps/stack_ops.gd") | |
| 257 | ||
| 258 | ||
| 231 | 259 | func _set_wedge(x: float, z: float) -> void: |
| 232 | 260 | _shear_x = x |
| 233 | 261 | _shear_z = z |
| … | … | @@ -376,6 +404,9 @@ func handle_key(event: InputEvent) -> bool: |
| 376 | 404 | KEY_L: |
| 377 | 405 | _live = not _live |
| 378 | 406 | return true |
| 407 | KEY_Y: | |
| 408 | _extract_now() | |
| 409 | return true | |
| 379 | 410 | KEY_X: |
| 380 | 411 | _mode = (_mode + 1) % MODES.size() |
| 381 | 412 | return true |
| … | … | @@ -0,0 +1,204 @@ |
| 1 | extends RefCounted | |
| 2 | ## THE Q MENU'S STACK OPERATIONS (#120 L2, docs/node-tree-and-stacks.md §13.4–13.5): | |
| 3 | ## Change cut, Extract, Apply up to here. Static, taking the map, like CollectionOps; each | |
| 4 | ## is ONE gesture, so one Ctrl+Z takes it back. | |
| 5 | ## | |
| 6 | ## Two of them need the target AS IT WAS before some of its cuts. They get it the journal's | |
| 7 | ## way, inside their own gesture: switch those ops off (op.set), refold, read the pieces, | |
| 8 | ## and switch them back on. The net state is the ops on again; the off / on pair rides in | |
| 9 | ## the same gesture, so nothing in between is ever seen by a peer as a separate step. | |
| 10 | ||
| 11 | const OpFold := preload("res://scenes/maps/op_fold.gd") | |
| 12 | ||
| 13 | ||
| 14 | ## Change cut ▸: one row per (op on `subject`, kind it is not): picking it is set_op. | |
| 15 | static func change_rows(map: Node, subject: String) -> Array: | |
| 16 | var rows: Array = [] | |
| 17 | for o: Dictionary in (map.op_stack(subject) as Array): | |
| 18 | for k: String in OpFold.CUT_OPS: | |
| 19 | if k == String(o["kind"]): | |
| 20 | continue | |
| 21 | rows.append(["%s → %s" % [_label(map, o), k], _set_kind.bind(map, String(o["dot"]), k)]) | |
| 22 | return rows | |
| 23 | ||
| 24 | ||
| 25 | static func _set_kind(map: Node, dot: String, kind: String) -> void: | |
| 26 | map.set_op(dot, {"op": kind}) | |
| 27 | ||
| 28 | ||
| 29 | static func _label(map: Node, o: Dictionary) -> String: | |
| 30 | return "%s %s" % [String(o["kind"]), String(o["dot"]).get_slice(":", 1)] | |
| 31 | ||
| 32 | ||
| 33 | ## Rows naming each op on `subject`, picking runs `fn(map, subject, op_dot)`. | |
| 34 | static func op_rows(map: Node, subject: String, fn: Callable) -> Array: | |
| 35 | var rows: Array = [] | |
| 36 | for o: Dictionary in (map.op_stack(subject) as Array): | |
| 37 | rows.append([_label(map, o), fn.bind(map, subject, String(o["dot"]))]) | |
| 38 | return rows | |
| 39 | ||
| 40 | ||
| 41 | # ---- EXTRACT ------------------------------------------------------------------------- | |
| 42 | ||
| 43 | ## A world plane set as a HULL shape.create payload, framed on the world axes at its | |
| 44 | ## bounds' low corner (make_parametric's mapping, with an identity basis). | |
| 45 | static func hull_payload(planes: PackedFloat32Array, material: int, level_id: int) -> Dictionary: | |
| 46 | var cs := FaceKit.corners(planes) | |
| 47 | var lo := Vector3.INF | |
| 48 | var hi := -Vector3.INF | |
| 49 | for c: Vector3 in cs: | |
| 50 | lo = lo.min(c) | |
| 51 | hi = hi.max(c) | |
| 52 | var size := (hi - lo).max(Vector3.ONE * 1.0e-4) | |
| 53 | var unit: Array = [] | |
| 54 | for i in planes.size() / 4: | |
| 55 | var n := Vector3(planes[i * 4], planes[i * 4 + 1], planes[i * 4 + 2]) | |
| 56 | var m := n * size | |
| 57 | var dd := planes[i * 4 + 3] - n.dot(lo) | |
| 58 | var l := maxf(m.length(), 1.0e-12) | |
| 59 | unit.append_array([m.x / l, m.y / l, m.z / l, dd / l]) | |
| 60 | return {"type": "hull", "gv": 1, "params": {"planes": unit, "face_tex": []}, | |
| 61 | "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, lo)), "size": [size.x, size.y, size.z], | |
| 62 | "material_id": material, "level_id": level_id, "tex": {}} | |
| 63 | ||
| 64 | ||
| 65 | static func _has_volume(planes: PackedFloat32Array) -> bool: | |
| 66 | var cs := FaceKit.corners(planes) | |
| 67 | if cs.size() < 4: | |
| 68 | return false | |
| 69 | var lo := cs[0] | |
| 70 | var hi := cs[0] | |
| 71 | for v: Vector3 in cs: | |
| 72 | lo = lo.min(v) | |
| 73 | hi = hi.max(v) | |
| 74 | var e := hi - lo | |
| 75 | return e.x > 1.0e-3 and e.y > 1.0e-3 and e.z > 1.0e-3 | |
| 76 | ||
| 77 | ||
| 78 | ## The SOLID pieces of `groups` in the level now: [{planes, material, level_id}]. | |
| 79 | static func solids_of(map: Node, groups: Array) -> Array: | |
| 80 | var level: Node = map.get("level") | |
| 81 | var out: Array = [] | |
| 82 | for g: Variant in groups: | |
| 83 | for b: int in (level.get_group_brush_ids(String(g)) as PackedInt32Array): | |
| 84 | var d: Dictionary = level.get_brush(b) | |
| 85 | if bool(d.get("clearance", false)) or bool(d.get("cutter", false)): | |
| 86 | continue | |
| 87 | out.append({"planes": d["planes"], "material": int(d.get("material_id", 0)), | |
| 88 | "level_id": int(d.get("level_id", 0))}) | |
| 89 | return out | |
| 90 | ||
| 91 | ||
| 92 | ## Every target piece ∩ cutter piece with volume, as new HULL shapes in a new "Extract" | |
| 93 | ## collection. INSIDE the caller's gesture. Returns the new shape dots. | |
| 94 | static func extract_planes(map: Node, targets: Array, cutters: Array) -> Array[String]: | |
| 95 | var p: Node = map.get("_persist") | |
| 96 | var made: Array[String] = [] | |
| 97 | for t: Dictionary in solids_of(map, targets): | |
| 98 | for c: Variant in cutters: | |
| 99 | var pl: PackedFloat32Array = (t["planes"] as PackedFloat32Array) + (c as PackedFloat32Array) | |
| 100 | if not _has_volume(pl): | |
| 101 | continue | |
| 102 | var sd: String = p.record("shape.create", hull_payload(pl, int(t["material"]), int(t["level_id"]))) | |
| 103 | if sd != "": | |
| 104 | made.append(sd) | |
| 105 | if not made.is_empty(): | |
| 106 | var col: String = p.record("col.create", {"name": "Extract", "parent": ""}) | |
| 107 | if col != "": | |
| 108 | var members: Array = [] | |
| 109 | for sd2: String in made: | |
| 110 | members.append("g:" + sd2) | |
| 111 | p.record("col.link", {"target": col, "members": members}) | |
| 112 | return made | |
| 113 | ||
| 114 | ||
| 115 | ## Extract ▸ <op>: the target as it was BEFORE that cut, ∩ its cutter, as new shapes. | |
| 116 | static func extract(map: Node, _subject: String, op_dot: String) -> Array[String]: | |
| 117 | map._ensure_persistence() | |
| 118 | var p: Node = map.get("_persist") | |
| 119 | var st: Dictionary = OpFold.resolve_states(p.records(false)).get(op_dot, {}) | |
| 120 | if st.is_empty(): | |
| 121 | return [] | |
| 122 | var cutters: Array = [] | |
| 123 | for b: int in (map.get("level").get_group_brush_ids(String(st["cutter"])) as PackedInt32Array): | |
| 124 | var d: Dictionary = map.get("level").get_brush(b) | |
| 125 | if bool(d.get("cutter", false)): | |
| 126 | cutters.append(d["planes"]) | |
| 127 | var was_on := bool(st.get("on", true)) | |
| 128 | p.begin_gesture("Extract") | |
| 129 | if was_on: | |
| 130 | p.record("op.set", {"target": op_dot, "on": false}) | |
| 131 | p.refold() | |
| 132 | var made := extract_planes(map, Array(st["targets"]), cutters) | |
| 133 | if was_on: | |
| 134 | p.record("op.set", {"target": op_dot, "on": true}) | |
| 135 | p.end_gesture() | |
| 136 | p.refold() | |
| 137 | print("[stack] extracted %d shape(s) from %s" % [made.size(), op_dot]) | |
| 138 | return made | |
| 139 | ||
| 140 | ||
| 141 | # ---- APPLY UP TO HERE ------------------------------------------------------------------ | |
| 142 | ||
| 143 | ## Apply up to here ▸ <op> on the SHAPE `target`: bake it as folded with only the ops up to | |
| 144 | ## and including `op_dot` live, and retire those (op.apply; a cutter no other live op uses | |
| 145 | ## goes too). The baked pieces keep the TARGET's own name as their group, so the LATER ops | |
| 146 | ## — which name it — go on cutting them. Returns the piece count (0 = refused). | |
| 147 | static func apply_up_to(map: Node, target: String, op_dot: String) -> int: | |
| 148 | map._ensure_persistence() | |
| 149 | var p: Node = map.get("_persist") | |
| 150 | if not (p.live_shapes() as Dictionary).has(target): | |
| 151 | print("[stack] apply up to here: %s is not a live shape (Make Real works on shapes)" % target) | |
| 152 | return 0 | |
| 153 | var ops: Array = OpFold.existing_ops(p.records(false)) | |
| 154 | var mine: Array = [] # this target's ops, in stack order | |
| 155 | for s: Dictionary in ops: | |
| 156 | if (s["targets"] as PackedStringArray).has(target): | |
| 157 | mine.append(s) | |
| 158 | var k := -1 | |
| 159 | for i in mine.size(): | |
| 160 | if String(mine[i]["dot"]) == op_dot: | |
| 161 | k = i | |
| 162 | if k < 0: | |
| 163 | return 0 | |
| 164 | var later_on: Array = [] | |
| 165 | for i2 in range(k + 1, mine.size()): | |
| 166 | if bool(mine[i2].get("on", true)): | |
| 167 | later_on.append(String(mine[i2]["dot"])) | |
| 168 | var level: Node = map.get("level") | |
| 169 | p.begin_gesture("Apply up to here") | |
| 170 | for d: String in later_on: | |
| 171 | p.record("op.set", {"target": d, "on": false}) | |
| 172 | p.refold() | |
| 173 | var n := 0 | |
| 174 | for id: int in (level.get_group_brush_ids(target) as PackedInt32Array): | |
| 175 | var bd: Dictionary = level.get_brush(id) | |
| 176 | if bool(bd.get("cutter", false)): | |
| 177 | continue | |
| 178 | var payload: Dictionary = p._brush_payload(bd) | |
| 179 | payload["group"] = target # the later ops' target name | |
| 180 | if p.record("brush.create", payload) != "": | |
| 181 | n += 1 | |
| 182 | p.record("shape.detach", {"target": target}) | |
| 183 | var retired := {} | |
| 184 | for i3 in range(0, k + 1): | |
| 185 | var s3: Dictionary = mine[i3] | |
| 186 | if (s3["targets"] as PackedStringArray).size() == 1: | |
| 187 | p.record("op.apply", {"target": s3["dot"]}) | |
| 188 | retired[s3["dot"]] = true | |
| 189 | for s4: Dictionary in ops: | |
| 190 | var c := String(s4["cutter"]) | |
| 191 | if not retired.has(s4["dot"]) or c == "": | |
| 192 | continue | |
| 193 | var used := false | |
| 194 | for s5: Dictionary in ops: | |
| 195 | if not retired.has(s5["dot"]) and String(s5["cutter"]) == c: | |
| 196 | used = true | |
| 197 | if not used: | |
| 198 | p.record("shape.remove", {"target": c}) | |
| 199 | for d2: String in later_on: | |
| 200 | p.record("op.set", {"target": d2, "on": true}) | |
| 201 | p.end_gesture() | |
| 202 | p.refold() | |
| 203 | print("[stack] applied %d cut(s) of %s: %d piece(s); %d later cut(s) stay live" % [retired.size(), target, n, later_on.size()]) | |
| 204 | return n |
| … | … | @@ -0,0 +1,164 @@ |
| 1 | extends Node | |
| 2 | ## GATE — THE Q MENU'S STACK OPERATIONS (#120 L2 §13.4–13.5, stack_ops.gd). | |
| 3 | ## Q1 Change cut: the rows offer the other kinds; picking one changes the op's kind | |
| 4 | ## Q2 Extract: target ∩ cutter (as BEFORE the cut) becomes ONE new hull shape in an | |
| 5 | ## "Extract" collection; the cut stays on (the hole is still there) | |
| 6 | ## Q3 Apply up to here (first of two cuts): the first is baked (op.apply), the target is | |
| 7 | ## plain brushes keeping its name, the second cut stays LIVE and still cuts | |
| 8 | ## Q4 ... one Ctrl+Z: the shape is live again with both cuts | |
| 9 | ## Q5 BoxCutter Y: the drawn shape ∩ the target becomes a new shape, nothing is cut | |
| 10 | ## | |
| 11 | ## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_q_stack.tscn | |
| 12 | ||
| 13 | const StackOps := preload("res://scenes/maps/stack_ops.gd") | |
| 14 | ||
| 15 | var fails := 0 | |
| 16 | var checks := 0 | |
| 17 | var map: Node3D | |
| 18 | var persist: Node | |
| 19 | var level: Node | |
| 20 | ||
| 21 | ||
| 22 | func _ok(what: String, cond: bool, note: Variant = "") -> void: | |
| 23 | checks += 1 | |
| 24 | if not cond: | |
| 25 | fails += 1 | |
| 26 | print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)]) | |
| 27 | ||
| 28 | ||
| 29 | func _ready() -> void: | |
| 30 | var gs: Node = get_tree().root.get_node_or_null("/root/GameState") | |
| 31 | gs.route("world:level:*", 1, "peer") | |
| 32 | var keet: Node = get_tree().root.get_node_or_null("/root/KeetService") | |
| 33 | if keet != null: | |
| 34 | keet.current_member_id = "gate-author" | |
| 35 | get_tree().root.get_node("/root/GameMode").set_mode("build") | |
| 36 | map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate() | |
| 37 | add_child(map) | |
| 38 | for i in 6: | |
| 39 | await get_tree().process_frame | |
| 40 | await _run() | |
| 41 | get_tree().root.get_node("/root/GameMode").set_mode("play") | |
| 42 | if checks < 11: | |
| 43 | print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks) | |
| 44 | fails += 1 | |
| 45 | print("\nGATE q_stack: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails]) | |
| 46 | get_tree().quit(0 if fails == 0 else 1) | |
| 47 | ||
| 48 | ||
| 49 | func _box(at: Vector3, size: Vector3) -> String: | |
| 50 | var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {}, | |
| 51 | "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), | |
| 52 | "size": [size.x, size.y, size.z], "material_id": 2, "level_id": 0}) | |
| 53 | persist.refold() | |
| 54 | return d | |
| 55 | ||
| 56 | ||
| 57 | func _cut(kind: String, cutter: String, target: String) -> String: | |
| 58 | persist.begin_gesture("cut") | |
| 59 | var d: String = persist.record("op.create", {"op": kind, "cutter": cutter, "targets": [target]}) | |
| 60 | persist.end_gesture() | |
| 61 | persist.refold() | |
| 62 | return d | |
| 63 | ||
| 64 | ||
| 65 | func _count(group: String, p: Vector3) -> int: | |
| 66 | var n := 0 | |
| 67 | for b: int in (level.get_group_brush_ids(group) as PackedInt32Array): | |
| 68 | var d: Dictionary = level.get_brush(b) | |
| 69 | if bool(d.get("clearance", false)) or bool(d.get("cutter", false)): | |
| 70 | continue | |
| 71 | var pl: PackedFloat32Array = d["planes"] | |
| 72 | var inside := true | |
| 73 | for i in pl.size() / 4: | |
| 74 | if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).dot(p) > pl[i * 4 + 3] + 1.0e-4: | |
| 75 | inside = false | |
| 76 | break | |
| 77 | if inside: | |
| 78 | n += 1 | |
| 79 | return n | |
| 80 | ||
| 81 | ||
| 82 | func _kind(op: String) -> String: | |
| 83 | return String(LevelPersistence.OpFold.resolve_states(persist.records(false)).get(op, {}).get("kind", "")) | |
| 84 | ||
| 85 | ||
| 86 | func _col_named(name: String) -> Array: | |
| 87 | var out: Array = [] | |
| 88 | var t: Dictionary = persist.collection_tree() | |
| 89 | for cd: String in (t.get("cols", {}) as Dictionary): | |
| 90 | if String(t["cols"][cd]["name"]) == name: | |
| 91 | out.append(cd) | |
| 92 | return out | |
| 93 | ||
| 94 | ||
| 95 | func _run() -> void: | |
| 96 | persist = map.get("_persist") | |
| 97 | level = map.get("level") | |
| 98 | persist.refold() | |
| 99 | ||
| 100 | # ---- Q1 change cut ---- | |
| 101 | var t1 := _box(Vector3(300, 0, 0), Vector3(4, 2, 4)) | |
| 102 | var c1 := _box(Vector3(301, -1, 1), Vector3(2, 4, 2)) | |
| 103 | var o1 := _cut("subtract", c1, t1) | |
| 104 | var rows: Array = StackOps.change_rows(map, t1) | |
| 105 | var texts: Array = rows.map(func(r: Array) -> String: return String(r[0])) | |
| 106 | _ok("Q1 Change cut offers the other four kinds", rows.size() == 4 and not str(texts).contains("→ subtract"), texts) | |
| 107 | for r: Array in rows: | |
| 108 | if String(r[0]).ends_with("→ slice"): | |
| 109 | (r[1] as Callable).call() | |
| 110 | _ok("Q1 ... picking slice changes the op", _kind(o1) == "slice", _kind(o1)) | |
| 111 | ||
| 112 | # ---- Q2 extract ---- | |
| 113 | var t2 := _box(Vector3(310, 0, 0), Vector3(4, 2, 4)) | |
| 114 | var c2 := _box(Vector3(311, 1, 1), Vector3(2, 2, 2)) # dips 1 into the top | |
| 115 | var o2 := _cut("subtract", c2, t2) | |
| 116 | var made: Array = StackOps.extract(map, t2, o2) | |
| 117 | _ok("Q2 Extract makes one new shape", made.size() == 1, made) | |
| 118 | if made.size() == 1: | |
| 119 | var lo := Vector3.INF | |
| 120 | var hi := -Vector3.INF | |
| 121 | for b: int in (level.get_group_brush_ids(String(made[0])) as PackedInt32Array): | |
| 122 | for c: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]): | |
| 123 | lo = lo.min(c) | |
| 124 | hi = hi.max(c) | |
| 125 | _ok("Q2 ... it is target ∩ cutter: x 311..313, y 1..2, z 1..3", | |
| 126 | lo.is_equal_approx(Vector3(311, 1, 1)) and hi.is_equal_approx(Vector3(313, 2, 3)), [lo, hi]) | |
| 127 | var ex := _col_named("Extract") | |
| 128 | _ok("Q2 ... in an Extract collection", ex.size() >= 1 and (persist.collection_tree()["cols"][ex[-1]]["members"] as Array).has("g:" + String(made[0]) if made.size() == 1 else "")) | |
| 129 | _ok("Q2 ... and the cut is still on (the hole)", _count(t2, Vector3(312, 1.5, 2)) == 0) | |
| 130 | ||
| 131 | # ---- Q3 / Q4 apply up to here ---- | |
| 132 | var t3 := _box(Vector3(320, 0, 0), Vector3(6, 2, 4)) | |
| 133 | var ca := _box(Vector3(320.5, -1, 1), Vector3(1, 4, 2)) | |
| 134 | var cb := _box(Vector3(324.5, -1, 1), Vector3(1, 4, 2)) | |
| 135 | var oa := _cut("subtract", ca, t3) | |
| 136 | var ob := _cut("subtract", cb, t3) | |
| 137 | var n3: int = StackOps.apply_up_to(map, t3, oa) | |
| 138 | var live: Dictionary = persist.live_shapes() | |
| 139 | var states: Dictionary = LevelPersistence.OpFold.resolve_states(persist.records(false)) | |
| 140 | _ok("Q3 Apply up to the first cut: the target is baked (no longer a live shape)", n3 > 0 and not live.has(t3), n3) | |
| 141 | _ok("Q3 ... the first cut is applied, the second is not", bool(states[oa]["applied"]) and not bool(states[ob]["applied"])) | |
| 142 | _ok("Q3 ... both holes are there (the second cut still cuts the baked pieces)", | |
| 143 | _count(t3, Vector3(321, 1, 2)) == 0 and _count(t3, Vector3(325, 1, 2)) == 0 | |
| 144 | and _count(t3, Vector3(323, 1, 2)) == 1, [_count(t3, Vector3(321, 1, 2)), _count(t3, Vector3(325, 1, 2)), _count(t3, Vector3(323, 1, 2))]) | |
| 145 | persist.undo_last() | |
| 146 | persist.refold() | |
| 147 | _ok("Q4 one Ctrl+Z: the shape is live again, both cuts live", (persist.live_shapes() as Dictionary).has(t3) | |
| 148 | and not bool(LevelPersistence.OpFold.resolve_states(persist.records(false))[oa]["applied"])) | |
| 149 | ||
| 150 | # ---- Q5 BoxCutter Y ---- | |
| 151 | map._ensure_ops() | |
| 152 | var op: Node = map.get_node("BoxCutter") | |
| 153 | var t5 := _box(Vector3(340, 0, 0), Vector3(4, 2, 4)) | |
| 154 | var nops := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size() | |
| 155 | op.set_tool_setting("depth", 0) | |
| 156 | op.place(Vector3(341, 2, 1), Vector3.UP, Vector3(341, 2, 1), Vector3(343, 2, 3), t5) | |
| 157 | var k := InputEventKey.new() | |
| 158 | k.keycode = KEY_Y | |
| 159 | k.physical_keycode = KEY_Y | |
| 160 | k.pressed = true | |
| 161 | op.handle_key(k) | |
| 162 | var nops2 := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size() | |
| 163 | _ok("Q5 Y extracts the drawn shape ∩ the target, and cuts nothing", nops2 == nops and _count(t5, Vector3(342, 1, 2)) == 1 | |
| 164 | and (_col_named("Extract").size() >= 2), [nops2 - nops, _count(t5, Vector3(342, 1, 2)), _col_named("Extract").size()]) |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | [gd_scene load_steps=2 format=3] | |
| 2 | ||
| 3 | [ext_resource type="Script" path="res://scenes/test/gate_q_stack.gd" id="1"] | |
| 4 | ||
| 5 | [node name="QStackGate" type="Node"] | |
| 6 | script = ExtResource("1") |