| … | … | @@ -1126,6 +1126,10 @@ steps: |
| 1126 | 1126 | # Cables from edges (#120 L3 §14.5): chains (stop at a 3-way vertex, close a loop), |
| 1127 | 1127 | # vertices / centres, offset along the mean normal, corners; Alt+C, one gesture. |
| 1128 | 1128 | 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_edges.tscn" |
| 1129 | - name: gate-cable-bundle | |
| 1130 | # A bundle between two face sets (#120 L3 §14.6): islands, seeded ends on the faces, | |
| 1131 | # free end handles, slack; N cables + a Bundle collection in one gesture; Shift+Ctrl+C. | |
| 1132 | 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_bundle.tscn" | |
| 1129 | 1133 | - name: gate-curve-overlay |
| 1130 | 1134 | # Curves (wire, points, held handles) are an editor overlay: hidden in play mode, the |
| 1131 | 1135 | # Curves switch (Shift+Alt+K) and the master switch hide them in build (2026-10-18). |
| … | … | @@ -399,6 +399,9 @@ const DEFAULTS: Dictionary = { |
| 399 | 399 | "builder.drop_cable": {"key": KEY_D, "ctrl": true, "shift": true, "label": "Drop the selected cables to the ground (ends stay)"}, |
| 400 | 400 | # #120 L3 §14.5. Alt+C: Blender 2.79's Convert (to Curve from Mesh), the nearest act. |
| 401 | 401 | "builder.cable_from_edges": {"key": KEY_C, "alt": true, "label": "Cable from edges (edge mode: a cable per chain of selected edges)"}, |
| 402 | # #120 L3 §14.6. Shift+Ctrl+C, beside Alt+C (Blender 2.79's is Add Constraint with | |
| 403 | # targets; there are no constraints here). | |
| 404 | "builder.cable_bundle": {"key": KEY_C, "ctrl": true, "shift": true, "label": "Cable bundle between two face sets (face mode: two islands)"}, | |
| 402 | 405 | "builder.assign_generator": {"label": "Generator… (stairs, tunnel … on this shape; it stays the passage)"}, |
| 403 | 406 | "builder.add_modifier": {"key": KEY_M, "ctrl": true, "shift": true, "label": "Add modifier… (mirror, array, grid, radial, curve, chamfer, dice on this shape)"}, |
| 404 | 407 | # Menu-only, as in Blender (the outliner's / X menu's Delete Hierarchy): X keeps children. |
| … | … | @@ -1012,6 +1012,13 @@ Cablerator's Create a Cable Mass: |
| 1012 | 1012 | - The map's `add_cable` splits into `_record_cable(points, closed, params)` (records only) |
| 1013 | 1013 | and the gesture around it, so from-edges and the bundle write many cables in one |
| 1014 | 1014 | gesture. |
| 1015 | - **Built 2026-10-19** (`gate_cable_bundle` 20/20; 3 mutants killed): `BundleCables` (pure: | |
| 1016 | islands by union-find over shared corners, ends, cables), `CableBundleOperator`, | |
| 1017 | `map.cable_bundle`, and `ModalControls.focus(id)` so Z / D / N / S jump to their value | |
| 1018 | ([ ] then steps it). Slack is a bound value (Tab), not [ ] directly: [ ] steps whatever | |
| 1019 | is focused, in every tool. `builder.cable_bundle` = **Shift+Ctrl+C**, in Add ▸ Other and | |
| 1020 | the face Q menu when 2+ faces are in the set. It reads `_selected_face_polys()`, which | |
| 1021 | still has §14.4's unchecked tombstone lookup. | |
| 1015 | 1022 | |
| 1016 | 1023 | ### 14.7 Menus and keys |
| 1017 | 1024 | |
| … | … | @@ -1022,6 +1029,8 @@ Every command in a menu and on a key (standing rule): |
| 1022 | 1029 | and recorded here: |
| 1023 | 1030 | - Drop to ground: **Ctrl+Shift+D** (beside Shift+D / Alt+D; free in Blender's 3D view). |
| 1024 | 1031 | - Cable from edges: **Alt+C** (Blender 2.79's Convert, to Curve from Mesh). |
| 1032 | - Cable bundle: **Shift+Ctrl+C** (beside Alt+C; Blender 2.79's Add Constraint with | |
| 1033 | targets — there are no constraints here). | |
| 1025 | 1034 | |
| 1026 | 1035 | ### 14.8 Order and gates (each red first) |
| 1027 | 1036 |
| … | … | @@ -26,7 +26,7 @@ const MENUS: Array = [ |
| 26 | 26 | "build_mode.sel_brush", "builder.element_toggle", "builder.select_mode_menu"]], |
| 27 | 27 | ["Add", ["builder.add_menu", "-", "builder.add_box", "builder.add_stairs", "builder.add_spiral_stairs", |
| 28 | 28 | "builder.add_tunnel", "builder.add_cylinder", "builder.add_cone", "builder.add_sphere", "-", |
| 29 | "builder.create_box", "builder.add_curve", "builder.add_cable", "builder.cable_from_edges", "builder.carve"]], | |
| 29 | "builder.create_box", "builder.add_curve", "builder.add_cable", "builder.cable_from_edges", "builder.cable_bundle", "builder.carve"]], | |
| 30 | 30 | # Blender 2.79's OBJECT menu (was "Brush"): transforms, CSG, pivot, and since #120 F1 |
| 31 | 31 | # parenting, join / separate and collections. One menu, not a new one: a seventh title |
| 32 | 32 | # pushed the top band past a 1280-px window (gate_editor_e1). |
| … | … | @@ -0,0 +1,137 @@ |
| 1 | extends RefCounted | |
| 2 | class_name BundleCables | |
| 3 | ## A BUNDLE BETWEEN TWO FACE SETS (#120 L3, docs/node-tree-and-stacks.md §14.6): | |
| 4 | ## Cablerator's Create a Cable Mass, on the editor's face set. Pure and static: the faces | |
| 5 | ## come in as map._selected_face_polys() gives them ({pts, n}), the cables' curve points go | |
| 6 | ## out, and the map writes them. | |
| 7 | ## | |
| 8 | ## ISLANDS: the faces' connected components by shared corners (within ON). Exactly two, or | |
| 9 | ## nothing is made and `islands` says how many there were (Cablerator refuses the same way). | |
| 10 | ## Island A = the one holding the first face. | |
| 11 | ## | |
| 12 | ## ENDS, from one Pcg32 stream (`seed`), cable i from A_i to B_i: | |
| 13 | ## random a face picked by area, a triangle of its fan by area, a uniform point in it; | |
| 14 | ## centres the island's face centres, cycled (cable i uses centre i mod faces). | |
| 15 | ## Each end's normal is its face's. Both handles are FREE: an end leaves along its normal, | |
| 16 | ## tension x chord / 3 long, its tension x (1 + jitter x u), u in [-1, 1] from the same | |
| 17 | ## stream (drawn whatever the jitter, so changing it never moves the ends). `slack` hangs | |
| 18 | ## each cable as the curve tool does (CurveOperator.hung: interior points). | |
| 19 | ||
| 20 | const Pcg32Script := preload("res://scenes/maps/shapes/pcg32.gd") | |
| 21 | const CurveOperatorScript := preload("res://scenes/maps/operators/curve_operator.gd") | |
| 22 | ## Two corners closer than this (metres) are one. | |
| 23 | const ON := 1.0e-3 | |
| 24 | ||
| 25 | ||
| 26 | ## The faces' islands: [[face index, …], …], each ascending, ordered by their first face. | |
| 27 | static func islands(faces: Array) -> Array: | |
| 28 | var parent: Array = [] | |
| 29 | for i in faces.size(): | |
| 30 | parent.append(i) | |
| 31 | for i in faces.size(): | |
| 32 | for j in range(i + 1, faces.size()): | |
| 33 | if _touch(faces[i], faces[j]): | |
| 34 | var ri := _root(parent, i) | |
| 35 | var rj := _root(parent, j) | |
| 36 | if ri != rj: | |
| 37 | parent[maxi(ri, rj)] = mini(ri, rj) | |
| 38 | var by_root := {} | |
| 39 | var order: Array = [] | |
| 40 | for i in faces.size(): | |
| 41 | var r := _root(parent, i) | |
| 42 | if not by_root.has(r): | |
| 43 | by_root[r] = [] | |
| 44 | order.append(r) | |
| 45 | (by_root[r] as Array).append(i) | |
| 46 | var out: Array = [] | |
| 47 | for r: int in order: | |
| 48 | out.append(by_root[r]) | |
| 49 | return out | |
| 50 | ||
| 51 | ||
| 52 | static func _root(parent: Array, i: int) -> int: | |
| 53 | while int(parent[i]) != i: | |
| 54 | i = int(parent[i]) | |
| 55 | return i | |
| 56 | ||
| 57 | ||
| 58 | static func _touch(a: Dictionary, b: Dictionary) -> bool: | |
| 59 | for p: Vector3 in (a["pts"] as PackedVector3Array): | |
| 60 | for q: Vector3 in (b["pts"] as PackedVector3Array): | |
| 61 | if p.distance_to(q) < ON: | |
| 62 | return true | |
| 63 | return false | |
| 64 | ||
| 65 | ||
| 66 | ## The bundle: {islands: how many, cables: [[CurveMath points], …]} — no cables unless | |
| 67 | ## there are exactly two islands. `o` = {count, random, seed, tension, jitter, slack}. | |
| 68 | static func bundle(faces: Array, o: Dictionary) -> Dictionary: | |
| 69 | var isl := islands(faces) | |
| 70 | if isl.size() != 2: | |
| 71 | return {"islands": isl.size(), "cables": []} | |
| 72 | var A: Array = (isl[0] as Array).map(func(i: int) -> Dictionary: return faces[i]) | |
| 73 | var B: Array = (isl[1] as Array).map(func(i: int) -> Dictionary: return faces[i]) | |
| 74 | var ta := _tris(A) | |
| 75 | var tb := _tris(B) | |
| 76 | var rng: Object = Pcg32Script.seeded(maxi(0, int(o.get("seed", 0))), 1) | |
| 77 | var random := bool(o.get("random", true)) | |
| 78 | var tension := float(o.get("tension", 1.0)) | |
| 79 | var jitter := float(o.get("jitter", 0.0)) | |
| 80 | var slack := float(o.get("slack", 0.0)) | |
| 81 | var cables: Array = [] | |
| 82 | for i in maxi(0, int(o.get("count", 1))): | |
| 83 | var ea := _end(A, ta, i, random, rng) | |
| 84 | var eb := _end(B, tb, i, random, rng) | |
| 85 | var u1: float = rng.range_f(-1.0, 1.0) | |
| 86 | var u2: float = rng.range_f(-1.0, 1.0) | |
| 87 | var chord := (ea["p"] as Vector3).distance_to(eb["p"]) | |
| 88 | var pa := CurveMath.point(ea["p"], ea["n"]) | |
| 89 | pa["handle"] = "free" | |
| 90 | pa["h_out"] = (ea["n"] as Vector3) * tension * (1.0 + jitter * u1) * chord / 3.0 | |
| 91 | pa["h_in"] = -(pa["h_out"] as Vector3) | |
| 92 | var pb := CurveMath.point(eb["p"], eb["n"]) | |
| 93 | pb["handle"] = "free" | |
| 94 | pb["h_in"] = (eb["n"] as Vector3) * tension * (1.0 + jitter * u2) * chord / 3.0 | |
| 95 | pb["h_out"] = -(pb["h_in"] as Vector3) | |
| 96 | var pts: Array = [pa, pb] | |
| 97 | if slack > 1.0e-6: | |
| 98 | pts = CurveOperatorScript.hung(pts, false, slack) | |
| 99 | cables.append(CurveMath.auto_handles(pts, false)) | |
| 100 | return {"islands": 2, "cables": cables} | |
| 101 | ||
| 102 | ||
| 103 | ## An island's fan triangles: {tris: [[a, b, c, n, area]], total}. | |
| 104 | static func _tris(island: Array) -> Dictionary: | |
| 105 | var tris: Array = [] | |
| 106 | var total := 0.0 | |
| 107 | for f: Dictionary in island: | |
| 108 | var pts: PackedVector3Array = f["pts"] | |
| 109 | for k in range(1, pts.size() - 1): | |
| 110 | var area := (pts[k] - pts[0]).cross(pts[k + 1] - pts[0]).length() * 0.5 | |
| 111 | if area > 1.0e-12: | |
| 112 | tris.append([pts[0], pts[k], pts[k + 1], f["n"], area]) | |
| 113 | total += area | |
| 114 | return {"tris": tris, "total": total} | |
| 115 | ||
| 116 | ||
| 117 | ## End i on an island: {p, n}. | |
| 118 | static func _end(island: Array, t: Dictionary, i: int, random: bool, rng: Object) -> Dictionary: | |
| 119 | if not random or float(t["total"]) <= 0.0: | |
| 120 | var f: Dictionary = island[i % island.size()] | |
| 121 | var c := Vector3.ZERO | |
| 122 | for p: Vector3 in (f["pts"] as PackedVector3Array): | |
| 123 | c += p | |
| 124 | return {"p": c / float((f["pts"] as PackedVector3Array).size()), "n": f["n"]} | |
| 125 | var pick: float = rng.next_float() * float(t["total"]) | |
| 126 | var tri: Array = t["tris"][(t["tris"] as Array).size() - 1] | |
| 127 | for tr: Array in (t["tris"] as Array): | |
| 128 | if pick < float(tr[4]): | |
| 129 | tri = tr | |
| 130 | break | |
| 131 | pick -= float(tr[4]) | |
| 132 | var s := sqrt(float(rng.next_float())) | |
| 133 | var r2: float = rng.next_float() | |
| 134 | var a: Vector3 = tri[0] | |
| 135 | var b: Vector3 = tri[1] | |
| 136 | var c2: Vector3 = tri[2] | |
| 137 | return {"p": a * (1.0 - s) + b * (s * (1.0 - r2)) + c2 * (s * r2), "n": tri[3]} |
| … | … | @@ -557,6 +557,7 @@ func context_sections() -> Array: |
| 557 | 557 | ["build_mode.faces_extrude"]]) |
| 558 | 558 | if _sel_faces.size() >= 2: |
| 559 | 559 | el.append(["build_mode.faces_hull"]) |
| 560 | el.append(["builder.cable_bundle"]) # #120 L3 §14.6 | |
| 560 | 561 | if _sel_kind == "edge": |
| 561 | 562 | el.append(["builder.cable_from_edges"]) # #120 L3 §14.5 |
| 562 | 563 | el.append(["builder.deselect"]) |
| … | … | @@ -1315,7 +1316,7 @@ func add_sections() -> Array: |
| 1315 | 1316 | for s: String in ADD_SHAPES: |
| 1316 | 1317 | rows.append(["builder.add_%s" % s]) |
| 1317 | 1318 | return [["Add shape", rows], ["Other", [["builder.add_curve"], ["builder.add_cable"], ["builder.cable_from_edges"], |
| 1318 | ["builder.carve"]]]] | |
| 1319 | ["builder.cable_bundle"], ["builder.carve"]]]] | |
| 1319 | 1320 | |
| 1320 | 1321 | |
| 1321 | 1322 | ## Start the create tool already set to `shape_id`. |
| … | … | @@ -3045,6 +3046,7 @@ func _ensure_ops() -> OperatorStack: |
| 3045 | 3046 | _curve_edit = _make_op(CurveEditOperator.new(), "CurveEdit") |
| 3046 | 3047 | _drop_op = _make_op(DropCableOperatorScript.new(), "DropCable") # #120 L3 §14.3 |
| 3047 | 3048 | _edges_op = _make_op(CableEdgesOperatorScript.new(), "CableEdges") # #120 L3 §14.5 |
| 3049 | _bundle_op = _make_op(CableBundleOperatorScript.new(), "CableBundle") # #120 L3 §14.6 | |
| 3048 | 3050 | _grab_op = _make_op(ElementOperator.new(), "Grab") |
| 3049 | 3051 | _extrude = _make_op(ExtrudeOperator.new(), "Extrude") |
| 3050 | 3052 | _box = _make_op(CreateBoxOperator.new(), "CreateBox") |
| … | … | @@ -4371,6 +4373,10 @@ func _builder_key(event: InputEvent) -> void: |
| 4371 | 4373 | if _ensure_ops().activate(_curve_edit): |
| 4372 | 4374 | print("[MapDCLevel] curve edit: the point follows your aim, Tab next, X delete, I insert, Enter applies") |
| 4373 | 4375 | return |
| 4376 | if Keymap.tapped(event, "builder.cable_bundle"): | |
| 4377 | if _ensure_ops().activate(_bundle_op): | |
| 4378 | print("[MapDCLevel] cable bundle: Z count, D tension, N jitter, S radius, R ends, Q seed, Enter adds") | |
| 4379 | return | |
| 4374 | 4380 | if Keymap.tapped(event, "builder.cable_from_edges"): |
| 4375 | 4381 | if _ensure_ops().activate(_edges_op): |
| 4376 | 4382 | print("[MapDCLevel] cable from edges: P vertices / centres, C corners, [ ] offset, Enter adds") |
| … | … | @@ -5819,6 +5825,44 @@ func cables_from_edges(edges: Array, centres: bool, offset: float, sharp: bool, |
| 5819 | 5825 | return out |
| 5820 | 5826 | |
| 5821 | 5827 | |
| 5828 | ## A BUNDLE BETWEEN TWO FACE SETS (#120 L3 §14.6). Preloaded for the class-cache reason. | |
| 5829 | const CableBundleOperatorScript := preload("res://scenes/maps/operators/cable_bundle_operator.gd") | |
| 5830 | const BundleCablesScript := preload("res://scenes/maps/curves/bundle_cables.gd") | |
| 5831 | var _bundle_op: LevelOperator = null | |
| 5832 | ||
| 5833 | ||
| 5834 | ## The bundle on `faces` (_selected_face_polys()' form; exactly two islands) with options `o` | |
| 5835 | ## (BundleCables.bundle): N curves + cables and a "Bundle" collection holding them (under | |
| 5836 | ## Scene), ONE gesture; the cables become the selection. Their dots ([] when refused). | |
| 5837 | func cable_bundle(faces: Array, o: Dictionary, params: Dictionary = {}) -> Array: | |
| 5838 | _ensure_persistence() | |
| 5839 | var out: Array = [] | |
| 5840 | var cables: Array = BundleCablesScript.bundle(faces, o)["cables"] | |
| 5841 | if cables.is_empty(): | |
| 5842 | return out | |
| 5843 | _persist.begin_gesture("Cable bundle") | |
| 5844 | for pts: Array in cables: | |
| 5845 | var d := _record_cable(pts, false, params, false) | |
| 5846 | if d != "": | |
| 5847 | out.append(d) | |
| 5848 | if not out.is_empty(): | |
| 5849 | var col: String = _persist.record("col.create", {"name": "Bundle", "parent": ""}) | |
| 5850 | var members: Array = [] | |
| 5851 | for d2: String in out: | |
| 5852 | members.append("g:" + d2) | |
| 5853 | if col != "": | |
| 5854 | _persist.record("col.link", {"target": col, "members": members}) | |
| 5855 | _persist.end_gesture() | |
| 5856 | if out.is_empty(): | |
| 5857 | return out | |
| 5858 | _persist.refold() | |
| 5859 | var ids := PackedInt32Array() | |
| 5860 | for d3: String in out: | |
| 5861 | ids.append_array(level.get_group_brush_ids(d3) as PackedInt32Array) | |
| 5862 | select_brushes(ids) | |
| 5863 | return out | |
| 5864 | ||
| 5865 | ||
| 5822 | 5866 | ## Is `p` inside (or on, within FaceKit.ON_PLANE) any of the convex plane sets? |
| 5823 | 5867 | static func _inside_any(p: Vector3, sets: Array) -> bool: |
| 5824 | 5868 | for pl: PackedFloat32Array in sets: |
| … | … | @@ -0,0 +1,194 @@ |
| 1 | extends LevelOperator | |
| 2 | ## A BUNDLE BETWEEN TWO FACE SETS (#120 L3, docs/node-tree-and-stacks.md §14.6): | |
| 3 | ## Cablerator's Create a Cable Mass on the face set — exactly two islands. The preview draws | |
| 4 | ## the cables; Enter writes ONE gesture (N curves + cables and a "Bundle" collection holding | |
| 5 | ## them, map.cable_bundle). Adjust Last re-runs it with the same seed. | |
| 6 | ## Keys (Cablerator's, tool-local): Z count, D tension, N tension jitter, S radius — each | |
| 7 | ## focuses its value, then [ ] steps it (Tab cycles, as everywhere); R random points / face | |
| 8 | ## centres; Q a new seed. | |
| 9 | ||
| 10 | const BundleCablesScript := preload("res://scenes/maps/curves/bundle_cables.gd") | |
| 11 | ||
| 12 | var count := 8 | |
| 13 | var tension := 1.0 | |
| 14 | var jitter := 0.3 | |
| 15 | var radius := 0.03 | |
| 16 | var slack := 0.0 | |
| 17 | var random := true | |
| 18 | var rng_seed := 1 | |
| 19 | var _faces: Array = [] # _selected_face_polys() at activation (Adjust Last: stored) | |
| 20 | var _frozen := false | |
| 21 | ||
| 22 | ||
| 23 | func op_name() -> String: | |
| 24 | return "cable bundle" | |
| 25 | ||
| 26 | ||
| 27 | func gesture_label() -> String: | |
| 28 | return "Cable bundle" | |
| 29 | ||
| 30 | ||
| 31 | func preview_color() -> Color: | |
| 32 | return Color(1.0, 0.85, 0.3, 0.9) # the curve view's amber | |
| 33 | ||
| 34 | ||
| 35 | func setup_modal(m: ModalControls) -> void: | |
| 36 | m.confirm_label = "add the bundle" | |
| 37 | m.table([["R", "ends", func() -> String: return "random points" if random else "face centres"], | |
| 38 | ["Q", "new seed", func() -> String: return "%d" % rng_seed]]) | |
| 39 | m.bind("count", "cables (Z)", func() -> float: return float(count), | |
| 40 | func(v: float) -> void: | |
| 41 | count = clampi(roundi(v), 1, 64) | |
| 42 | update_preview(), | |
| 43 | 1.0, "") | |
| 44 | m.bind("tension", "tension (D)", func() -> float: return tension, | |
| 45 | func(v: float) -> void: | |
| 46 | tension = clampf(v, 0.0, 5.0) | |
| 47 | update_preview(), | |
| 48 | 0.1, "", 0.02) | |
| 49 | m.bind("jitter", "tension jitter (N)", func() -> float: return jitter, | |
| 50 | func(v: float) -> void: | |
| 51 | jitter = clampf(v, 0.0, 1.0) | |
| 52 | update_preview(), | |
| 53 | 0.05, "", 0.01) | |
| 54 | m.bind("radius", "radius (S)", func() -> float: return radius, | |
| 55 | func(v: float) -> void: | |
| 56 | radius = clampf(v, 0.005, 2.0) | |
| 57 | update_preview(), | |
| 58 | 0.01, " m", 0.002) | |
| 59 | m.bind("slack", "slack", func() -> float: return slack, | |
| 60 | func(v: float) -> void: | |
| 61 | slack = maxf(0.0, v) | |
| 62 | update_preview(), | |
| 63 | 0.1, " m", 0.02) | |
| 64 | ||
| 65 | ||
| 66 | func status() -> String: | |
| 67 | return modal().status_text() | |
| 68 | ||
| 69 | ||
| 70 | func _opts() -> Dictionary: | |
| 71 | return {"count": count, "random": random, "seed": rng_seed, "tension": tension, "jitter": jitter, "slack": slack} | |
| 72 | ||
| 73 | ||
| 74 | func on_activate() -> bool: | |
| 75 | if not _frozen: | |
| 76 | _faces = map._selected_face_polys() if map != null and map.has_method("_selected_face_polys") else [] | |
| 77 | var n: int = BundleCablesScript.islands(_faces).size() | |
| 78 | if n != 2: | |
| 79 | print("[bundle] select exactly 2 face islands (face mode, Shift-click), found %d" % n) | |
| 80 | return false | |
| 81 | update_preview() | |
| 82 | return true | |
| 83 | ||
| 84 | ||
| 85 | func on_deactivate() -> void: | |
| 86 | clear_preview() | |
| 87 | ||
| 88 | ||
| 89 | func handle_key(event: InputEvent) -> bool: | |
| 90 | if not (event is InputEventKey) or not (event as InputEventKey).pressed: | |
| 91 | return false | |
| 92 | match KeyRules.key_of(event as InputEventKey): | |
| 93 | KEY_Z: | |
| 94 | return modal().focus("count") | |
| 95 | KEY_D: | |
| 96 | return modal().focus("tension") | |
| 97 | KEY_N: | |
| 98 | return modal().focus("jitter") | |
| 99 | KEY_S: | |
| 100 | return modal().focus("radius") | |
| 101 | KEY_R: | |
| 102 | random = not random | |
| 103 | update_preview() | |
| 104 | return true | |
| 105 | KEY_Q: | |
| 106 | rng_seed += 1 | |
| 107 | update_preview() | |
| 108 | return true | |
| 109 | return modal().handle_key(event) | |
| 110 | ||
| 111 | ||
| 112 | func update_preview() -> void: | |
| 113 | clear_preview() | |
| 114 | var lines := PackedVector3Array() | |
| 115 | for pts: Array in (BundleCablesScript.bundle(_faces, _opts())["cables"] as Array): | |
| 116 | var fs := CurveMath.sample(pts, false, 48) | |
| 117 | for i in range(1, fs.size()): | |
| 118 | lines.append(fs[i - 1]["p"]) | |
| 119 | lines.append(fs[i]["p"]) | |
| 120 | draw_lines(lines) | |
| 121 | ||
| 122 | ||
| 123 | func confirm() -> bool: | |
| 124 | var dots: Array = map.cable_bundle(_faces, _opts(), {"radius": radius}) | |
| 125 | if dots.is_empty(): | |
| 126 | return false | |
| 127 | ModalControls.summary("Added a bundle of %d cable%s" % [dots.size(), "" if dots.size() == 1 else "s"]) | |
| 128 | return true | |
| 129 | ||
| 130 | ||
| 131 | func tool_settings() -> Array: | |
| 132 | return [ | |
| 133 | {"id": "count", "label": "Cables", "type": "int", "value": count, "min": 1, "max": 64, "step": 1}, | |
| 134 | {"id": "tension", "label": "Tension", "type": "float", "value": tension, "min": 0.0, "max": 5.0, "step": 0.1, | |
| 135 | "tip": "How far each end leaves its face along the normal, x chord / 3"}, | |
| 136 | {"id": "jitter", "label": "Tension jitter", "type": "float", "value": jitter, "min": 0.0, "max": 1.0, "step": 0.05}, | |
| 137 | {"id": "radius", "label": "Radius", "type": "float", "value": radius, "min": 0.005, "max": 2.0, "step": 0.01}, | |
| 138 | {"id": "slack", "label": "Slack", "type": "float", "value": slack, "min": 0.0, "max": 20.0, "step": 0.1, | |
| 139 | "tip": "Extra length: each cable hangs"}, | |
| 140 | {"id": "random", "label": "Ends", "type": "enum", "value": 0 if random else 1, | |
| 141 | "choices": ["Random points", "Face centres"], "tip": "R"}, | |
| 142 | {"id": "seed", "label": "Seed", "type": "int", "value": rng_seed, "min": 0, "max": 1000000, "step": 1, "tip": "Q: a new one"}, | |
| 143 | ] | |
| 144 | ||
| 145 | ||
| 146 | func set_tool_setting(sid: String, value: Variant) -> bool: | |
| 147 | match sid: | |
| 148 | "count": | |
| 149 | count = clampi(int(value), 1, 64) | |
| 150 | "tension": | |
| 151 | tension = clampf(float(value), 0.0, 5.0) | |
| 152 | "jitter": | |
| 153 | jitter = clampf(float(value), 0.0, 1.0) | |
| 154 | "radius": | |
| 155 | radius = clampf(float(value), 0.005, 2.0) | |
| 156 | "slack": | |
| 157 | slack = maxf(0.0, float(value)) | |
| 158 | "random": | |
| 159 | random = int(value) == 0 | |
| 160 | "seed": | |
| 161 | rng_seed = maxi(0, int(value)) | |
| 162 | _: | |
| 163 | return false | |
| 164 | if not _frozen: | |
| 165 | update_preview() | |
| 166 | return true | |
| 167 | ||
| 168 | ||
| 169 | func adjust_ids() -> Array: | |
| 170 | return ["count", "tension", "jitter", "radius", "slack", "random", "seed"] | |
| 171 | ||
| 172 | ||
| 173 | func adjust_snapshot() -> Dictionary: | |
| 174 | var s := _opts() | |
| 175 | s["faces"] = _faces.duplicate(true) | |
| 176 | s["radius"] = radius | |
| 177 | return s | |
| 178 | ||
| 179 | ||
| 180 | func adjust_restore(snap: Dictionary) -> void: | |
| 181 | _faces = (snap.get("faces", []) as Array).duplicate(true) | |
| 182 | count = int(snap.get("count", count)) | |
| 183 | random = bool(snap.get("random", random)) | |
| 184 | rng_seed = int(snap.get("seed", rng_seed)) | |
| 185 | tension = float(snap.get("tension", tension)) | |
| 186 | jitter = float(snap.get("jitter", jitter)) | |
| 187 | slack = float(snap.get("slack", slack)) | |
| 188 | radius = float(snap.get("radius", radius)) | |
| 189 | _frozen = true | |
| 190 | ||
| 191 | ||
| 192 | func adjust_end() -> void: | |
| 193 | _frozen = false | |
| 194 | clear_preview() |
| … | … | @@ -73,6 +73,16 @@ func focused() -> String: |
| 73 | 73 | return String(_values[_focus]["id"]) if not _values.is_empty() else "" |
| 74 | 74 | |
| 75 | 75 | |
| 76 | ## Focus the bound value `id` (a tool's own letter for it, as Cablerator's Z / D / N / S | |
| 77 | ## jump to a value: [ ] then step that one). False if no value has that id. | |
| 78 | func focus(id: String) -> bool: | |
| 79 | for i in _values.size(): | |
| 80 | if String(_values[i]["id"]) == id: | |
| 81 | _focus = i | |
| 82 | return true | |
| 83 | return false | |
| 84 | ||
| 85 | ||
| 76 | 86 | ## H (help), Tab (focus) and [ ] (the focused value). True if the key was one of these. |
| 77 | 87 | func handle_key(event: InputEvent) -> bool: |
| 78 | 88 | if not (event is InputEventKey) or not (event as InputEventKey).pressed: |
| … | … | @@ -0,0 +1,309 @@ |
| 1 | extends Node | |
| 2 | ## GATE — A BUNDLE BETWEEN TWO FACE SETS (#120 L3, docs/node-tree-and-stacks.md §14.6): | |
| 3 | ## Cablerator's Create a Cable Mass. | |
| 4 | ## pure (BundleCables, faces as _selected_face_polys() gives them: {pts, n}): | |
| 5 | ## B1 two touching faces + one apart: two islands, [2 faces] and [1 face] | |
| 6 | ## B2 one island, or three: refused, saying how many | |
| 7 | ## B3 the same seed, the same ends; another seed, other ends | |
| 8 | ## B4 random ends lie ON their island's faces (on the plane, inside the polygon) | |
| 9 | ## B5 jitter 0: each end's handle runs along its face normal, tension x chord / 3 long | |
| 10 | ## B6 random off: ends are the face centres, cycled | |
| 11 | ## B7 count 8: 8 cables, each from island A to island B | |
| 12 | ## B8 slack: each cable hangs (2 ends + 3 interior points) | |
| 13 | ## in the map: | |
| 14 | ## M1 two islands selected, the key, Enter: 5 cables + a "Bundle" collection holding | |
| 15 | ## them, ONE gesture; one Ctrl+Z takes it all | |
| 16 | ## M2 one island: the tool refuses | |
| 17 | ## M3 Adjust Last with count 3: 3 cables, still one Bundle collection | |
| 18 | ## M4 Z / D / N / S focus count / tension / jitter / radius; R and Q work | |
| 19 | ## M5 the Add menu and the face Q menu offer it; the key is bound | |
| 20 | ## M6 map.cable_bundle called directly (not through the operator stack): one Ctrl+Z | |
| 21 | ## | |
| 22 | ## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_cable_bundle.tscn | |
| 23 | ||
| 24 | const CollectionFold := preload("res://scenes/maps/collection_fold.gd") | |
| 25 | ||
| 26 | var fails := 0 | |
| 27 | var checks := 0 | |
| 28 | var BC: GDScript = null | |
| 29 | ||
| 30 | ||
| 31 | func _ok(what: String, cond: bool, note: Variant = "") -> void: | |
| 32 | checks += 1 | |
| 33 | if not cond: | |
| 34 | fails += 1 | |
| 35 | print(" %-66s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)]) | |
| 36 | ||
| 37 | ||
| 38 | func _ready() -> void: | |
| 39 | if ResourceLoader.exists("res://scenes/maps/curves/bundle_cables.gd"): | |
| 40 | BC = load("res://scenes/maps/curves/bundle_cables.gd") | |
| 41 | _ok("setup: BundleCables exists", BC != null) | |
| 42 | if BC != null: | |
| 43 | _pure() | |
| 44 | var gs: Node = get_tree().root.get_node_or_null("/root/GameState") | |
| 45 | gs.route("world:level:*", 1, "peer") | |
| 46 | var keet: Node = get_tree().root.get_node_or_null("/root/KeetService") | |
| 47 | if keet != null: | |
| 48 | keet.current_member_id = "gate-author" | |
| 49 | get_tree().root.get_node("/root/GameMode").set_mode("build") | |
| 50 | var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate() | |
| 51 | add_child(map) | |
| 52 | for i in 6: | |
| 53 | await get_tree().process_frame | |
| 54 | await _run(map) | |
| 55 | get_tree().root.get_node("/root/GameMode").set_mode("play") | |
| 56 | if checks < 20: | |
| 57 | print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks) | |
| 58 | fails += 1 | |
| 59 | print("\nGATE cable_bundle: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails]) | |
| 60 | get_tree().quit(0 if fails == 0 else 1) | |
| 61 | ||
| 62 | ||
| 63 | func _face(pts: Array, n: Vector3) -> Dictionary: | |
| 64 | return {"pts": PackedVector3Array(pts), "n": n} | |
| 65 | ||
| 66 | ||
| 67 | ## Box [0,2]x[0,1]x[0,2]: its top and its +X side (they share an edge); and a face of a | |
| 68 | ## box 10 m away. | |
| 69 | func _faces() -> Array: | |
| 70 | var top := _face([Vector3(0, 1, 0), Vector3(0, 1, 2), Vector3(2, 1, 2), Vector3(2, 1, 0)], Vector3.UP) | |
| 71 | var side := _face([Vector3(2, 0, 0), Vector3(2, 1, 0), Vector3(2, 1, 2), Vector3(2, 0, 2)], Vector3.RIGHT) | |
| 72 | var far := _face([Vector3(10, 0, 0), Vector3(10, 0, 2), Vector3(10, 1, 2), Vector3(10, 1, 0)], Vector3.LEFT) | |
| 73 | return [top, far, side] | |
| 74 | ||
| 75 | ||
| 76 | func _opts(over: Dictionary = {}) -> Dictionary: | |
| 77 | var o := {"count": 4, "random": true, "seed": 7, "tension": 1.0, "jitter": 0.0, "slack": 0.0} | |
| 78 | for k: String in over: | |
| 79 | o[k] = over[k] | |
| 80 | return o | |
| 81 | ||
| 82 | ||
| 83 | ## Is `p` on the plane of face `f` and inside its (convex) polygon? | |
| 84 | func _on_face(p: Vector3, f: Dictionary) -> bool: | |
| 85 | var pts: PackedVector3Array = f["pts"] | |
| 86 | var n: Vector3 = f["n"] | |
| 87 | if absf(n.dot(p - pts[0])) > 1.0e-4: | |
| 88 | return false | |
| 89 | var sgn := 0.0 | |
| 90 | for i in pts.size(): | |
| 91 | var s := n.dot((pts[(i + 1) % pts.size()] - pts[i]).cross(p - pts[i])) | |
| 92 | if absf(s) < 1.0e-9: | |
| 93 | continue | |
| 94 | if sgn == 0.0: | |
| 95 | sgn = signf(s) | |
| 96 | elif signf(s) != sgn: | |
| 97 | return false | |
| 98 | return true | |
| 99 | ||
| 100 | ||
| 101 | func _pure() -> void: | |
| 102 | var fs := _faces() | |
| 103 | # ---- B1 ---- | |
| 104 | var isl: Array = BC.islands(fs) | |
| 105 | _ok("B1 two islands: [top, side] and [far]", isl.size() == 2 and isl[0] == [0, 2] and isl[1] == [1], isl) | |
| 106 | # ---- B2 ---- | |
| 107 | var one: Dictionary = BC.bundle([fs[0], fs[2]], _opts()) | |
| 108 | var three: Dictionary = BC.bundle([fs[0], fs[1], _face([Vector3(20, 0, 0), Vector3(20, 0, 1), Vector3(20, 1, 1)], Vector3.LEFT)], _opts()) | |
| 109 | _ok("B2 one island / three islands: refused, with the count", int(one.get("islands", -1)) == 1 | |
| 110 | and int(three.get("islands", -1)) == 3 and (one.get("cables", []) as Array).is_empty() | |
| 111 | and (three.get("cables", []) as Array).is_empty(), [one.get("islands"), three.get("islands")]) | |
| 112 | # ---- B3 ---- | |
| 113 | var r1: Array = BC.bundle(fs, _opts())["cables"] | |
| 114 | var r2: Array = BC.bundle(fs, _opts())["cables"] | |
| 115 | var r3: Array = BC.bundle(fs, _opts({"seed": 8}))["cables"] | |
| 116 | var same := r1.size() == 4 and r2.size() == 4 | |
| 117 | for i in mini(r1.size(), r2.size()): | |
| 118 | same = same and (r1[i][0]["p"] as Vector3) == (r2[i][0]["p"] as Vector3) and (r1[i][1]["p"] as Vector3) == (r2[i][1]["p"] as Vector3) | |
| 119 | _ok("B3 the same seed: the same ends", same) | |
| 120 | _ok("B3 ... another seed: other ends", r3.size() == 4 and (r3[0][0]["p"] as Vector3) != (r1[0][0]["p"] as Vector3)) | |
| 121 | # ---- B4 + B7 ---- | |
| 122 | var r8: Array = BC.bundle(fs, _opts({"count": 8}))["cables"] | |
| 123 | var on := r8.size() == 8 | |
| 124 | for c: Array in r8: | |
| 125 | var a: Vector3 = c[0]["p"] | |
| 126 | var b: Vector3 = c[c.size() - 1]["p"] | |
| 127 | on = on and (_on_face(a, fs[0]) or _on_face(a, fs[2])) and _on_face(b, fs[1]) | |
| 128 | _ok("B4 + B7 count 8: 8 cables, each from a face of A to the face of B (on it)", on, r8.size()) | |
| 129 | # ---- B5 ---- | |
| 130 | var c0: Array = r1[0] if r1.size() > 0 else [] | |
| 131 | var b5 := false | |
| 132 | if c0.size() == 2: | |
| 133 | var pa: Dictionary = c0[0] | |
| 134 | var pb: Dictionary = c0[1] | |
| 135 | var chord := (pa["p"] as Vector3).distance_to(pb["p"]) | |
| 136 | var ho: Vector3 = pa["h_out"] | |
| 137 | var hi: Vector3 = pb["h_in"] | |
| 138 | b5 = ho.normalized().distance_to(pa["n"]) < 1.0e-4 and absf(ho.length() - chord / 3.0) < 1.0e-4 \ | |
| 139 | and hi.normalized().distance_to(pb["n"]) < 1.0e-4 and absf(hi.length() - chord / 3.0) < 1.0e-4 \ | |
| 140 | and CurveMath.handle_type(pa) == "free" and CurveMath.handle_type(pb) == "free" | |
| 141 | _ok("B5 jitter 0: each end leaves along its normal, chord / 3 long, free", b5, c0) | |
| 142 | # ---- B6 ---- | |
| 143 | var rc: Array = BC.bundle(fs, _opts({"random": false, "count": 3}))["cables"] | |
| 144 | var cen := [Vector3(1, 1, 1), Vector3(2, 0.5, 1)] # top, side: island A in face order | |
| 145 | var b6 := rc.size() == 3 | |
| 146 | for i in rc.size(): | |
| 147 | b6 = b6 and (rc[i][0]["p"] as Vector3).distance_to(cen[i % 2]) < 1.0e-4 \ | |
| 148 | and (rc[i][rc[i].size() - 1]["p"] as Vector3).distance_to(Vector3(10, 0.5, 1)) < 1.0e-4 | |
| 149 | _ok("B6 random off: A's face centres cycled, B's centre", b6, rc.map(func(c: Array) -> Vector3: return c[0]["p"])) | |
| 150 | # ---- B8 ---- | |
| 151 | var rs: Array = BC.bundle(fs, _opts({"slack": 0.5}))["cables"] | |
| 152 | _ok("B8 slack 0.5: each cable 2 ends + 3 interior points", rs.size() == 4 | |
| 153 | and rs.all(func(c: Array) -> bool: return c.size() == 5), rs.map(func(c: Array) -> int: return c.size())) | |
| 154 | ||
| 155 | ||
| 156 | func _box_planes(lo: Vector3, hi: Vector3) -> Array: | |
| 157 | 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] | |
| 158 | ||
| 159 | ||
| 160 | func _key(level: Node, brush: int, n: Vector3) -> String: | |
| 161 | var d: Dictionary = level.get_brush(brush) | |
| 162 | var pl: PackedFloat32Array = d["planes"] | |
| 163 | var keys: PackedStringArray = d["plane_keys"] | |
| 164 | for i in pl.size() / 4: | |
| 165 | if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).normalized().distance_to(n) < 1.0e-4: | |
| 166 | return keys[i] | |
| 167 | return "" | |
| 168 | ||
| 169 | ||
| 170 | ## The map's face set, replaced IN PLACE (an Array[Dictionary]: set() would not convert). | |
| 171 | func _set_faces(map: Node, list: Array) -> void: | |
| 172 | var a: Array = map.get("_sel_faces") | |
| 173 | a.clear() | |
| 174 | for f: Dictionary in list: | |
| 175 | a.append(f) | |
| 176 | ||
| 177 | ||
| 178 | func _press(map: Node, action: String) -> void: | |
| 179 | var ev := InputEventAction.new() | |
| 180 | ev.action = action | |
| 181 | ev.pressed = true | |
| 182 | map._unhandled_input(ev) | |
| 183 | ||
| 184 | ||
| 185 | func _key_ev(code: Key) -> InputEventKey: | |
| 186 | var k := InputEventKey.new() | |
| 187 | k.keycode = code | |
| 188 | k.physical_keycode = code | |
| 189 | k.pressed = true | |
| 190 | return k | |
| 191 | ||
| 192 | ||
| 193 | func _cables(persist: Node) -> Array: | |
| 194 | var out: Array = [] | |
| 195 | for d: String in (persist.live_shapes() as Dictionary): | |
| 196 | if String(persist.shape_state(d)["type"]) == "cable": | |
| 197 | out.append(d) | |
| 198 | return out | |
| 199 | ||
| 200 | ||
| 201 | func _bundles(persist: Node) -> Array: | |
| 202 | var out: Array = [] | |
| 203 | var t: Dictionary = persist.collection_tree() | |
| 204 | for c: String in (t.get("cols", {}) as Dictionary): | |
| 205 | if String(t["cols"][c].get("name", "")) == "Bundle": | |
| 206 | out.append(c) | |
| 207 | return out | |
| 208 | ||
| 209 | ||
| 210 | func _run(map: Node3D) -> void: | |
| 211 | map._ensure_persistence() | |
| 212 | var persist: Node = map.get("_persist") | |
| 213 | var level: Node = map.get("level") | |
| 214 | var ops: Node = map.call("_ensure_ops") | |
| 215 | var has := map.has_method("cable_bundle") and map.get("_bundle_op") != null | |
| 216 | _ok("setup: map.cable_bundle + the tool", has) | |
| 217 | if not has: | |
| 218 | return | |
| 219 | var a: int = map._create_brush_journalled({"planes": _box_planes(Vector3(200, 60, 0), Vector3(202, 61, 2)), | |
| 220 | "proj": [0, 0, 0, 0, 0, 0], "material_id": 7, "level_id": 0}) | |
| 221 | var b: int = map._create_brush_journalled({"planes": _box_planes(Vector3(210, 60, 0), Vector3(212, 61, 2)), | |
| 222 | "proj": [0, 0, 0, 0, 0, 0], "material_id": 7, "level_id": 0}) | |
| 223 | level.rebuild() | |
| 224 | var ka := String(level.get_brush_key(a)) | |
| 225 | var kb := String(level.get_brush_key(b)) | |
| 226 | var two := [{"b": ka, "p": _key(level, a, Vector3.UP)}, {"b": ka, "p": _key(level, a, Vector3.RIGHT)}, | |
| 227 | {"b": kb, "p": _key(level, b, Vector3.LEFT)}] | |
| 228 | map.set_select_mode("face") | |
| 229 | var op: Object = map.get("_bundle_op") | |
| 230 | ||
| 231 | # ---- M2 ---- | |
| 232 | _set_faces(map, [two[0], two[1]]) | |
| 233 | _press(map, "builder.cable_bundle") | |
| 234 | _ok("M2 one island: the tool refuses", ops.get("active") == null, ops.get("active")) | |
| 235 | ||
| 236 | # ---- M1 + M4 ---- | |
| 237 | _set_faces(map, two) | |
| 238 | var c0 := _cables(persist).size() | |
| 239 | var b0 := _bundles(persist).size() | |
| 240 | var n0: int = (persist.records(false) as Array).size() | |
| 241 | _press(map, "builder.cable_bundle") | |
| 242 | _ok("M1 two islands: the tool is up", ops.get("active") == op, ops.get("active")) | |
| 243 | if ops.get("active") != op: | |
| 244 | return | |
| 245 | op.set_tool_setting("count", 5) | |
| 246 | var focus_ok := true | |
| 247 | for pair: Array in [[KEY_Z, "count"], [KEY_D, "tension"], [KEY_N, "jitter"], [KEY_S, "radius"]]: | |
| 248 | op.handle_key(_key_ev(pair[0])) | |
| 249 | focus_ok = focus_ok and op.modal().focused() == String(pair[1]) | |
| 250 | var seed0 := int(op.get("rng_seed")) | |
| 251 | op.handle_key(_key_ev(KEY_Q)) | |
| 252 | var rnd0 := bool(op.get("random")) | |
| 253 | op.handle_key(_key_ev(KEY_R)) | |
| 254 | var r_ok := bool(op.get("random")) != rnd0 | |
| 255 | op.handle_key(_key_ev(KEY_R)) | |
| 256 | _ok("M4 Z / D / N / S focus count / tension / jitter / radius; Q reseeds; R toggles", | |
| 257 | focus_ok and int(op.get("rng_seed")) != seed0 and r_ok, [op.modal().focused(), seed0, op.get("rng_seed")]) | |
| 258 | ops.confirm() | |
| 259 | var new_c := _cables(persist).size() - c0 | |
| 260 | var bun := _bundles(persist) | |
| 261 | var members := 0 | |
| 262 | if bun.size() == b0 + 1: | |
| 263 | var t: Dictionary = persist.collection_tree() | |
| 264 | for d: String in _cables(persist): | |
| 265 | if CollectionFold.collections_of(t, "g:" + d).has(bun[-1]): | |
| 266 | members += 1 | |
| 267 | _ok("M1 5 cables + one Bundle collection holding all 5", new_c == 5 and bun.size() == b0 + 1 and members == 5, | |
| 268 | [new_c, bun.size() - b0, members]) | |
| 269 | _ok("M1 ... 12 records (5 x curve + shape, col.create, col.link)", | |
| 270 | (persist.records(false) as Array).size() == n0 + 12, (persist.records(false) as Array).size() - n0) | |
| 271 | ||
| 272 | # ---- M3 ---- | |
| 273 | var adj: bool = ops.adjust("count", 3) | |
| 274 | _ok("M3 Adjust Last count 3: 3 cables, one Bundle", adj and _cables(persist).size() - c0 == 3 | |
| 275 | and _bundles(persist).size() == b0 + 1, [adj, _cables(persist).size() - c0, _bundles(persist).size() - b0]) | |
| 276 | persist.undo_last() | |
| 277 | persist.refold() | |
| 278 | _ok("M1 one Ctrl+Z takes the whole bundle", _cables(persist).size() == c0 and _bundles(persist).size() == b0, | |
| 279 | [_cables(persist).size() - c0, _bundles(persist).size() - b0]) | |
| 280 | ||
| 281 | # ---- M6: the API direct (no operator stack wrapping a gesture round it) ---- | |
| 282 | _set_faces(map, two) | |
| 283 | var c6 := _cables(persist).size() | |
| 284 | var b6 := _bundles(persist).size() | |
| 285 | var direct: Array = map.cable_bundle(map._selected_face_polys(), _opts({"count": 3}), {"radius": 0.03}) | |
| 286 | var made := _cables(persist).size() - c6 | |
| 287 | persist.undo_last() | |
| 288 | persist.refold() | |
| 289 | _ok("M6 cable_bundle called directly: 3 cables + a Bundle, ONE Ctrl+Z takes it all", direct.size() == 3 | |
| 290 | and made == 3 and _cables(persist).size() == c6 and _bundles(persist).size() == b6, | |
| 291 | [direct.size(), made, _cables(persist).size() - c6, _bundles(persist).size() - b6]) | |
| 292 | ||
| 293 | # ---- M5 ---- | |
| 294 | var rows: Array = [] | |
| 295 | for sec: Array in map.add_sections(): | |
| 296 | for r: Array in sec[1]: | |
| 297 | rows.append(r[0]) | |
| 298 | _set_faces(map, two) | |
| 299 | map.set("_sel_kind", "face") | |
| 300 | map.set("_sel_brush", a) | |
| 301 | var qrows: Array = [] | |
| 302 | for sec2: Array in map.q_sections(): | |
| 303 | for r2: Array in sec2[1]: | |
| 304 | qrows.append(r2[0]) | |
| 305 | var spec: Dictionary = Keymap.DEFAULTS.get("builder", {}).get("builder.cable_bundle", {}) | |
| 306 | _ok("M5 the Add menu and the face Q menu offer Bundle", rows.has("builder.cable_bundle") | |
| 307 | and qrows.has("builder.cable_bundle"), [rows, qrows]) | |
| 308 | _ok("M5 Shift+Ctrl+C is bound", int(spec.get("key", 0)) == KEY_C and bool(spec.get("ctrl", false)) | |
| 309 | and bool(spec.get("shift", false)) and not bool(spec.get("alt", false)), spec) |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | [gd_scene load_steps=2 format=3] | |
| 2 | ||
| 3 | [ext_resource type="Script" path="res://scenes/test/gate_cable_bundle.gd" id="1"] | |
| 4 | ||
| 5 | [node name="CableBundleGate" type="Node"] | |
| 6 | script = ExtResource("1") |