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
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ doc/source/stdlib/generated/
doc/build/
doc/source/__pycache__/

# Tutorial recordings: raw .apng drivers drop are intermediate; ffmpeg-converted
# .mp4s under doc/source/_static/tutorials/ are tracked.
# Tutorial recordings: the .apng capture plus the voiceover/music intermediates
# are transient; only the ffmpeg-converted .mp4s under
# doc/source/_static/tutorials/ are tracked.
doc/source/_static/tutorials/*.apng
doc/source/_static/tutorials/*_music.wav
doc/source/_static/tutorials/*.mp4.ffmpeg.txt
doc/source/_static/tutorials/voiceover/

# Runtime artifacts dropped next to features / at repo root.
imgui.ini
Expand Down
4 changes: 4 additions & 0 deletions daslib/imgui_editor_playwright.das
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def public ne_delete_link(s : EditorSession; id : int) : JsonValue? {
return post_command(s.app, "delete_link_cmd", JV((editor = s.handle, id = id)))
}

def public ne_flow(s : EditorSession; link_id : int; backward : bool = false) : JsonValue? {
return post_command(s.app, "flow_cmd", JV((editor = s.handle, id = link_id, backward = backward)))
}
Comment on lines +65 to +67
Comment on lines +65 to +67

def public ne_move_node(s : EditorSession; id : int; x : float; y : float) : JsonValue? {
return post_command(s.app, "move_node", JV((editor = s.handle, id = id, x = x, y = y)))
}
Expand Down
Binary file modified doc/source/_static/tutorials/connect_by_drag.mp4
Binary file not shown.
Binary file modified doc/source/_static/tutorials/create_by_drag.mp4
Binary file not shown.
Binary file modified doc/source/_static/tutorials/first_graph.mp4
Binary file not shown.
5 changes: 5 additions & 0 deletions doc/source/tutorials/connect_by_drag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ Walkthrough

.. video:: connect_by_drag.mp4

The recording is voiced and self-verifying: a real synthetic pin-drag commits
the link, the recording asserts it committed (a no-op aborts at teardown), then
pulses the new link with ``flow()`` to show data running output pin 11 -> input
pin 21.

.. literalinclude:: ../../../examples/tutorial/connect_by_drag.das
:language: das
:linenos:
Expand Down
5 changes: 5 additions & 0 deletions doc/source/tutorials/create_by_drag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ Walkthrough

.. video:: create_by_drag.mp4

The recording is voiced and self-verifying: a real synthetic pin-drag released
in empty canvas must open the create menu, and the menu pick must spawn the node
and commit the auto-link (a no-op aborts at teardown). It closes by pulsing the
freshly enqueued link with ``flow()`` to show the editor-made wire is live.

.. literalinclude:: ../../../examples/tutorial/create_by_drag.das
:language: das
:linenos:
Expand Down
9 changes: 9 additions & 0 deletions doc/source/tutorials/first_graph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Walkthrough

.. video:: first_graph.mp4

The recording is voiced and self-verifying: it tours each construct in turn and
asserts every one rendered, then closes by pulsing the link with ``flow()`` to
watch data travel from output pin 11 to input pin 21.

.. literalinclude:: ../../../examples/tutorial/first_graph.das
:language: das
:linenos:
Expand All @@ -58,3 +62,8 @@ Links
``link(id, from_pin, to_pin)`` draws an edge from an output pin to an input
pin. This graph hard-codes the one link; :ref:`tutorial_ne_connect_by_drag`
makes links with the mouse.

``flow(ctx, link_id)`` fires a one-shot data-flow pulse along a link — a marker
that travels the edge and fades over ``style.FlowDuration``. The walkthrough
re-pulses it so the link visibly carries data; from a live host it is the
``flow`` command (``ne_flow`` in the playwright helpers).
Comment on lines +66 to +69
88 changes: 46 additions & 42 deletions tests/integration/record_connect_by_drag.das
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ require imgui/imgui_editor_playwright public
require daslib/json public
require daslib/json_boost public

//! Driver: record connect_by_drag.apng — the signature gesture, narrated and
//! driven by a SLOW synthetic mouse drag (with the cursor sprite + trail on).
//! Press on the output pin, drag across, release on the input pin → link.
//! Run with `daslang tests/integration/record_connect_by_drag.das`, then ffmpeg
//! the .apng to .mp4. The headless regression for the same gesture is
//! test_connect_drag.das.
//! Driver: record connect_by_drag.apng - the signature gesture, voiced and
//! self-verifying. A REAL synthetic pin-drag (press on the output pin, travel
//! across, release on the input pin) commits a link; the recording asserts the
//! link committed (a no-op aborts at teardown) and pulses it to show data flow.
//! The headless regression for the same gesture is test_connect_drag.das.
Comment on lines +11 to +15

let CANVAS = "MAIN_WIN/graph"

Expand All @@ -27,53 +26,58 @@ def widget_center(var snap : JsonValue?; ident : string) : tuple<float; float> {
def main {
with_node_editor_recording_app("examples/tutorial/connect_by_drag.das",
"connect_by_drag.apng", 50) $(app) {
var snap = wait_for_widget(app, "{CANVAS}/node_1/pin_11", 15.0f)
let T_OUT = "{CANVAS}/node_1/pin_11"
let T_IN = "{CANVAS}/node_2/pin_21"
let T_LINK = "{CANVAS}/link_100"

let s = ne_open(app, CANVAS)
if (s.handle == uint64(0)) {
panic("{CANVAS} never rendered / no editor handle - wrong app running?")
}
var snap = wait_for_widget(app, T_OUT, 15.0f)
if (snap == null) {
panic("{CANVAS}/node_1/pin_11 never rendered wrong app running?")
panic("{T_OUT} never rendered - wrong app running?")
}
let out_c = widget_center(snap, "{CANVAS}/node_1/pin_11")
let in_c = widget_center(snap, "{CANVAS}/node_2/pin_21")
let out_c = widget_center(snap, T_OUT)
let in_c = widget_center(snap, T_IN)
Comment on lines +41 to +42

// Park the cursor on the output pin, then narrate. wait_for_mouse_idle polls
// imgui_mouse_status until the scripted move finishes (the cursor reaches its
// target) — event-driven, no blind sleep.
// ---- Beat 1: the setup ----
move_to(app, out_c, 700)
wait_for_mouse_idle(app)
say(app, "begin_create opens the create scope. Press on an output pin and start dragging...", "{CANVAS}/node_1/pin_11")
record_check_rendered(app, T_OUT, true)
record_check_rendered(app, T_IN, true)
say(app, "drag output -> input", T_OUT,
[voice = "To connect two nodes you drag from an output pin to an input pin. begin_create opens the create scope."])

// The hero gesture. Timing is VERBATIM from test_connect_drag (the proven
// committing sequence): park ON the output pin through the press, travel the
// full distance (~900ms) with the button held, park ON the input pin through
// the release. (drag_along presses ~10% into the travel — off-pin for a small
// target — so we hand-build the timeline.)
// ---- Beat 2: the hero drag (proven committing timeline) ----
// Press ON the output pin, travel the full distance with the button held, release
// ON the input pin. Verbatim from test_connect_drag (drag_along presses ~10% into
// the travel - off-pin for a small target - so the timeline is hand-built).
var events <- [
JV((t_ms = 0, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 350, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 400, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 1400, kind = "move", x = in_c._0, y = in_c._1)),
JV((t_ms = 1550, kind = "move", x = in_c._0, y = in_c._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = in_c._0, y = in_c._1))
JV((t_ms = 0, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 350, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 400, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 1400, kind = "move", x = in_c._0, y = in_c._1)),
JV((t_ms = 1550, kind = "move", x = in_c._0, y = in_c._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = in_c._0, y = in_c._1))
]
post_command(app, "imgui_mouse_play", JV((events = events)))

// Let the synth gesture FULLY drain (cursor arrives + button releases) before
// touching the snapshot API. wait_for_widget below snapshot-polls every frame;
// a snapshot landing mid-drag during an active recording (APNG capture already
// contends for each frame) disrupts the release — accept_new_item never fires
// and the link silently fails to commit. Waiting for mouse-idle first closes that
// window. (The non-recording test tolerates the concurrent polling — without
// frame-capture contention the loop keeps up — which is why it always passed and
// masked this.)
wait_for_mouse_idle(app)

// Now that the gesture is done, poll for the committed link and narrate the result.
var snapL = wait_for_widget(app, "{CANVAS}/link_100", 10.0f)
if (snapL != null) {
say(app, "...release on a compatible input pin. query_new_link reports the pair; accept_new_item commits the link.", "{CANVAS}/link_100")
} else {
say(app, "(the drag did not land - see test_connect_drag for the geometry)", "{CANVAS}/node_2/pin_21")
// Hard self-verify: the drag MUST commit the link, or the recording aborts at teardown.
record_check_rendered(app, T_LINK, true)

// ---- Beat 3: the committed link + data-flow pulse ----
let dwell = say_begin(app, "link 11 -> 21 created", T_LINK,
[voice = "query_new_link reports the pair, accept_new_item commits it, and now the link carries data from the output to the input."])
var pulses = int(dwell) / 500
if (pulses < 4) { pulses = 4 }
let interval = dwell / uint(pulses)
for (_i in range(pulses)) {
ne_flow(s, 100)
sleep(interval)
}
}
}
95 changes: 56 additions & 39 deletions tests/integration/record_create_by_drag.das
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ require imgui/imgui_editor_playwright public
require daslib/json public
require daslib/json_boost public

//! Driver: record create_by_drag.apng drag from the source output pin into EMPTY
//! Driver: record create_by_drag.apng - drag from the source output pin into EMPTY
//! canvas, pick a kind from the create-node menu, and watch the new node spawn at the
//! drop point already wired to the source. Narrated, with a slow synthetic pin-drag then
//! a real synthetic click on the menu. Run with
//! `daslang tests/integration/record_create_by_drag.das`, then ffmpeg the .apng to .mp4.
//!
//! NB: set_user_control(false) hands IO fully to the synthetic timeline — without it the
//! real OS cursor races the synth and the drag / menu click are eaten. The headless
//! regression for the same gesture is test_create_by_drag.das.
//! drop point already wired to the source. Voiced and self-verifying: the drag MUST open
//! the create menu and the menu pick MUST spawn the node + auto-link, or the recording
//! aborts at teardown. Closes by pulsing the freshly enqueued link to show it is live.
//! The headless regression for the same gesture is test_create_by_drag.das.

let CANVAS = "MAIN_WIN/graph"

Expand All @@ -29,49 +26,69 @@ def widget_center(var snap : JsonValue?; ident : string) : tuple<float; float> {
[export]
def main {
with_node_editor_recording_app("examples/tutorial/create_by_drag.das",
"create_by_drag.apng", 50) $(app) {
var snap = wait_for_widget(app, "{CANVAS}/node_1/pin_11", 15.0f)
"create_by_drag.apng", 60) $(app) {
let T_OUT = "{CANVAS}/node_1/pin_11"
let T_MENU = "{CANVAS}/CREATE_MENU/ADD_MUL"
let T_NODE = "{CANVAS}/node_200"
let T_LINK = "{CANVAS}/link_100"

let s = ne_open(app, CANVAS)
if (s.handle == uint64(0)) {
panic("{CANVAS} never rendered / no editor handle - wrong app running?")
}
var snap = wait_for_widget(app, T_OUT, 15.0f)
if (snap == null) {
panic("{CANVAS}/node_1/pin_11 never rendered wrong app running?")
panic("{T_OUT} never rendered - wrong app running?")
}
// Synthetic IO only — no real-cursor race (see header note).
post_command(app, "set_user_control", JV((enabled = false)))

let out_c = widget_center(snap, "{CANVAS}/node_1/pin_11")
let drop : tuple<float; float> = (680.0, 430.0) // empty canvas
let add_mul = "{CANVAS}/CREATE_MENU/ADD_MUL"
let out_c = widget_center(snap, T_OUT)
let drop : tuple<float; float> = (680.0f, 430.0f) // empty canvas

// ---- Beat 1: the setup ----
move_to(app, out_c, 700)
wait_for_mouse_idle(app)
say(app, "Drag from an output pin - but release in EMPTY canvas instead of on a pin.", "{CANVAS}/node_1/pin_11")
record_check_rendered(app, T_OUT, true)
say(app, "drag output -> EMPTY canvas", T_OUT,
[voice = "Drag from an output pin again - but this time release in empty canvas, not on a pin."])

// The hero gesture. Timing VERBATIM from connect_by_drag (the proven committing
// sequence): park ON the pin through the press, travel ~900ms with the button held,
// park at the drop point through the release.
// ---- Beat 2: the hero drag into empty canvas (proven committing timeline) ----
// Press ON the output pin, travel the full distance with the button held, release
// in empty canvas. Verbatim timing from connect_by_drag.
var events <- [
JV((t_ms = 0, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 350, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 400, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 1400, kind = "move", x = drop._0, y = drop._1)),
JV((t_ms = 1550, kind = "move", x = drop._0, y = drop._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = drop._0, y = drop._1))
JV((t_ms = 0, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 350, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 400, kind = "button", button = 0, action = "press")),
JV((t_ms = 500, kind = "move", x = out_c._0, y = out_c._1)),
JV((t_ms = 1400, kind = "move", x = drop._0, y = drop._1)),
JV((t_ms = 1550, kind = "move", x = drop._0, y = drop._1)),
JV((t_ms = 1650, kind = "button", button = 0, action = "release")),
JV((t_ms = 1800, kind = "move", x = drop._0, y = drop._1))
]
post_command(app, "imgui_mouse_play", JV((events = events)))
wait_for_mouse_idle(app)

var menuSnap = wait_for_widget(app, add_mul, 6.0f)
print("create menu opened -> {menuSnap != null}\n")
say(app, "show_new_node_drag reports the source pin + drop point; the app opens its create menu.", add_mul)
click(app, add_mul)
// Hard self-verify: a drop in empty canvas MUST open the create menu (show_new_node_drag
// fired), or the recording aborts at teardown.
record_check_rendered(app, T_MENU, true)
say(app, "show_new_node_drag -> create menu", T_MENU,
[voice = "Releasing in empty canvas fires show_new_node_drag - it reports the source pin and the drop point, so the app opens its create-node menu right there."])

// ---- Beat 3: pick a kind -> spawn + auto-connect ----
click(app, T_MENU)
// Hard self-verify: the pick MUST spawn the node AND the auto-link, or abort.
record_check_rendered(app, T_NODE, true)
record_check_rendered(app, T_LINK, true)
say(app, "spawned + auto-wired", T_NODE,
[voice = "Picking Multiply spawns the node at the drop point, and enqueue_new_link wires it straight back to the source pin."])

var nSnap = wait_for_widget(app, "{CANVAS}/link_100", 6.0f)
print("node + auto-link created -> {nSnap != null}\n")
if (nSnap != null) {
say(app, "Picked Multiply: spawned at the drop point and enqueue_new_link wired it to the source.", "{CANVAS}/node_200")
} else {
say(app, "(the create did not land - see test_create_by_drag for the geometry)", "{CANVAS}/node_1")
// ---- Beat 4: pulse the new link to show it is live ----
let dwell = say_begin(app, "the new link is live", T_LINK,
[voice = "The link the editor just made carries data exactly like a hand-dragged one - we can pulse it to watch it flow from the source into the node we just created."])
var pulses = int(dwell) / 500
if (pulses < 4) { pulses = 4 }
let interval = dwell / uint(pulses)
for (_i in range(pulses)) {
ne_flow(s, 100)
sleep(interval)
}
}
}
Loading
Loading