Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified doc/source/_static/tutorials/groups.mp4
Binary file not shown.
24 changes: 21 additions & 3 deletions doc/source/tutorials/groups.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
Groups
######

A **group** (or comment box) is just a node with no pins and an explicit content size.
Drag nodes over it and they move *with* the group — the editor handles the corral for
free. It is the editor's tool for visually organizing a large graph.
A **group** (or comment box) is a node with no pins and an explicit content size — the
editor's tool for visually organizing a large graph. Membership is purely **spatial**:
drag a node *inside* the group's bounds and it travels with the group from then on; drag
it back *out* and the group leaves it behind. There is no "add to group" call.
Comment on lines +7 to +10

.. code-block:: das

Expand All @@ -26,6 +27,11 @@ Walkthrough

.. video:: groups.mp4

The recording is voiced and self-verifying: Texture starts inside the group, Tint outside.
It drags Tint *in* (a group move then carries both nodes), then drags Tint back *out* (a
group move then leaves it behind) — asserting at each step that the right nodes moved and,
crucially, that the outside node stayed put (a no-op aborts at teardown).

.. literalinclude:: ../../../examples/tutorial/groups.das
:language: das
:linenos:
Expand All @@ -38,6 +44,18 @@ pin-less node whose body is its label. It shares the **same id space** as ``node
keep the ids disjoint (the tutorial uses 1 for the group, 10 / 20 for the nodes). Draw
groups **first** so the regular nodes paint on top of them.

Membership is spatial
=====================

The editor decides what a group carries at the **moment you drag it**: it moves every node
whose bounds fall inside the group's content box (``FindNodesInRect`` over ``m_GroupBounds``
in the C++). There is no membership list and no join API — drop a node inside the bounds and
the next group move takes it along; drag it back outside and the next move leaves it.

To move the group itself, grab its **header** — the title strip *above* the content box.
Pressing the content box instead starts a rubber-band selection (it moves nothing), which
is why the recording aims its group-drag at the title.

Editor-owned geometry
=====================

Expand Down
10 changes: 6 additions & 4 deletions examples/tutorial/groups.das
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ let NODE_B = 20
def draw_editor() {
node_editor("graph", (editor = g_ed)) {
if (!g_seeded) {
imgui_node_editor::SetNodePosition(GROUP_ID, float2(60.0, 60.0))
imgui_node_editor::SetNodePosition(NODE_A, float2(110.0, 130.0))
imgui_node_editor::SetNodePosition(NODE_B, float2(110.0, 250.0))
imgui_node_editor::SetNodePosition(GROUP_ID, float2(60.0, 80.0))
imgui_node_editor::SetNodePosition(NODE_A, float2(130.0, 170.0)) // INSIDE the group
imgui_node_editor::SetNodePosition(NODE_B, float2(560.0, 200.0)) // OUTSIDE the group
g_seeded = true
}
// The group, drawn first so the two nodes sit on top of it. `size` is the
Expand All @@ -56,7 +56,9 @@ def draw_editor() {
let mn = imgui_node_editor::GetGroupMin()
fg |> add_text(float2(mn.x + 4.0, mn.y - 18.0), rgba(232u, 226u, 210u, 255u), "Inputs")
}
// Two ordinary nodes seeded inside the group. Drag the group and they follow.
// Two ordinary nodes: Texture starts INSIDE the group, Tint OUTSIDE. Membership is
// spatial - dragging the group carries whatever sits within its bounds at that moment
// (the editor's FindNodesInRect), so drag Tint in and it joins, drag it out and it leaves.
node(NODE_A) {
text("Texture")
pin(11, (kind = PinKind.Output, pivot_alignment = float2(1.0, 0.5))) {
Expand Down
137 changes: 98 additions & 39 deletions tests/integration/record_groups.das
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,116 @@ require imgui/imgui_editor_playwright public
require daslib/json public
require daslib/json_boost public

//! Driver: record groups.apng — a group/comment box with two nodes seeded on top of it,
//! then a drag of the GROUP that carries both nodes with it (the editor's group-move). The
//! recording narrates the box and demonstrates the corral behavior with a slow synthetic
//! drag on the group's title bar. Run with `daslang tests/integration/record_groups.das`,
//! then ffmpeg the .apng to .mp4.
//!
//! NB: set_user_control(false) hands IO to the synthetic timeline — without it the real OS
//! cursor races the synth and the drag is eaten. The headless regression is test_groups.das.
//! Driver: record groups.apng - group membership is spatial. Texture starts INSIDE the
//! group, Tint OUTSIDE. Drag Tint in and a group move carries it; drag Tint back out and
//! a group move leaves it behind. Voiced and self-verifying: each drag/move MUST move the
//! nodes it should (record_check_changed) and the out-node MUST stay put when the group
//! moves (record_check_unchanged), or the recording aborts at teardown. This tutorial has
//! no links, so there is no flow pulse. The headless regression is test_groups.das.

let CANVAS = "MAIN_WIN/graph"

def widget_center(var snap : JsonValue?; ident : string) : tuple<float; float> {
def bbox(var snap : JsonValue?; ident : string) : float4 {
let b = find_widget(snap, ident)?["bbox"]
return (((b?["x"] ?? 0.0f) + (b?["z"] ?? 0.0f)) * 0.5f,
((b?["y"] ?? 0.0f) + (b?["w"] ?? 0.0f)) * 0.5f)
return float4(b?["x"] ?? 0.0f, b?["y"] ?? 0.0f, b?["z"] ?? 0.0f, b?["w"] ?? 0.0f)
}

def center(var snap : JsonValue?; ident : string) : tuple<float; float> {
let b = bbox(snap, ident)
return ((b.x + b.z) * 0.5f, (b.y + b.w) * 0.5f)
}

def title_point(var snap : JsonValue?; ident : string) : tuple<float; float> {
// A node's title row - grabbing the center would hit the right-aligned pin and start a
// link drag; the title strip reliably grabs the node body for a move.
let b = bbox(snap, ident)
return ((b.x + b.z) * 0.5f, b.y + 10.0f)
}

def cbbox_json(var snap : JsonValue?; ident : string) : string {
let v = widget_payload_field(snap, ident, "canvas_bbox")
return v != null ? write_json(v) : ""
}

def do_drag(app : ImguiApp; from, to : tuple<float; float>) {
// Press on `from`, travel held to `to`, release - the proven committing timeline.
var events <- [
JV((t_ms = 0, kind = "move", x = from._0, y = from._1)),
JV((t_ms = 300, kind = "move", x = from._0, y = from._1)),
JV((t_ms = 380, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = from._0, y = from._1)),
JV((t_ms = 1400, kind = "move", x = to._0, y = to._1)),
JV((t_ms = 1550, kind = "move", x = to._0, y = to._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = to._0, y = to._1))
]
post_command(app, "imgui_mouse_play", JV((events = events)))
wait_for_mouse_idle(app)
}

[export]
def main {
with_node_editor_recording_app("examples/tutorial/groups.das",
"groups.apng", 50) $(app) {
var snap = wait_for_widget(app, "{CANVAS}/node_group_1", 15.0f)
"groups.apng", 65) $(app) {
let T_GROUP = "{CANVAS}/node_group_1"
let T_TITLE = "{CANVAS}/node_group_1/GROUP_TITLE"
let T_TEX = "{CANVAS}/node_10"
let T_TINT = "{CANVAS}/node_20"

var snap = wait_for_widget(app, T_GROUP, 15.0f)
if (snap == null) {
panic("{CANVAS}/node_group_1 never rendered wrong app running?")
panic("{T_GROUP} never rendered - wrong app running?")
}
post_command(app, "set_user_control", JV((enabled = false)))

say(app, "node_group(id, size=...) is a pin-less node - a comment box that corrals what's on it.", "{CANVAS}/node_group_1")
wait_for_mouse_idle(app)
say(app, "Two ordinary nodes seeded inside the box.", "{CANVAS}/node_10")
wait_for_mouse_idle(app)
// ---- Beat 1: the setup - one node in, one out ----
record_check_rendered(app, T_GROUP, true)
record_check_rendered(app, T_TEX, true)
record_check_rendered(app, T_TINT, true)
say(app, "Texture is inside, Tint is outside", T_GROUP,
[voice = "A group owns the nodes inside its bounds - membership is spatial. Texture sits inside the box; Tint is outside it."])

// ---- Beat 2: drag Tint INTO the group ----
snap = snapshot(app)
let before_tint_in = cbbox_json(snap, T_TINT)
do_drag(app, title_point(snap, T_TINT), center(snap, T_GROUP))
record_check_changed(app, T_TINT, "canvas_bbox", before_tint_in)
say(app, "drag Tint into the group", T_TINT,
[voice = "Drag Tint into the group's bounds and it becomes a member - no API call, just position."])

// ---- Beat 3: move the group -> both nodes follow ----
snap = snapshot(app)
let before_g1 = cbbox_json(snap, T_GROUP)
let before_tex1 = cbbox_json(snap, T_TEX)
let before_tint1 = cbbox_json(snap, T_TINT)
let grab1 = center(snap, T_TITLE)
do_drag(app, grab1, (grab1._0 + 170.0f, grab1._1 + 90.0f))
record_check_changed(app, T_GROUP, "canvas_bbox", before_g1)
record_check_changed(app, T_TEX, "canvas_bbox", before_tex1)
record_check_changed(app, T_TINT, "canvas_bbox", before_tint1)
say(app, "move the group -> both travel with it", T_GROUP,
[voice = "Move the group now and both nodes travel with it - Texture and the freshly added Tint."])

// ---- Beat 4: drag Tint back OUT of the group ----
snap = snapshot(app)
let before_tint_out = cbbox_json(snap, T_TINT)
let gb = bbox(snap, T_GROUP)
do_drag(app, title_point(snap, T_TINT), (gb.z + 150.0f, (gb.y + gb.w) * 0.5f))
record_check_changed(app, T_TINT, "canvas_bbox", before_tint_out)
say(app, "drag Tint back out", T_TINT,
[voice = "Drag Tint back outside the bounds and it leaves the group again."])

// Drag the group by its title strip (just inside the top edge). The two nodes
// move with it — the editor handles the corral for free.
let g_top = widget_center(snap, "{CANVAS}/node_group_1")
let grab : tuple<float; float> = (g_top._0, g_top._1 - 120.0) // up near the title bar
let dest : tuple<float; float> = (grab._0 + 220.0, grab._1 + 60.0)
say(app, "Drag the group's title bar - the nodes on it come along.", "{CANVAS}/node_group_1")
var events <- [
JV((t_ms = 0, kind = "move", x = grab._0, y = grab._1)),
JV((t_ms = 300, kind = "move", x = grab._0, y = grab._1)),
JV((t_ms = 380, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = grab._0, y = grab._1)),
JV((t_ms = 1400, kind = "move", x = dest._0, y = dest._1)),
JV((t_ms = 1550, kind = "move", x = dest._0, y = dest._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = dest._0, y = dest._1))
]
post_command(app, "imgui_mouse_play", JV((events = events)))
wait_for_mouse_idle(app)
say(app, "The whole group moved together.", "{CANVAS}/node_group_1")
wait_for_mouse_idle(app)
// ---- Beat 5: move the group -> Texture follows, Tint stays ----
snap = snapshot(app)
let before_g2 = cbbox_json(snap, T_GROUP)
let before_tex2 = cbbox_json(snap, T_TEX)
let before_tint2 = cbbox_json(snap, T_TINT)
let grab2 = center(snap, T_TITLE)
do_drag(app, grab2, (grab2._0 - 130.0f, grab2._1 + 70.0f))
record_check_changed(app, T_GROUP, "canvas_bbox", before_g2)
record_check_changed(app, T_TEX, "canvas_bbox", before_tex2)
// Hard self-verify the membership rule: Tint is outside now, so it MUST NOT move.
record_check_unchanged(app, T_TINT, "canvas_bbox", before_tint2)
say(app, "move again -> Texture aboard, Tint left behind", T_GROUP,
Comment on lines +116 to +120
[voice = "Move the group one more time - Texture stays aboard, but Tint, now outside, is left behind."])
}
}
Loading