| … | … | @@ -1118,6 +1118,10 @@ steps: |
| 1118 | 1118 | # Drop to the ground (#120 L3 §14.3): CurveDrop on a fake floor; real physics with the |
| 1119 | 1119 | # cable's own pieces skipped; Ctrl+Shift+D tool, one gesture, Adjust Last. |
| 1120 | 1120 | run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_drop.tscn" |
| 1121 | - name: gate-edge-set | |
| 1122 | # The edge set (#120 L3 §14.4): Shift-click edges across brushes, held as brush key + | |
| 1123 | # two plane keys, resolved from the planes, live brushes only, highlighted. | |
| 1124 | run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_edge_set.tscn" | |
| 1121 | 1125 | - name: gate-curve-overlay |
| 1122 | 1126 | # Curves (wire, points, held handles) are an editor overlay: hidden in play mode, the |
| 1123 | 1127 | # Curves switch (Shift+Alt+K) and the master switch hide them in build (2026-10-18). |
| … | … | @@ -955,6 +955,13 @@ Cablerator's Drop the Cable, adapted: |
| 955 | 955 | on BOTH planes (FaceKit.corners, ON_PLANE). An edge whose planes no longer meet in a |
| 956 | 956 | segment drops out of the set. |
| 957 | 957 | - The highlight draws every edge in the set. |
| 958 | - **Built 2026-10-19** (`gate_edge_set` 13/13): `_sel_edges`, `_edge_key` (the two planes | |
| 959 | through both ends of the picked edge), `selected_edges()` → `{b, p, a, e, normals}`, | |
| 960 | `edge_set_lines()`; Shift on a second edge of a selected brush keeps the brush, as for | |
| 961 | faces; Make Real carries the set over like the face set. A removed brush is a TOMBSTONE | |
| 962 | that `brush_by_key` / `get_brush` still return, so resolving checks `get_live_brush_ids` | |
| 963 | (E8 caught it). `_selected_face_polys` (the face set, Alt+J / Alt+E) has the same | |
| 964 | unchecked lookup — not changed here. | |
| 958 | 965 | |
| 959 | 966 | ### 14.5 Cables from edges (`builder.cable_from_edges`) |
| 960 | 967 |
| … | … | @@ -1636,6 +1636,12 @@ func _q_make_real_then() -> void: |
| 1636 | 1636 | for fk: Dictionary in _sel_faces: |
| 1637 | 1637 | var ob: int = level.brush_by_key(String(fk["b"])) |
| 1638 | 1638 | faces.append([String(fk["b"]), ob, level.face_by_key(ob, String(fk["p"])) if ob >= 0 else -1]) |
| 1639 | var edges: Array = [] # the edge set (§14.4): [key, old id, plane index, plane index] | |
| 1640 | for ek: Dictionary in _sel_edges: | |
| 1641 | var oe: int = level.brush_by_key(String(ek["b"])) | |
| 1642 | var pk: Array = ek["p"] | |
| 1643 | edges.append([String(ek["b"]), oe, level.face_by_key(oe, String(pk[0])) if oe >= 0 else -1, | |
| 1644 | level.face_by_key(oe, String(pk[1])) if oe >= 0 else -1]) | |
| 1639 | 1645 | if make_real(dot) <= 0: |
| 1640 | 1646 | return |
| 1641 | 1647 | var nb: int = level.brush_by_key(String(last_real.get(old, ""))) if last_real.has(old) else -1 |
| … | … | @@ -1661,6 +1667,15 @@ func _q_make_real_then() -> void: |
| 1661 | 1667 | var k2: PackedStringArray = level.get_brush(b2).get("plane_keys", PackedStringArray()) if b2 >= 0 else PackedStringArray() |
| 1662 | 1668 | if b2 >= 0 and int(f[2]) >= 0 and int(f[2]) < k2.size(): |
| 1663 | 1669 | _sel_faces.append({"b": key, "p": k2[int(f[2])]}) |
| 1670 | _sel_edges.clear() | |
| 1671 | for e: Array in edges: | |
| 1672 | var ekey := String(last_real[int(e[1])]) if last_real.has(int(e[1])) else String(e[0]) | |
| 1673 | var b3: int = level.brush_by_key(ekey) | |
| 1674 | var k3: PackedStringArray = level.get_brush(b3).get("plane_keys", PackedStringArray()) if b3 >= 0 else PackedStringArray() | |
| 1675 | if b3 >= 0 and int(e[2]) >= 0 and int(e[3]) >= 0 and int(e[2]) < k3.size() and int(e[3]) < k3.size(): | |
| 1676 | var pr: Array = [String(k3[int(e[2])]), String(k3[int(e[3])])] | |
| 1677 | pr.sort() | |
| 1678 | _sel_edges.append({"b": ekey, "p": pr}) | |
| 1664 | 1679 | _update_face_highlight() |
| 1665 | 1680 | _update_element_highlight() |
| 1666 | 1681 | if resume.is_valid(): |
| … | … | @@ -5049,6 +5064,96 @@ var _sel_keys: PackedStringArray = PackedStringArray() |
| 5049 | 5064 | ## _sel_brush/_sel_keys (nudge, extrude act on it); this set is what Alt+J / Alt+E |
| 5050 | 5065 | ## build from. Empty outside face mode. |
| 5051 | 5066 | var _sel_faces: Array[Dictionary] = [] |
| 5067 | ## THE EDGE SET (#120 L3 §14.4): edge mode, Shift-click adds an edge of ANY brush, as | |
| 5068 | ## _sel_faces does for faces. {b: brush key, p: [plane key, plane key], sorted} — the edge | |
| 5069 | ## IS where those two planes meet on that brush, so a fold between clicks cannot leave it | |
| 5070 | ## pointing elsewhere, and no picked point is kept. The ACTIVE edge is still | |
| 5071 | ## _sel_edge_a/_b. Cables from edges (§14.5) read it. Empty outside edge mode. | |
| 5072 | var _sel_edges: Array[Dictionary] = [] | |
| 5073 | ||
| 5074 | ||
| 5075 | ## The edge a..b of `brush` as a durable key: the two planes through both ends. {} unless | |
| 5076 | ## exactly two. | |
| 5077 | func _edge_key(brush: int, a: Vector3, b: Vector3) -> Dictionary: | |
| 5078 | var d: Dictionary = level.get_brush(brush) | |
| 5079 | var pl: PackedFloat32Array = d.get("planes", PackedFloat32Array()) | |
| 5080 | var keys: PackedStringArray = d.get("plane_keys", PackedStringArray()) | |
| 5081 | var found: Array = [] | |
| 5082 | for i in mini(pl.size() / 4, keys.size()): | |
| 5083 | var n := Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]) | |
| 5084 | var l := n.length() | |
| 5085 | if l < 1.0e-9: | |
| 5086 | continue | |
| 5087 | var dd := pl[i * 4 + 3] / l | |
| 5088 | n /= l | |
| 5089 | if absf(n.dot(a) - dd) < FaceKit.ON_PLANE and absf(n.dot(b) - dd) < FaceKit.ON_PLANE: | |
| 5090 | found.append(String(keys[i])) | |
| 5091 | if found.size() != 2: | |
| 5092 | return {} | |
| 5093 | found.sort() | |
| 5094 | return {"b": String(level.get_brush_key(brush)), "p": found} | |
| 5095 | ||
| 5096 | ||
| 5097 | ## One edge key resolved on the level now: {b, p, a, e (the two ends, in (x, y, z) order), | |
| 5098 | ## normals: [n1, n2] (the planes', in p's order)}, or {} when the brush is gone or its two | |
| 5099 | ## planes no longer meet in a segment of it. `live` = the live brush ids as keys: a removed | |
| 5100 | ## brush is a TOMBSTONE, which brush_by_key and get_brush still return. | |
| 5101 | func _resolve_edge(ek: Dictionary, live: Dictionary) -> Dictionary: | |
| 5102 | var b: int = level.brush_by_key(String(ek.get("b", ""))) | |
| 5103 | if b <= 0 or not live.has(b): | |
| 5104 | return {} | |
| 5105 | var d: Dictionary = level.get_brush(b) | |
| 5106 | var pl: PackedFloat32Array = d.get("planes", PackedFloat32Array()) | |
| 5107 | var keys: PackedStringArray = d.get("plane_keys", PackedStringArray()) | |
| 5108 | var p: Array = ek.get("p", []) | |
| 5109 | if p.size() != 2: | |
| 5110 | return {} | |
| 5111 | var ns: Array = [] | |
| 5112 | var ds: Array = [] | |
| 5113 | for k: String in p: | |
| 5114 | var i := keys.find(k) | |
| 5115 | if i < 0 or i * 4 + 3 >= pl.size(): | |
| 5116 | return {} | |
| 5117 | var n := Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]) | |
| 5118 | var l := n.length() | |
| 5119 | if l < 1.0e-9: | |
| 5120 | return {} | |
| 5121 | ns.append(n / l) | |
| 5122 | ds.append(pl[i * 4 + 3] / l) | |
| 5123 | var on: Array = [] | |
| 5124 | for v: Vector3 in FaceKit.corners(pl): | |
| 5125 | if absf((ns[0] as Vector3).dot(v) - float(ds[0])) < FaceKit.ON_PLANE \ | |
| 5126 | and absf((ns[1] as Vector3).dot(v) - float(ds[1])) < FaceKit.ON_PLANE: | |
| 5127 | on.append(v) | |
| 5128 | if on.size() != 2: | |
| 5129 | return {} | |
| 5130 | if FaceKit._before(on[1], on[0]): | |
| 5131 | on.reverse() | |
| 5132 | return {"b": ek["b"], "p": p, "a": on[0], "e": on[1], "normals": ns} | |
| 5133 | ||
| 5134 | ||
| 5135 | ## The edge set, resolved (see _resolve_edge); edges that no longer resolve are left out. | |
| 5136 | func selected_edges() -> Array: | |
| 5137 | var out: Array = [] | |
| 5138 | if _sel_edges.is_empty(): | |
| 5139 | return out | |
| 5140 | var live := {} | |
| 5141 | for id: int in (level.get_live_brush_ids() as PackedInt32Array): | |
| 5142 | live[id] = true | |
| 5143 | for ek: Dictionary in _sel_edges: | |
| 5144 | var r := _resolve_edge(ek, live) | |
| 5145 | if not r.is_empty(): | |
| 5146 | out.append(r) | |
| 5147 | return out | |
| 5148 | ||
| 5149 | ||
| 5150 | ## The edge set as line pairs, for the highlight. | |
| 5151 | func edge_set_lines() -> PackedVector3Array: | |
| 5152 | var out := PackedVector3Array() | |
| 5153 | for r: Dictionary in selected_edges(): | |
| 5154 | out.append(r["a"]) | |
| 5155 | out.append(r["e"]) | |
| 5156 | return out | |
| 5052 | 5157 | ## The `face_sel` keymap context follows "is anything selected" (_process). |
| 5053 | 5158 | var _face_sel_on := false |
| 5054 | 5159 | ## Home / End pressed again on the SAME selection step to the next fit / edge. |
| … | … | @@ -5909,6 +6014,7 @@ func select_brushes(ids: PackedInt32Array) -> void: |
| 5909 | 6014 | # highlight is cleared rather than left pointing at whatever was picked last. |
| 5910 | 6015 | _sel_keys = PackedStringArray() |
| 5911 | 6016 | _sel_faces.clear() |
| 6017 | _sel_edges.clear() | |
| 5912 | 6018 | # "brush", NOT "". MoveOperator gates its selection branch on |
| 5913 | 6019 | # `_sel_kind == "brush"` (move_operator.gd on_activate), so an empty kind made |
| 5914 | 6020 | # G ignore the outliner entirely and fall through to the crosshair — which |
| … | … | @@ -6214,6 +6320,7 @@ func _clear_face_selection() -> void: |
| 6214 | 6320 | _sel_brush = -1 |
| 6215 | 6321 | _sel_keys = PackedStringArray() |
| 6216 | 6322 | _sel_faces.clear() |
| 6323 | _sel_edges.clear() | |
| 6217 | 6324 | _sel_kind = "" |
| 6218 | 6325 | _update_face_highlight() |
| 6219 | 6326 | _update_element_highlight() |
| … | … | @@ -6332,8 +6439,11 @@ func _update_element_highlight() -> void: |
| 6332 | 6439 | im.surface_add_vertex(_sel_point) |
| 6333 | 6440 | im.surface_end() |
| 6334 | 6441 | "edge": |
| 6335 | ol.set_layer("element", PackedVector3Array([_sel_edge_a, _sel_edge_b]), | |
| 6336 | Color(0.2, 1.0, 0.6), 3.0, 0.4, 1.5) | |
| 6442 | # Every edge in the set (§14.4); the active one alone when the set is empty. | |
| 6443 | var segs := edge_set_lines() | |
| 6444 | if segs.is_empty(): | |
| 6445 | segs = PackedVector3Array([_sel_edge_a, _sel_edge_b]) | |
| 6446 | ol.set_layer("element", segs, Color(0.2, 1.0, 0.6), 3.0, 0.4, 1.5) | |
| 6337 | 6447 | "brush": |
| 6338 | 6448 | # OUTLINES ARE THE WHOLE GRAMMAR (docs/editor-texturing-selection-move.md §3): |
| 6339 | 6449 | # every selected brush outlined orange; the ACTIVE one — the operand, the |
| … | … | @@ -6560,6 +6670,7 @@ func _pick_element(add: bool = false, at: Variant = null) -> void: |
| 6560 | 6670 | if _sel_mode == "brush": |
| 6561 | 6671 | _note_selected(brush, add) |
| 6562 | 6672 | _sel_faces.clear() |
| 6673 | _sel_edges.clear() | |
| 6563 | 6674 | _sel_keys = level.get_brush(brush).get("plane_keys", PackedStringArray()) |
| 6564 | 6675 | _sel_point = hit["position"] |
| 6565 | 6676 | _sel_kind = "brush" |
| … | … | @@ -6632,6 +6743,20 @@ func _pick_element(add: bool = false, at: Variant = null) -> void: |
| 6632 | 6743 | # The cross-brush FACE SET: Shift toggles this face in it, a plain click restarts |
| 6633 | 6744 | # it. Captured before _note_selected, whose `add` toggle may drop the brush from |
| 6634 | 6745 | # _sel_brushes when a second face of an already-selected brush is clicked. |
| 6746 | # The cross-brush EDGE SET (§14.4), the same way: Shift toggles, a plain click restarts. | |
| 6747 | if kind == "edge": | |
| 6748 | var ek := _edge_key(brush, _sel_edge_a, _sel_edge_b) | |
| 6749 | var ei := _sel_edges.find(ek) | |
| 6750 | if ek.is_empty(): | |
| 6751 | pass | |
| 6752 | elif not add: | |
| 6753 | _sel_edges = [ek] | |
| 6754 | elif ei >= 0: | |
| 6755 | _sel_edges.remove_at(ei) | |
| 6756 | else: | |
| 6757 | _sel_edges.append(ek) | |
| 6758 | else: | |
| 6759 | _sel_edges.clear() | |
| 6635 | 6760 | if kind == "face" and face < keys.size(): |
| 6636 | 6761 | var fk := {"b": String(level.get_brush_key(brush)), "p": keys[face]} |
| 6637 | 6762 | var fi := _sel_faces.find(fk) |
| … | … | @@ -6643,8 +6768,8 @@ func _pick_element(add: bool = false, at: Variant = null) -> void: |
| 6643 | 6768 | _sel_faces.append(fk) |
| 6644 | 6769 | else: |
| 6645 | 6770 | _sel_faces.clear() |
| 6646 | if kind == "face" and add and _sel_brushes.has(brush): | |
| 6647 | _sel_brush = brush # another face of a selected brush: keep it, make it active | |
| 6771 | if (kind == "face" or kind == "edge") and add and _sel_brushes.has(brush): | |
| 6772 | _sel_brush = brush # another face / edge of a selected brush: keep it, make it active | |
| 6648 | 6773 | else: |
| 6649 | 6774 | _note_selected(brush, add) |
| 6650 | 6775 | _sel_point = target |
| … | … | @@ -7609,6 +7734,7 @@ func _after_new_brushes(ids: PackedInt32Array) -> void: |
| 7609 | 7734 | level.rebuild() |
| 7610 | 7735 | _refresh_brush_view() |
| 7611 | 7736 | _sel_faces.clear() |
| 7737 | _sel_edges.clear() | |
| 7612 | 7738 | _sel_mode = "brush" |
| 7613 | 7739 | select_brushes(ids) |
| 7614 | 7740 | _update_face_highlight() |
| … | … | @@ -0,0 +1,170 @@ |
| 1 | extends Node | |
| 2 | ## GATE — THE EDGE SET (#120 L3, docs/node-tree-and-stacks.md §14.4): edge mode, Shift-click | |
| 3 | ## builds a cross-brush SET of edges, as face mode does for faces. An edge is held by | |
| 4 | ## durable names — the brush key and its two plane keys — never by a picked point. | |
| 5 | ## E1 a plain click on an edge: a set of one, {b, p: the two plane keys, sorted} | |
| 6 | ## E2 Shift-click another edge of the SAME brush: two, and the brush stays selected | |
| 7 | ## E3 Shift-click an edge of another brush: three | |
| 8 | ## E4 Shift-click the first edge again: removed (two) | |
| 9 | ## E5 selected_edges() resolves each from its planes: the right endpoints and normals | |
| 10 | ## E6 the set survives a re-fold | |
| 11 | ## E7 the highlight draws every edge in the set | |
| 12 | ## E8 a brush gone: its edge drops out of selected_edges() | |
| 13 | ## E9 a plain click restarts the set | |
| 14 | ## E10 face mode clears it | |
| 15 | ## | |
| 16 | ## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_edge_set.tscn | |
| 17 | ||
| 18 | var fails := 0 | |
| 19 | var checks := 0 | |
| 20 | var _cam: Camera3D = null | |
| 21 | ||
| 22 | ||
| 23 | func _ok(what: String, cond: bool, note: Variant = "") -> void: | |
| 24 | checks += 1 | |
| 25 | if not cond: | |
| 26 | fails += 1 | |
| 27 | print(" %-66s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)]) | |
| 28 | ||
| 29 | ||
| 30 | func _ready() -> void: | |
| 31 | var gs: Node = get_tree().root.get_node_or_null("/root/GameState") | |
| 32 | gs.route("world:level:*", 1, "peer") | |
| 33 | var keet: Node = get_tree().root.get_node_or_null("/root/KeetService") | |
| 34 | if keet != null: | |
| 35 | keet.current_member_id = "gate-author" | |
| 36 | get_tree().root.get_node("/root/GameMode").set_mode("build") | |
| 37 | var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate() | |
| 38 | add_child(map) | |
| 39 | _cam = Camera3D.new() | |
| 40 | add_child(_cam) | |
| 41 | _cam.current = true | |
| 42 | for i in 6: | |
| 43 | await get_tree().process_frame | |
| 44 | await _run(map) | |
| 45 | get_tree().root.get_node("/root/GameMode").set_mode("play") | |
| 46 | if checks < 13: | |
| 47 | print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks) | |
| 48 | fails += 1 | |
| 49 | print("\nGATE edge_set: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails]) | |
| 50 | get_tree().quit(0 if fails == 0 else 1) | |
| 51 | ||
| 52 | ||
| 53 | func _box_planes(lo: Vector3, hi: Vector3) -> Array: | |
| 54 | return [-1, 0, 0, -lo.x, 1, 0, 0, hi.x, 0, -1, 0, -lo.y, 0, 1, 0, hi.y, 0, 0, -1, -lo.z, 0, 0, 1, hi.z] | |
| 55 | ||
| 56 | ||
| 57 | func _wall(map: Node, lo: Vector3, hi: Vector3) -> int: | |
| 58 | return map._create_brush_journalled({"planes": _box_planes(lo, hi), | |
| 59 | "proj": [0, 0, 0, 0, 0, 0], "material_id": 7, "level_id": 0}) | |
| 60 | ||
| 61 | ||
| 62 | ## Aim along -n at `at` (a point on a face, near the edge wanted) and click. | |
| 63 | func _pick(map: Node, at: Vector3, n: Vector3, add: bool) -> void: | |
| 64 | _cam.global_position = at + n * 3.0 | |
| 65 | _cam.look_at(at, Vector3.UP if absf(n.y) < 0.9 else Vector3.FORWARD) | |
| 66 | await get_tree().physics_frame | |
| 67 | map._pick_element(add, get_viewport().get_visible_rect().size * 0.5) | |
| 68 | ||
| 69 | ||
| 70 | ## The plane key of `brush` whose outward normal is `n`. | |
| 71 | func _key(level: Node, brush: int, n: Vector3) -> String: | |
| 72 | var d: Dictionary = level.get_brush(brush) | |
| 73 | var pl: PackedFloat32Array = d["planes"] | |
| 74 | var keys: PackedStringArray = d["plane_keys"] | |
| 75 | for i in pl.size() / 4: | |
| 76 | if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).normalized().distance_to(n) < 1.0e-4: | |
| 77 | return keys[i] | |
| 78 | return "" | |
| 79 | ||
| 80 | ||
| 81 | func _pair(level: Node, brush: int, n1: Vector3, n2: Vector3) -> Array: | |
| 82 | var p := [_key(level, brush, n1), _key(level, brush, n2)] | |
| 83 | p.sort() | |
| 84 | return p | |
| 85 | ||
| 86 | ||
| 87 | func _run(map: Node3D) -> void: | |
| 88 | map._ensure_persistence() | |
| 89 | var persist: Node = map.get("_persist") | |
| 90 | var level: Node = map.get("level") | |
| 91 | var y0 := 60.0 | |
| 92 | var wa := _wall(map, Vector3(200, y0, 0), Vector3(201, y0 + 2, 2)) | |
| 93 | var wb := _wall(map, Vector3(203, y0, 0), Vector3(204, y0 + 2, 2)) | |
| 94 | level.rebuild() | |
| 95 | for i in 3: | |
| 96 | await get_tree().physics_frame | |
| 97 | var ka := String(level.get_brush_key(wa)) | |
| 98 | var kb := String(level.get_brush_key(wb)) | |
| 99 | var has := map.get("_sel_edges") != null and map.has_method("selected_edges") | |
| 100 | _ok("setup: the map has _sel_edges and selected_edges()", has) | |
| 101 | if not has: | |
| 102 | return | |
| 103 | map.set_select_mode("edge") | |
| 104 | map.clear_selection() | |
| 105 | ||
| 106 | # ---- E1: the top edge of wa's +X face ---- | |
| 107 | await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, false) | |
| 108 | var se: Array = map.get("_sel_edges") | |
| 109 | var e_top := {"b": ka, "p": _pair(level, wa, Vector3.RIGHT, Vector3.UP)} | |
| 110 | _ok("E1 a plain click: one edge, wa's +X / +Y planes", se.size() == 1 and se[0] == e_top, se) | |
| 111 | ||
| 112 | # ---- E2: wa's +X face, its front (+Z) edge ---- | |
| 113 | await _pick(map, Vector3(201, y0 + 1, 1.9), Vector3.RIGHT, true) | |
| 114 | se = map.get("_sel_edges") | |
| 115 | var e_front := {"b": ka, "p": _pair(level, wa, Vector3.RIGHT, Vector3.BACK)} | |
| 116 | _ok("E2 Shift-click another edge of the same brush: two", se.size() == 2 and se.has(e_front), se) | |
| 117 | _ok("E2 ... and the brush stays selected", (map.get("_sel_brushes") as Array).has(wa), map.get("_sel_brushes")) | |
| 118 | ||
| 119 | # ---- E3: wb's -X face, its top edge ---- | |
| 120 | await _pick(map, Vector3(203, y0 + 1.9, 1), Vector3.LEFT, true) | |
| 121 | se = map.get("_sel_edges") | |
| 122 | var e_b := {"b": kb, "p": _pair(level, wb, Vector3.LEFT, Vector3.UP)} | |
| 123 | _ok("E3 Shift-click an edge of another brush: three", se.size() == 3 and se.has(e_b), se) | |
| 124 | ||
| 125 | # ---- E4 ---- | |
| 126 | await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, true) | |
| 127 | se = map.get("_sel_edges") | |
| 128 | _ok("E4 Shift-click the first edge again: removed", se.size() == 2 and not se.has(e_top), se) | |
| 129 | ||
| 130 | # ---- E5 ---- | |
| 131 | var res: Array = map.selected_edges() | |
| 132 | var front: Dictionary = {} | |
| 133 | for r: Dictionary in res: | |
| 134 | if String(r["b"]) == ka: | |
| 135 | front = r | |
| 136 | var ends: Array = [front.get("a", Vector3.INF), front.get("e", Vector3.INF)] if not front.is_empty() else [] | |
| 137 | var want := [Vector3(201, y0, 2), Vector3(201, y0 + 2, 2)] | |
| 138 | var ends_ok := ends.size() == 2 and ((ends[0] as Vector3).distance_to(want[0]) < 1.0e-3 and (ends[1] as Vector3).distance_to(want[1]) < 1.0e-3 | |
| 139 | or (ends[0] as Vector3).distance_to(want[1]) < 1.0e-3 and (ends[1] as Vector3).distance_to(want[0]) < 1.0e-3) | |
| 140 | _ok("E5 the front edge resolves to (201, y0, 2) .. (201, y0+2, 2)", res.size() == 2 and ends_ok, ends) | |
| 141 | var ns: Array = front.get("normals", []) | |
| 142 | _ok("E5 ... with its two face normals (+X, +Z)", ns.size() == 2 and (ns.has(Vector3.RIGHT) or (ns[0] as Vector3).distance_to(Vector3.RIGHT) < 1.0e-4 | |
| 143 | or (ns[1] as Vector3).distance_to(Vector3.RIGHT) < 1.0e-4), ns) | |
| 144 | ||
| 145 | # ---- E6 ---- | |
| 146 | persist.refold() | |
| 147 | _ok("E6 a re-fold keeps the set (both still resolve)", (map.get("_sel_edges") as Array).size() == 2 | |
| 148 | and (map.selected_edges() as Array).size() == 2, map.selected_edges().size()) | |
| 149 | ||
| 150 | # ---- E7 ---- | |
| 151 | var lines: PackedVector3Array = map.edge_set_lines() if map.has_method("edge_set_lines") else PackedVector3Array() | |
| 152 | _ok("E7 the highlight's lines: every edge in the set (2 x 2 points)", lines.size() == 4, lines.size()) | |
| 153 | ||
| 154 | # ---- E8 ---- | |
| 155 | persist.begin_gesture("Delete wb") | |
| 156 | persist.record("brush.remove", {"brush_key": kb}) | |
| 157 | persist.end_gesture() | |
| 158 | persist.refold() | |
| 159 | _ok("E8 wb gone: its edge drops out of selected_edges()", (map.selected_edges() as Array).size() == 1, | |
| 160 | (map.selected_edges() as Array).size()) | |
| 161 | persist.undo_last() | |
| 162 | persist.refold() | |
| 163 | ||
| 164 | # ---- E9 ---- | |
| 165 | await _pick(map, Vector3(201, y0 + 1.9, 1), Vector3.RIGHT, false) | |
| 166 | _ok("E9 a plain click restarts the set", (map.get("_sel_edges") as Array).size() == 1, map.get("_sel_edges")) | |
| 167 | ||
| 168 | # ---- E10 ---- | |
| 169 | map.set_select_mode("face") | |
| 170 | _ok("E10 face mode clears the edge set", (map.get("_sel_edges") as Array).is_empty(), map.get("_sel_edges")) |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | [gd_scene load_steps=2 format=3] | |
| 2 | ||
| 3 | [ext_resource type="Script" path="res://scenes/test/gate_edge_set.gd" id="1"] | |
| 4 | ||
| 5 | [node name="EdgeSetGate" type="Node"] | |
| 6 | script = ExtResource("1") |