Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save blueprint to file #5491

Merged
merged 5 commits into from
Mar 13, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/re_space_view/src/space_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ mod tests {
// No overrides set. Everybody has default values.
{
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down Expand Up @@ -595,6 +596,7 @@ mod tests {
// Parent is not visible, but children are
{
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down Expand Up @@ -643,6 +645,7 @@ mod tests {
// Nobody is visible
{
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down Expand Up @@ -673,6 +676,7 @@ mod tests {
{
let root = space_view.root_data_result(
&StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand All @@ -693,6 +697,7 @@ mod tests {
// Everyone has visible history
{
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down Expand Up @@ -734,6 +739,7 @@ mod tests {
// Child2 has its own visible history
{
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down Expand Up @@ -1019,6 +1025,7 @@ mod tests {

// Set up a store query and update the overrides.
let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down
1 change: 1 addition & 0 deletions crates/re_space_view/src/space_view_contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ mod tests {
});

let ctx = StoreContext {
app_id: re_log_types::ApplicationId::unknown(),
blueprint: &blueprint,
recording: Some(&recording),
all_recordings: vec![],
Expand Down
4 changes: 4 additions & 0 deletions crates/re_ui/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub enum UICommand {
Open,
SaveRecording,
SaveRecordingSelection,
SaveBlueprint,
CloseCurrentRecording,
#[cfg(not(target_arch = "wasm32"))]
Quit,
Expand Down Expand Up @@ -100,6 +101,8 @@ impl UICommand {
"Save data for the current loop selection to a Rerun data file (.rrd)",
),

Self::SaveBlueprint => ("Save blueprint…", "Save the current viewer setup as a Rerun blueprint file (.blueprint)"),

Self::Open => ("Open…", "Open any supported files (.rrd, images, meshes, …)"),

Self::CloseCurrentRecording => (
Expand Down Expand Up @@ -245,6 +248,7 @@ impl UICommand {
match self {
Self::SaveRecording => Some(cmd(Key::S)),
Self::SaveRecordingSelection => Some(cmd_alt(Key::S)),
Self::SaveBlueprint => None,
Self::Open => Some(cmd(Key::O)),
Self::CloseCurrentRecording => None,

Expand Down
64 changes: 50 additions & 14 deletions crates/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,16 +443,20 @@ impl App {
) {
match cmd {
UICommand::SaveRecording => {
save(self, store_context, None);
save_recording(self, store_context, None);
}
UICommand::SaveRecordingSelection => {
save(
save_recording(
self,
store_context,
self.state.loop_selection(store_context),
);
}

UICommand::SaveBlueprint => {
save_blueprint(self, store_context);
}

#[cfg(not(target_arch = "wasm32"))]
UICommand::Open => {
for file_path in open_file_dialog_native() {
Expand Down Expand Up @@ -1500,17 +1504,14 @@ async fn async_open_rrd_dialog() -> Vec<re_data_source::FileContents> {
file_contents
}

#[allow(clippy::needless_pass_by_ref_mut)]
fn save(
#[allow(unused_variables)] app: &mut App, // only used on native
fn save_recording(
app: &mut App,
store_context: Option<&StoreContext<'_>>,
loop_selection: Option<(re_entity_db::Timeline, re_log_types::TimeRangeF)>,
) {
re_tracing::profile_function!();

let Some(entity_db) = store_context.as_ref().and_then(|view| view.recording) else {
// NOTE: Can only happen if saving through the command palette.
re_log::error!("No data to save!");
re_log::error!("No recording data to save");
return;
};

Expand All @@ -1519,9 +1520,44 @@ fn save(
let title = if loop_selection.is_some() {
"Save loop selection"
} else {
"Save"
"Save recording"
};

save_entity_db(
app,
file_name.to_owned(),
title.to_owned(),
entity_db,
loop_selection,
);
}

fn save_blueprint(app: &mut App, store_context: Option<&StoreContext<'_>>) {
let Some(store_context) = store_context else {
re_log::error!("No blueprint to save");
return;
};

let entity_db = store_context.blueprint;

let file_name = format!(
"{}.blueprint",
crate::saving::sanitize_app_id(&store_context.app_id)
);
let title = "Save blueprint";
save_entity_db(app, file_name, title.to_owned(), entity_db, None);
}

#[allow(clippy::needless_pass_by_ref_mut)] // `app` is only used on native
fn save_entity_db(
#[allow(unused_variables)] app: &mut App, // only used on native
file_name: String,
title: String,
entity_db: &EntityDb,
loop_selection: Option<(re_log_types::Timeline, re_log_types::TimeRangeF)>,
) {
re_tracing::profile_function!();

// Web
#[cfg(target_arch = "wasm32")]
{
Expand All @@ -1534,7 +1570,7 @@ fn save(
};

wasm_bindgen_futures::spawn_local(async move {
if let Err(err) = async_save_dialog(file_name, title, &messages).await {
if let Err(err) = async_save_dialog(&file_name, &title, &messages).await {
re_log::error!("File saving failed: {err}");
}
});
Expand All @@ -1558,10 +1594,10 @@ fn save(
return;
}
};
if let Err(err) = app
.background_tasks
.spawn_file_saver(move || crate::saving::encode_to_file(&path, messages.iter()))
{
if let Err(err) = app.background_tasks.spawn_file_saver(move || {
crate::saving::encode_to_file(&path, messages.iter())?;
Ok(path)
}) {
// NOTE: Can only happen if saving through the command palette.
re_log::error!("File saving failed: {err}");
}
Expand Down
5 changes: 2 additions & 3 deletions crates/re_viewer/src/background_tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ impl BackgroundTasks {
}

#[cfg(not(target_arch = "wasm32"))]
pub fn spawn_file_saver<F, T>(&mut self, f: F) -> anyhow::Result<()>
pub fn spawn_file_saver<F>(&mut self, f: F) -> anyhow::Result<()>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
F: FnOnce() -> anyhow::Result<PathBuf> + Send + 'static,
{
self.spawn_threaded_promise(FILE_SAVER_PROMISE, f)
}
Expand Down
6 changes: 2 additions & 4 deletions crates/re_viewer/src/saving.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
#[cfg(not(target_arch = "wasm32"))]
use re_log_types::ApplicationId;

#[cfg(not(target_arch = "wasm32"))]
/// Convert to lowercase and replace any character that is not a fairly common
/// filename character with '-'
fn sanitize_app_id(app_id: &ApplicationId) -> String {
pub fn sanitize_app_id(app_id: &ApplicationId) -> String {
let output = app_id.0.to_lowercase();
output.replace(
|c: char| !matches!(c, '0'..='9' | 'a'..='z' | '.' | '_' | '+' | '(' | ')' | '[' | ']'),
"-",
)
}

#[cfg(not(target_arch = "wasm32"))]
/// Determine the default path for a blueprint based on its `ApplicationId`
/// This path should be deterministic and unique.
// TODO(#2579): Implement equivalent for web
#[cfg(not(target_arch = "wasm32"))]
pub fn default_blueprint_path(app_id: &ApplicationId) -> anyhow::Result<std::path::PathBuf> {
use anyhow::Context;

Expand Down
45 changes: 20 additions & 25 deletions crates/re_viewer/src/store_hub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,28 @@ impl StoreHub {
/// matching [`ApplicationId`].
pub fn read_context(&mut self) -> Option<StoreContext<'_>> {
// If we have an app-id, then use it to look up the blueprint.
let blueprint_id = self.selected_application_id.as_ref().map(|app_id| {
self.blueprint_by_app_id
.entry(app_id.clone())
.or_insert_with(|| StoreId::from_string(StoreKind::Blueprint, app_id.clone().0))
});
let app_id = self.selected_application_id.clone()?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much cleaner 😍 !


let blueprint_id = self
.blueprint_by_app_id
.entry(app_id.clone())
.or_insert_with(|| StoreId::from_string(StoreKind::Blueprint, app_id.clone().0));

// As long as we have a blueprint-id, create the blueprint.
blueprint_id
// Get or create the blueprint:
self.store_bundle.blueprint_entry(blueprint_id);
let blueprint = self.store_bundle.blueprint(blueprint_id)?;

let recording = self
.selected_rec_id
.as_ref()
.map(|id| self.store_bundle.blueprint_entry(id));

// If we have a blueprint, we can return the `StoreContext`. In most
// cases it should have already existed or been created above.
blueprint_id
.and_then(|id| self.store_bundle.blueprint(id))
.map(|blueprint| {
let recording = self
.selected_rec_id
.as_ref()
.and_then(|id| self.store_bundle.recording(id));

StoreContext {
blueprint,
recording,
all_recordings: self.store_bundle.recordings().collect_vec(),
}
})
.and_then(|id| self.store_bundle.recording(id));

Some(StoreContext {
app_id,
blueprint,
recording,
all_recordings: self.store_bundle.recordings().collect_vec(),
})
}

/// Keeps track if a recording was ever activated.
Expand Down
8 changes: 5 additions & 3 deletions crates/re_viewer/src/ui/rerun_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ impl App {

self.save_buttons_ui(ui, _store_context);

UICommand::SaveBlueprint.menu_button_ui(ui, &self.command_sender);

UICommand::CloseCurrentRecording.menu_button_ui(ui, &self.command_sender);

ui.add_space(SPACING);
Expand Down Expand Up @@ -165,13 +167,13 @@ impl App {

let file_save_in_progress = self.background_tasks.is_file_save_in_progress();

let save_button = UICommand::SaveRecording.menu_button(ui.ctx());
let save_recording_button = UICommand::SaveRecording.menu_button(ui.ctx());
let save_selection_button = UICommand::SaveRecordingSelection.menu_button(ui.ctx());

if file_save_in_progress {
ui.add_enabled_ui(false, |ui| {
ui.horizontal(|ui| {
ui.add(save_button);
ui.add(save_recording_button);
ui.spinner();
});
ui.horizontal(|ui| {
Expand All @@ -185,7 +187,7 @@ impl App {
.map_or(false, |recording| !recording.is_empty());
ui.add_enabled_ui(entity_db_is_nonempty, |ui| {
if ui
.add(save_button)
.add(save_recording_button)
.on_hover_text("Save all data to a Rerun data file (.rrd)")
.clicked()
{
Expand Down
2 changes: 2 additions & 0 deletions crates/re_viewer_context/src/store_context.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use re_entity_db::EntityDb;
use re_log_types::ApplicationId;

/// The current Blueprint and Recording being displayed by the viewer
pub struct StoreContext<'a> {
pub app_id: ApplicationId,
pub blueprint: &'a EntityDb,
pub recording: Option<&'a EntityDb>,
pub all_recordings: Vec<&'a EntityDb>,
Expand Down