Skip to content

Commit

Permalink
Add JS timeline control and callback APIs (#8673)
Browse files Browse the repository at this point in the history
  • Loading branch information
jprochazk authored Jan 15, 2025
1 parent 3ffdb37 commit d0a7d1f
Show file tree
Hide file tree
Showing 9 changed files with 701 additions and 54 deletions.
6 changes: 1 addition & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
// Don't forget to put it in a comment again before committing.
// "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
// "rust-analyzer.cargo.cfgs": {
// "web": null,
// "webgl": null,
// "webgpu": null,
// },
// "rust-analyzer.cargo.cfgs": ["web","webgl","webgpu"],

"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", // Use cmake-tools to grab configs.
"C_Cpp.autoAddFileAssociations": false,
Expand Down
63 changes: 61 additions & 2 deletions crates/viewer/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ pub struct StartupOptions {
/// This also can be changed in the viewer's option menu.
pub video_decoder_hw_acceleration: Option<re_video::decode::DecodeHardwareAcceleration>,

/// Interaction between JS and timeline.
///
/// This field isn't used directly, but is propagated to all recording configs
/// when they are created.
#[cfg(target_arch = "wasm32")]
pub timeline_options: Option<crate::web::TimelineOptions>,

/// Fullscreen is handled by JS on web.
///
/// This holds some callbacks which we use to communicate
Expand Down Expand Up @@ -131,6 +138,9 @@ impl Default for StartupOptions {
force_wgpu_backend: None,
video_decoder_hw_acceleration: None,

#[cfg(target_arch = "wasm32")]
timeline_options: Default::default(),

#[cfg(target_arch = "wasm32")]
fullscreen_options: Default::default(),

Expand Down Expand Up @@ -220,6 +230,12 @@ pub struct App {
pub(crate) panel_state_overrides: PanelStateOverrides,

reflection: re_types_core::reflection::Reflection,

/// Interaction between JS and timeline.
///
/// This field isn't used directly, but is propagated to all recording configs
/// when they are created.
pub timeline_callbacks: Option<re_viewer_context::TimelineCallbacks>,
}

impl App {
Expand Down Expand Up @@ -325,6 +341,46 @@ impl App {
Default::default()
});

#[cfg(target_arch = "wasm32")]
let timeline_callbacks = {
use crate::web_tools::string_from_js_value;
use std::rc::Rc;
use wasm_bindgen::JsValue;

startup_options.timeline_options.clone().map(|opts| {
re_viewer_context::TimelineCallbacks {
on_timelinechange: Rc::new(move |timeline, time| {
if let Err(err) = opts.on_timelinechange.call2(
&JsValue::from_str(timeline.name().as_str()),
&JsValue::from_f64(time.as_f64()),
) {
re_log::error!("{}", string_from_js_value(err));
};
}),
on_timeupdate: Rc::new(move |time| {
if let Err(err) =
opts.on_timeupdate.call1(&JsValue::from_f64(time.as_f64()))
{
re_log::error!("{}", string_from_js_value(err));
}
}),
on_play: Rc::new(move || {
if let Err(err) = opts.on_play.call0() {
re_log::error!("{}", string_from_js_value(err));
}
}),
on_pause: Rc::new(move || {
if let Err(err) = opts.on_pause.call0() {
re_log::error!("{}", string_from_js_value(err));
}
}),
}
})
};

#[cfg(not(target_arch = "wasm32"))]
let timeline_callbacks = None;

Self {
main_thread_token,
build_info,
Expand Down Expand Up @@ -374,6 +430,8 @@ impl App {
panel_state_overrides,

reflection,

timeline_callbacks,
}
}

Expand Down Expand Up @@ -1126,6 +1184,7 @@ impl App {
opacity: self.welcome_screen_opacity(egui_ctx),
},
is_history_enabled,
self.timeline_callbacks.as_ref(),
);
render_ctx.before_submit();
}
Expand Down Expand Up @@ -1573,7 +1632,7 @@ impl App {
{
if let Some(options) = &self.startup_options.fullscreen_options {
// Tell JS to toggle fullscreen.
if let Err(err) = options.on_toggle.call() {
if let Err(err) = options.on_toggle.call0() {
re_log::error!("{}", crate::web_tools::string_from_js_value(err));
};
}
Expand All @@ -1589,7 +1648,7 @@ impl App {
pub(crate) fn is_fullscreen_mode(&self) -> bool {
if let Some(options) = &self.startup_options.fullscreen_options {
// Ask JS if fullscreen is on or not.
match options.get_state.call() {
match options.get_state.call0() {
Ok(v) => return v.is_truthy(),
Err(err) => re_log::error_once!("{}", crate::web_tools::string_from_js_value(err)),
}
Expand Down
17 changes: 15 additions & 2 deletions crates/viewer/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ impl AppState {
command_sender: &CommandSender,
welcome_screen_state: &WelcomeScreenState,
is_history_enabled: bool,
timeline_callbacks: Option<&re_viewer_context::TimelineCallbacks>,
) {
re_tracing::profile_function!();

Expand Down Expand Up @@ -291,7 +292,7 @@ impl AppState {

// We move the time at the very start of the frame,
// so that we always show the latest data when we're in "follow" mode.
move_time(&ctx, recording, rx);
move_time(&ctx, recording, rx, timeline_callbacks);

// Update the viewport. May spawn new views and handle queued requests (like screenshots).
viewport_ui.on_frame_start(&ctx);
Expand Down Expand Up @@ -546,6 +547,11 @@ impl AppState {
*focused_item = None;
}

#[cfg(target_arch = "wasm32")] // Only used in Wasm
pub fn recording_config(&self, rec_id: &StoreId) -> Option<&RecordingConfig> {
self.recording_configs.get(rec_id)
}

pub fn recording_config_mut(&mut self, rec_id: &StoreId) -> Option<&mut RecordingConfig> {
self.recording_configs.get_mut(rec_id)
}
Expand Down Expand Up @@ -584,7 +590,12 @@ impl AppState {
}
}

fn move_time(ctx: &ViewerContext<'_>, recording: &EntityDb, rx: &ReceiveSet<LogMsg>) {
fn move_time(
ctx: &ViewerContext<'_>,
recording: &EntityDb,
rx: &ReceiveSet<LogMsg>,
timeline_callbacks: Option<&re_viewer_context::TimelineCallbacks>,
) {
let dt = ctx.egui_ctx.input(|i| i.stable_dt);

// Are we still connected to the data source for the current store?
Expand All @@ -598,13 +609,15 @@ fn move_time(ctx: &ViewerContext<'_>, recording: &EntityDb, rx: &ReceiveSet<LogM
recording.times_per_timeline(),
dt,
more_data_is_coming,
timeline_callbacks,
);

let blueprint_needs_repaint = if ctx.app_options.inspect_blueprint_timeline {
ctx.blueprint_cfg.time_ctrl.write().update(
ctx.store_context.blueprint.times_per_timeline(),
dt,
more_data_is_coming,
None,
)
} else {
re_viewer_context::NeedsRepaint::No
Expand Down
Loading

0 comments on commit d0a7d1f

Please sign in to comment.