diff --git a/doc/source/_static/tutorials/bar_groups_and_pie.mp4 b/doc/source/_static/tutorials/bar_groups_and_pie.mp4 index 813babf..8a1eec1 100644 Binary files a/doc/source/_static/tutorials/bar_groups_and_pie.mp4 and b/doc/source/_static/tutorials/bar_groups_and_pie.mp4 differ diff --git a/doc/source/_static/tutorials/colormaps_and_style.mp4 b/doc/source/_static/tutorials/colormaps_and_style.mp4 index a1f5636..ddd3d6e 100644 Binary files a/doc/source/_static/tutorials/colormaps_and_style.mp4 and b/doc/source/_static/tutorials/colormaps_and_style.mp4 differ diff --git a/doc/source/_static/tutorials/drag_tools.mp4 b/doc/source/_static/tutorials/drag_tools.mp4 index d590c29..393326a 100644 Binary files a/doc/source/_static/tutorials/drag_tools.mp4 and b/doc/source/_static/tutorials/drag_tools.mp4 differ diff --git a/doc/source/_static/tutorials/heatmap_histogram.mp4 b/doc/source/_static/tutorials/heatmap_histogram.mp4 index ab8740b..b8f5510 100644 Binary files a/doc/source/_static/tutorials/heatmap_histogram.mp4 and b/doc/source/_static/tutorials/heatmap_histogram.mp4 differ diff --git a/doc/source/_static/tutorials/line_plot.mp4 b/doc/source/_static/tutorials/line_plot.mp4 index 5e3f589..3002f23 100644 Binary files a/doc/source/_static/tutorials/line_plot.mp4 and b/doc/source/_static/tutorials/line_plot.mp4 differ diff --git a/doc/source/_static/tutorials/multi_axes.mp4 b/doc/source/_static/tutorials/multi_axes.mp4 index e431c1b..967fa15 100644 Binary files a/doc/source/_static/tutorials/multi_axes.mp4 and b/doc/source/_static/tutorials/multi_axes.mp4 differ diff --git a/doc/source/_static/tutorials/multi_series.mp4 b/doc/source/_static/tutorials/multi_series.mp4 index a9da4b2..626b1a2 100644 Binary files a/doc/source/_static/tutorials/multi_series.mp4 and b/doc/source/_static/tutorials/multi_series.mp4 differ diff --git a/doc/source/_static/tutorials/query_and_hover.mp4 b/doc/source/_static/tutorials/query_and_hover.mp4 index 878f5da..f31e9e0 100644 Binary files a/doc/source/_static/tutorials/query_and_hover.mp4 and b/doc/source/_static/tutorials/query_and_hover.mp4 differ diff --git a/doc/source/_static/tutorials/realtime_scroll.mp4 b/doc/source/_static/tutorials/realtime_scroll.mp4 index 9b4ab88..d8eefa8 100644 Binary files a/doc/source/_static/tutorials/realtime_scroll.mp4 and b/doc/source/_static/tutorials/realtime_scroll.mp4 differ diff --git a/doc/source/_static/tutorials/shaded_and_stairs.mp4 b/doc/source/_static/tutorials/shaded_and_stairs.mp4 index 9a3d2e4..071f9ca 100644 Binary files a/doc/source/_static/tutorials/shaded_and_stairs.mp4 and b/doc/source/_static/tutorials/shaded_and_stairs.mp4 differ diff --git a/doc/source/_static/tutorials/subplots.mp4 b/doc/source/_static/tutorials/subplots.mp4 index b120d3c..e7887a1 100644 Binary files a/doc/source/_static/tutorials/subplots.mp4 and b/doc/source/_static/tutorials/subplots.mp4 differ diff --git a/tests/integration/record_bar_groups_and_pie.das b/tests/integration/record_bar_groups_and_pie.das index c57004c..eaeb4f5 100644 --- a/tests/integration/record_bar_groups_and_pie.das +++ b/tests/integration/record_bar_groups_and_pie.das @@ -22,15 +22,6 @@ let PIE = "PLOT_WIN/PIE" let SERIES = "industrial" let LEAD_MS = 800u -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (gesture + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/bar_groups_and_pie.das", "bar_groups_and_pie.apng", 45) $(app) { @@ -52,25 +43,25 @@ def main { // ---- Beat 2: legend toggle on the grouped bars (hide across all groups, then restore) ---- let d_leg = say_begin(app, "toggle a series in the legend", BARS, [voice = "Each cluster shares one legend. Click a series to hide it across every group at once, then click again to bring it back."]) - let t_leg = ref_time_ticks() - sleep(LEAD_MS) + let t_leg = record_frame_count(app) + hold_content(app, LEAD_MS) legend_toggle(bars, SERIES) record_check(app, "{SERIES} hidden across all groups after the legend click", wait_for_series_shown(bars, SERIES, false) != null) - sleep(900u) // dwell so the whole-series-gone state reads before restoring + hold_content(app, 900u) // dwell so the whole-series-gone state reads before restoring legend_toggle(bars, SERIES) record_check(app, "{SERIES} restored after the second click", wait_for_series_shown(bars, SERIES, true) != null) - hold_remainder(d_leg, t_leg) + hold_remainder_content(app, d_leg, t_leg) // ---- Beat 3: the pie chart ---- let d_pie = say_begin(app, "a pie splits the whole", PIE, [voice = "The pie chart splits a single whole into slices, each labelled with its share as a percentage. A different shape from the same simple call."]) - let t_pie = ref_time_ticks() + let t_pie = record_frame_count(app) var psnap = snapshot(app) move_to(app, plot_center(psnap, pie), 900) wait_for_mouse_idle(app) record_check_rendered(app, PIE, true) - hold_remainder(d_pie, t_pie) + hold_remainder_content(app, d_pie, t_pie) } } diff --git a/tests/integration/record_colormaps_and_style.das b/tests/integration/record_colormaps_and_style.das index 65892e4..3823696 100644 --- a/tests/integration/record_colormaps_and_style.das +++ b/tests/integration/record_colormaps_and_style.das @@ -22,15 +22,6 @@ require daslib/json_boost public let HEAT = "PLOT_WIN/HEAT" let LINES = "PLOT_WIN/LINES" -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (glide + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/colormaps_and_style.das", "colormaps_and_style.apng", 45) $(app) { @@ -58,21 +49,21 @@ def main { // ---- Beat 2: the colorbar reads as a smooth ramp (the sequential map) ---- let d_heat = say_begin(app, "a sequential map reads as a ramp", HEAT, [voice = "Viridis is a sequential colormap, so the colorbar beside the heatmap reads as a smooth ramp from low to high. That is the natural fit for a heatmap."]) - let t_heat = ref_time_ticks() + let t_heat = record_frame_count(app) // Drift toward the colorbar on the heatmap's right edge. move_to(app, (hx + hw * 0.96f, hy + hh * 0.5f), 900) wait_for_mouse_idle(app) record_check_rendered(app, HEAT, true) - hold_remainder(d_heat, t_heat) + hold_remainder_content(app, d_heat, t_heat) // ---- Beat 3: auto-colored series ---- let d_lines = say_begin(app, "series auto-color from the map", LINES, [voice = "On the right, six series in one colormap scope. Each line takes the next color from the map, so a family of related series stays consistent with no per-series color set."]) - let t_lines = ref_time_ticks() + let t_lines = record_frame_count(app) var lsnap = snapshot(app) move_to(app, plot_center(lsnap, lines), 900) wait_for_mouse_idle(app) record_check_rendered(app, LINES, true) - hold_remainder(d_lines, t_lines) + hold_remainder_content(app, d_lines, t_lines) } } diff --git a/tests/integration/record_heatmap_histogram.das b/tests/integration/record_heatmap_histogram.das index b9d125d..edca44c 100644 --- a/tests/integration/record_heatmap_histogram.das +++ b/tests/integration/record_heatmap_histogram.das @@ -21,15 +21,6 @@ require daslib/json_boost public let HEAT = "PLOT_WIN/HEAT" let HIST = "PLOT_WIN/HIST" -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (glide + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/heatmap_histogram.das", "heatmap_histogram.apng", 45) $(app) { @@ -57,7 +48,7 @@ def main { // ---- Beat 2: trace the heatmap grid ---- let d_heat = say_begin(app, "a heatmap colors a grid by value", HEAT, [voice = "The heatmap draws a rows by columns grid, and each cell takes its color from its value through the active colormap, so the structure in the data shows up as a pattern of colors."]) - let t_heat = ref_time_ticks() + let t_heat = record_frame_count(app) // Glide diagonally across the grid so the cursor sweeps the gradient. move_to(app, (hx + hw * 0.1f, hy + hh * 0.15f), 300) wait_for_mouse_idle(app) @@ -66,16 +57,16 @@ def main { record_check(app, "the cursor lands hovered over the heatmap grid", wait_for_hovered(heat, true) != null) record_check_rendered(app, HEAT, true) - hold_remainder(d_heat, t_heat) + hold_remainder_content(app, d_heat, t_heat) // ---- Beat 3: the histogram ---- let d_hist = say_begin(app, "a histogram bins a sample set", HIST, [voice = "The histogram bins a flat array of samples into bars, showing the shape of the distribution. Here, a count per bin, with the bin count picked automatically from the sample size."]) - let t_hist = ref_time_ticks() + let t_hist = record_frame_count(app) var gsnap = snapshot(app) move_to(app, plot_center(gsnap, hist), 900) wait_for_mouse_idle(app) record_check_rendered(app, HIST, true) - hold_remainder(d_hist, t_hist) + hold_remainder_content(app, d_hist, t_hist) } } diff --git a/tests/integration/record_line_plot.das b/tests/integration/record_line_plot.das index 0174a17..d76fd56 100644 --- a/tests/integration/record_line_plot.das +++ b/tests/integration/record_line_plot.das @@ -62,15 +62,6 @@ def double_click(app : ImguiApp; pt : tuple) { wait_for_mouse_idle(app) } -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (gesture + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/line_plot.das", "line_plot.apng", 50) $(app) { @@ -93,35 +84,35 @@ def main { let before_pan = field_json(snapshot(app), PLOT, "x_max") let d_pan = say_begin(app, "drag to pan", PLOT, [voice = "Hold the left button inside the plot and drag to pan. The whole view slides with the cursor."]) - let t_pan = ref_time_ticks() - sleep(LEAD_MS) + let t_pan = record_frame_count(app) + hold_content(app, LEAD_MS) pan_drag(app, center, left) record_check_changed(app, PLOT, "x_max", before_pan) - hold_remainder(d_pan, t_pan) + hold_remainder_content(app, d_pan, t_pan) // ---- Beat 3: zoom (scroll wheel) ---- let before_zoom = field_json(snapshot(app), PLOT, "x_max") let d_zoom = say_begin(app, "scroll to zoom", PLOT, [voice = "Scroll the wheel over the plot to zoom. It zooms in around the cursor, toward the data under it."]) - let t_zoom = ref_time_ticks() - sleep(LEAD_MS) + let t_zoom = record_frame_count(app) + hold_content(app, LEAD_MS) move_to(app, center, 300) wait_for_mouse_idle(app) for (_b in range(3)) { scroll_zoom(app, 2) - sleep(260u) + hold_content(app, 260u) } record_check_changed(app, PLOT, "x_max", before_zoom) - hold_remainder(d_zoom, t_zoom) + hold_remainder_content(app, d_zoom, t_zoom) // ---- Beat 4: fit (double click) ---- let before_fit = field_json(snapshot(app), PLOT, "x_max") let d_fit = say_begin(app, "double click to fit", PLOT, [voice = "Double click anywhere in the plot to fit the axes back to the data. One gesture resets the view."]) - let t_fit = ref_time_ticks() - sleep(LEAD_MS) + let t_fit = record_frame_count(app) + hold_content(app, LEAD_MS) double_click(app, center) record_check_changed(app, PLOT, "x_max", before_fit) - hold_remainder(d_fit, t_fit) + hold_remainder_content(app, d_fit, t_fit) } } diff --git a/tests/integration/record_multi_axes.das b/tests/integration/record_multi_axes.das index 8740385..ece5ff4 100644 --- a/tests/integration/record_multi_axes.das +++ b/tests/integration/record_multi_axes.das @@ -23,15 +23,6 @@ let PLOT = "PLOT_WIN/CHART" let LEAD_MS = 800u let HOLD_TOL = 1.0lf // an untouched axis' range must not drift more than this (data units) -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (gesture + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/multi_axes.das", "multi_axes.apng", 45) $(app) { @@ -56,15 +47,15 @@ def main { let b2_y2 = plot_axis_limit(b2, s, "y2_max") let d_left = say_begin(app, "drag the left axis to pan temperature alone", PLOT, [voice = "Grab the left temperature axis and drag it. Only temperature pans. Pressure on the right does not move."]) - let t_left = ref_time_ticks() - sleep(LEAD_MS) + let t_left = record_frame_count(app) + hold_content(app, LEAD_MS) drag_axis(s, ImAxis.Y1, -110.0f) var dleft = wait_for_axis_limit_changed(s, "y_max", b2_y1) record_check(app, "temperature (Y1) panned by the left-axis drag", dleft != null) // Held-steady check on the SAME frame the pan was detected (dleft), not a later snapshot. record_check(app, "pressure (Y2) held steady while Y1 dragged", abs(plot_axis_limit(dleft, s, "y2_max") - b2_y2) < HOLD_TOL) - hold_remainder(d_left, t_left) + hold_remainder_content(app, d_left, t_left) // ---- Beat 3: drag the RIGHT axis (pressure / Y2) ---- var b3 = snapshot(app) @@ -72,13 +63,13 @@ def main { let b3_y2 = plot_axis_limit(b3, s, "y2_max") let d_right = say_begin(app, "drag the right axis to pan pressure alone", PLOT, [voice = "Now the right pressure axis. Drag it, and only pressure pans. Temperature on the left stays put. Each axis moves on its own."]) - let t_right = ref_time_ticks() - sleep(LEAD_MS) + let t_right = record_frame_count(app) + hold_content(app, LEAD_MS) drag_axis(s, ImAxis.Y2, 120.0f) var dright = wait_for_axis_limit_changed(s, "y2_max", b3_y2) record_check(app, "pressure (Y2) panned by the right-axis drag", dright != null) record_check(app, "temperature (Y1) held steady while Y2 dragged", abs(plot_axis_limit(dright, s, "y_max") - b3_y1) < HOLD_TOL) - hold_remainder(d_right, t_right) + hold_remainder_content(app, d_right, t_right) } } diff --git a/tests/integration/record_multi_series.das b/tests/integration/record_multi_series.das index d60e25a..fc26c27 100644 --- a/tests/integration/record_multi_series.das +++ b/tests/integration/record_multi_series.das @@ -20,15 +20,6 @@ let BARS = "PLOT_WIN/BARS" let SERIES = "cos" let LEAD_MS = 800u -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (gesture + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/multi_series.das", "multi_series.apng", 45) $(app) { @@ -50,19 +41,19 @@ def main { // ---- Beat 2: legend toggle - hide ---- let d_hide = say_begin(app, "click a legend entry to hide", PLOT, [voice = "Every series has a legend entry. Click one to hide that series. Here the co-sine curve disappears."]) - let t_hide = ref_time_ticks() - sleep(LEAD_MS) + let t_hide = record_frame_count(app) + hold_content(app, LEAD_MS) legend_toggle(s, SERIES) record_check(app, "{SERIES} hidden after legend click", wait_for_series_shown(s, SERIES, false) != null) - hold_remainder(d_hide, t_hide) + hold_remainder_content(app, d_hide, t_hide) // ---- Beat 3: legend toggle - show ---- let d_show = say_begin(app, "click again to show", PLOT, [voice = "Click the same entry again and the series comes right back."]) - let t_show = ref_time_ticks() - sleep(LEAD_MS) + let t_show = record_frame_count(app) + hold_content(app, LEAD_MS) legend_toggle(s, SERIES) record_check(app, "{SERIES} shown after second legend click", wait_for_series_shown(s, SERIES, true) != null) - hold_remainder(d_show, t_show) + hold_remainder_content(app, d_show, t_show) } } diff --git a/tests/integration/record_query_and_hover.das b/tests/integration/record_query_and_hover.das index f54c9e0..33bb133 100644 --- a/tests/integration/record_query_and_hover.das +++ b/tests/integration/record_query_and_hover.das @@ -5,7 +5,6 @@ options no_unused_function_arguments = false require imgui/imgui_implot_app public require imgui/imgui_implot_playwright public -require implot require daslib/json public require daslib/json_boost public @@ -20,15 +19,6 @@ require daslib/json_boost public let CHART = "PLOT_WIN/CHART" -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed work -// (glide + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/query_and_hover.das", "query_and_hover.apng", 45) $(app) { @@ -54,19 +44,19 @@ def main { // ---- Beat 2: the readout follows the cursor (glide to the left third) ---- let d_l = say_begin(app, "the readout follows the cursor", CHART, [voice = "Move the cursor and the readout follows. Here, near the left, the label shows the x and y under the pointer, updated every frame."]) - let t_l = ref_time_ticks() + let t_l = record_frame_count(app) move_to(app, (px + pw * 0.22f, py + ph * 0.42f), 900) wait_for_mouse_idle(app) var hl = wait_for_hovered(s, true) let mx_l = mouse_x(hl, s) record_check(app, "readout tracks the cursor near the left (x in [8,40], got {mx_l})", mx_l >= 8.0lf && mx_l <= 40.0lf) - hold_remainder(d_l, t_l) + hold_remainder_content(app, d_l, t_l) // ---- Beat 3: read any point (glide across to the right third) ---- let d_r = say_begin(app, "read any point", CHART, [voice = "Glide across to the right and the crosshair and the label track right along with it. You can read off the value at any point on the plot."]) - let t_r = ref_time_ticks() + let t_r = record_frame_count(app) move_to(app, (px + pw * 0.74f, py + ph * 0.58f), 1100) wait_for_mouse_idle(app) var hr = wait_for_hovered(s, true) @@ -75,7 +65,7 @@ def main { mx_r >= 60.0lf && mx_r <= 92.0lf) record_check(app, "the readout moved right with the cursor (left {mx_l} < right {mx_r})", mx_r > mx_l + 20.0lf) - hold_remainder(d_r, t_r) + hold_remainder_content(app, d_r, t_r) } } diff --git a/tests/integration/record_realtime_scroll.das b/tests/integration/record_realtime_scroll.das index c5da26e..d566a54 100644 --- a/tests/integration/record_realtime_scroll.das +++ b/tests/integration/record_realtime_scroll.das @@ -5,7 +5,6 @@ options no_unused_function_arguments = false require imgui/imgui_implot_app public require imgui/imgui_implot_playwright public -require implot require daslib/json public require daslib/json_boost public @@ -19,16 +18,6 @@ require daslib/json_boost public let SCROLL = "PLOT_WIN/SCROLL" let SCROLL_MARGIN = 2.0lf // x_min must advance at least this far over the narration (data units) -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work so the next say_begin can't fire early and overlap the voice. For this tutorial the -// hold doubles as the observation window: the plot keeps streaming while the caption holds. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/realtime_scroll.das", "realtime_scroll.apng", 45) $(app) { @@ -52,8 +41,8 @@ def main { let x0 = plot_axis_limits(snapshot(app), s).x_min let d_scroll = say_begin(app, "the window scrolls with time", SCROLL, [voice = "Watch it scroll. As time advances the window slides to the right. New samples push in on the right while the oldest ones drop off the left."]) - let t_scroll = ref_time_ticks() - hold_remainder(d_scroll, t_scroll) + let t_scroll = record_frame_count(app) + hold_remainder_content(app, d_scroll, t_scroll) let x1 = plot_axis_limits(snapshot(app), s).x_min record_check(app, "x window scrolled forward (x_min advanced over the narration)", x1 > x0 + SCROLL_MARGIN) diff --git a/tests/integration/record_shaded_and_stairs.das b/tests/integration/record_shaded_and_stairs.das index 756d29f..b38f41f 100644 --- a/tests/integration/record_shaded_and_stairs.das +++ b/tests/integration/record_shaded_and_stairs.das @@ -20,15 +20,6 @@ require daslib/json_boost public let BAND = "PLOT_WIN/BAND" let STAIRS = "PLOT_WIN/STAIRS" -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (glide + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - [export] def main { with_implot_recording_app("examples/tutorial/shaded_and_stairs.das", "shaded_and_stairs.apng", 45) $(app) { @@ -56,23 +47,23 @@ def main { // ---- Beat 2: trace the widening band ---- let d_band = say_begin(app, "a band fills between two curves", BAND, [voice = "The band fills the region between a lower and an upper curve. It widens to the right, the way a confidence interval grows, and the mean line is drawn on top."]) - let t_band = ref_time_ticks() + let t_band = record_frame_count(app) // Glide along the band, left to right, at mid-height to trace the widening fill. move_to(app, (bx + bw * 0.08f, by + bh * 0.5f), 300) wait_for_mouse_idle(app) move_to(app, (bx + bw * 0.92f, by + bh * 0.5f), 1200) wait_for_mouse_idle(app) record_check_rendered(app, BAND, true) - hold_remainder(d_band, t_band) + hold_remainder_content(app, d_band, t_band) // ---- Beat 3: down to the step plot ---- let d_stairs = say_begin(app, "stairs hold each sample", STAIRS, [voice = "Below, the step plot holds each sample until the next, drawing piecewise-constant data, and the shaded fill runs from the steps down to the baseline."]) - let t_stairs = ref_time_ticks() + let t_stairs = record_frame_count(app) var ssnap = snapshot(app) move_to(app, plot_center(ssnap, stairs), 900) wait_for_mouse_idle(app) record_check_rendered(app, STAIRS, true) - hold_remainder(d_stairs, t_stairs) + hold_remainder_content(app, d_stairs, t_stairs) } } diff --git a/tests/integration/record_subplots.das b/tests/integration/record_subplots.das index d2d546f..df87f29 100644 --- a/tests/integration/record_subplots.das +++ b/tests/integration/record_subplots.das @@ -5,7 +5,6 @@ options no_unused_function_arguments = false require imgui/imgui_implot_app public require imgui/imgui_implot_playwright public -require implot require math require daslib/json public require daslib/json_boost public @@ -24,15 +23,6 @@ let COS = "PLOT_WIN/GRID/P_COS" let LEAD_MS = 800u let HOLD_TOL = 0.5lf // a non-boxed cell's range must not drift more than this (data units) -// Hold a beat's caption for the remainder of its voice dwell, measuring real elapsed -// work (gesture + verify) so the next say_begin can't fire early and overlap the voice. -def hold_remainder(dwell : uint; t0 : int64) { - let elapsed = uint(get_time_usec(t0) / 1000) - if (dwell > elapsed) { - sleep(dwell - elapsed) - } -} - // Box-select zoom is a RIGHT-button drag (ImPlot's default Select binding): press inside the // cell, drag a box, release -> the cell zooms to the box. Walk a few waypoints so the rubber // band is visible on screen. Unlike a pan, the box is press-corner to release-corner (not @@ -93,14 +83,14 @@ def main { let to_pt = (px + pw * 0.75f, py + ph * 0.80f) let d_zoom = say_begin(app, "right drag a box to zoom one cell", SIN, [voice = "Right drag a box inside any cell to zoom into it. Because these cells are not linked, only that cell zooms. The others stay exactly where they were."]) - let t_zoom = ref_time_ticks() - sleep(LEAD_MS) + let t_zoom = record_frame_count(app) + hold_content(app, LEAD_MS) box_zoom(app, from_pt, to_pt) var dz = wait_for_axis_limit_changed(sin, "x_max", sin_x0) record_check(app, "sin cell zoomed in by the box drag (x_max moved)", dz != null) // Independence: the neighbour cell's range is untouched, read on the same frame. record_check(app, "cos cell unchanged by the box zoom (cells are independent)", abs(plot_axis_limit(dz, cos, "x_max") - cos_x0) < HOLD_TOL) - hold_remainder(d_zoom, t_zoom) + hold_remainder_content(app, d_zoom, t_zoom) } } diff --git a/utils/rerecord.ps1 b/utils/rerecord.ps1 new file mode 100644 index 0000000..eb03596 --- /dev/null +++ b/utils/rerecord.ps1 @@ -0,0 +1,141 @@ +# utils/rerecord.ps1 - clean full re-record of THIS repo's tutorial videos. +# +# Default (no args) re-records the ENTIRE repo in four phases: +# Clean wipe apng/mp4/music/ffmpeg.txt + voiceover/ under doc/source/_static/tutorials +# Prepare per driver: prepare_recording.das -> TTS each say() line via Kokoro (:8880) +# Record per driver: run it -> .apng + voiceover sidecar +# Convert per .apng: convert_recording.das -> mux video + music bed + voiceovers -> .mp4 +# +# Run one repo at a time. Kokoro TTS must be up at :8880 for the Prepare phase. +# The recording infra (prepare/convert) + the imgui module live in dasImgui; -DasImguiRoot +# defaults to the sibling checkout (../dasImgui), else D:/DASPKG/dasImgui. +# Filters: -Only NAME (single driver, NAME = stem without record_/.das), -From NAME (resume), +# -Skip "a,b", -Skip, -DryRun, -StopOnFail. + +param( + [string]$DaslangExe = $(if ($env:DASLANG) { $env:DASLANG } else { "D:/Work/daScript/bin/Release/daslang.exe" }), + [string]$DasRoot = "D:/Work/daScript", # convert: music render + sf2 live under the daslang source tree + [string]$DasImguiRoot= "", # holds prepare/convert + the imgui module; default below + [string]$Voice = "bf_emma", + [switch]$SkipClean, + [switch]$SkipPrepare, + [switch]$SkipRecord, + [switch]$SkipConvert, + [string]$From = "", + [string]$Skip = "", + [string]$Only = "", + [switch]$DryRun, + [switch]$StopOnFail +) + +$ErrorActionPreference = "Continue" +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + +# ===== per-repo config (depends on dasImgui; loads both modules) ===== +if (-not $DasImguiRoot) { + $sib = Join-Path $RepoRoot "../dasImgui" + $DasImguiRoot = if (Test-Path $sib) { (Resolve-Path $sib).Path } else { "D:/DASPKG/dasImgui" } +} +$ModuleArgs = @("-load_module", $DasImguiRoot, "-load_module", $RepoRoot) +$AssetRoot = $RepoRoot +# ==================================================================== + +$tutDir = Join-Path $RepoRoot "doc/source/_static/tutorials" +$voDir = Join-Path $tutDir "voiceover" +$prepareScript = Join-Path $DasImguiRoot "utils/prepare_recording.das" +$convertScript = Join-Path $DasImguiRoot "utils/convert_recording.das" +$scanDir = Join-Path $RepoRoot "tests/integration" + +if (-not (Test-Path $DaslangExe)) { Write-Host "FAIL: daslang not found: $DaslangExe" -ForegroundColor Red; exit 2 } +if (-not (Test-Path $prepareScript)) { Write-Host "FAIL: missing $prepareScript (DasImguiRoot=$DasImguiRoot)" -ForegroundColor Red; exit 2 } +if (-not (Test-Path $scanDir)) { Write-Host "FAIL: no $scanDir" -ForegroundColor Red; exit 2 } + +$drivers = Get-ChildItem -Path $scanDir -Filter "record_*.das" | Sort-Object Name +Write-Host "[rerecord] repo: $RepoRoot" +Write-Host "[rerecord] daslang: $DaslangExe" +Write-Host "[rerecord] drivers: $($drivers.Count)" +Write-Host "[rerecord] modules: $($ModuleArgs -join ' ')" +Write-Host "[rerecord] assets: $(if ($AssetRoot) { $AssetRoot } else { '(dasImgui default)' })" + +$skipSet = @{} +if ($Skip) { foreach ($s in $Skip.Split(",")) { $k = $s.Trim(); if ($k) { $skipSet[$k] = $true } } } +$skipUntil = $From -ne "" + +function Sel($name) { + if ($Only -ne "" -and $name -ne $Only) { return $false } + if ($script:skipUntil) { if ($name -eq $From) { $script:skipUntil = $false } else { return $false } } + if ($skipSet.ContainsKey($name)) { return $false } + return $true +} + +# ---- Clean ---- +if (-not $SkipClean) { + Write-Host "`n==== CLEAN ====" -ForegroundColor Cyan + if ($DryRun) { + Write-Host " [dry-run] would remove *.apng *.mp4 *_music.wav *.mp4.ffmpeg.txt and voiceover/ under $tutDir" + } else { + Get-ChildItem -Path (Join-Path $tutDir '*') -Include *.apng,*.mp4,*_music.wav,*.mp4.ffmpeg.txt -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue + if (Test-Path $voDir) { Remove-Item -Recurse -Force $voDir -ErrorAction SilentlyContinue } + Write-Host " cleaned $tutDir" + } +} + +$fail = @() + +# ---- Prepare ---- +if (-not $SkipPrepare) { + Write-Host "`n==== PREPARE (TTS) ====" -ForegroundColor Cyan + foreach ($d in $drivers) { + $name = $d.BaseName -replace "^record_", "" + if (-not (Sel $name)) { continue } + $pargs = @($ModuleArgs) + @($prepareScript, "--", "--driver", $d.FullName, "--voice", $Voice) + if ($AssetRoot) { $pargs += @("--asset-root", $AssetRoot) } + if ($DryRun) { Write-Host " [dry-run] $DaslangExe $($pargs -join ' ')"; continue } + Write-Host " prepare $name" -ForegroundColor DarkCyan + & $DaslangExe @pargs + if ($LASTEXITCODE -ne 0) { $fail += "prepare:$name"; Write-Host " FAIL prepare $name" -ForegroundColor Red; if ($StopOnFail) { break } } + } +} + +# ---- Record ---- +if (-not $SkipRecord -and $fail.Count -eq 0) { + Write-Host "`n==== RECORD ====" -ForegroundColor Cyan + foreach ($d in $drivers) { + $name = $d.BaseName -replace "^record_", "" + if (-not (Sel $name)) { continue } + $rargs = @($ModuleArgs) + @($d.FullName) + if ($DryRun) { Write-Host " [dry-run] $DaslangExe $($rargs -join ' ')"; continue } + Write-Host " record $name" -ForegroundColor DarkCyan + $t0 = Get-Date + & $DaslangExe @rargs + $sec = [int]((Get-Date) - $t0).TotalSeconds + if ($LASTEXITCODE -ne 0) { $fail += "record:$name"; Write-Host " FAIL record $name (${sec}s)" -ForegroundColor Red; if ($StopOnFail) { break } } + else { Write-Host " ok (${sec}s)" -ForegroundColor Green } + } +} + +# ---- Convert ---- +if (-not $SkipConvert) { + Write-Host "`n==== CONVERT ====" -ForegroundColor Cyan + $apngs = Get-ChildItem -Path $tutDir -Filter "*.apng" -File -ErrorAction SilentlyContinue | Sort-Object Name + Write-Host " $($apngs.Count) apng(s) to convert" + foreach ($a in $apngs) { + if (-not (Sel $a.BaseName)) { continue } + if ($DryRun) { Write-Host " [dry-run] convert $($a.Name)"; continue } + Write-Host " convert $($a.BaseName)" -ForegroundColor DarkCyan + & $DaslangExe "-load_module" $DasImguiRoot $convertScript "--" "--apng" $a.FullName "--das_root" $DasRoot + if ($LASTEXITCODE -ne 0) { $fail += "convert:$($a.BaseName)"; Write-Host " FAIL convert $($a.BaseName)" -ForegroundColor Red; if ($StopOnFail) { break } } + } +} + +# ---- Summary ---- +Write-Host "`n==== SUMMARY ($RepoRoot) ====" -ForegroundColor Cyan +$mp4s = @(Get-ChildItem -Path $tutDir -Filter "*.mp4" -File -ErrorAction SilentlyContinue) +Write-Host " mp4s produced: $($mp4s.Count)" +if ($fail.Count -gt 0) { + Write-Host " FAILURES ($($fail.Count)):" -ForegroundColor Red + foreach ($f in $fail) { Write-Host " $f" -ForegroundColor Red } + exit 1 +} +Write-Host " all phases clean" -ForegroundColor Green +exit 0