diff --git a/crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs b/crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs index d000a99786af..26e6a3cd9b64 100644 --- a/crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs +++ b/crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs @@ -11,8 +11,27 @@ table PanelBlueprint ( // --- Optional --- - /// Current state of the panels. + /// Current state of the panel. state: rerun.blueprint.components.PanelState ("attr.rerun.component_optional", nullable, order: 1000); // TODO(jleibs): Add a float to track how expanded the panel is. } + +/// Time panel specific state. +table TimePanelBlueprint ( + "attr.rerun.scope": "blueprint", + "attr.rust.derive": "Default" +) { + // --- Required --- + + // --- Optional --- + + /// Current state of the panel. + state: rerun.blueprint.components.PanelState ("attr.rerun.component_optional", nullable, order: 1000); + + /// What timeline the panel is on. + timeline: rerun.blueprint.components.TimelineName ("attr.rerun.component_optional", nullable, order: 2000); + + /// What time the time cursor should be on. + time: rerun.blueprint.components.TimeCell ("attr.rerun.component_optional", nullable, order: 2100); +} diff --git a/crates/store/re_types/definitions/rerun/blueprint/components.fbs b/crates/store/re_types/definitions/rerun/blueprint/components.fbs index f1580b52ca2f..04f30c6d8f36 100644 --- a/crates/store/re_types/definitions/rerun/blueprint/components.fbs +++ b/crates/store/re_types/definitions/rerun/blueprint/components.fbs @@ -29,6 +29,7 @@ include "./components/root_container.fbs"; include "./components/row_share.fbs"; include "./components/selected_columns.fbs"; include "./components/tensor_dimension_index_slider.fbs"; +include "./components/time_cell.fbs"; include "./components/timeline_name.fbs"; include "./components/view_class.fbs"; include "./components/view_fit.fbs"; diff --git a/crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs b/crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs new file mode 100644 index 000000000000..b86ebc5270af --- /dev/null +++ b/crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs @@ -0,0 +1,15 @@ +namespace rerun.blueprint.components; + +// --- + +/// A reference to a time. +struct TimeCell ( + "attr.arrow.transparent", + "attr.rerun.scope": "blueprint", + "attr.python.aliases": "long", + "attr.rust.derive": "Copy, PartialEq, Eq, PartialOrd, Ord", + "attr.rust.repr": "transparent", + "attr.rust.tuple_struct" +) { + time: rerun.datatypes.TimeInt (order: 100); +} diff --git a/crates/store/re_types/src/blueprint/archetypes/.gitattributes b/crates/store/re_types/src/blueprint/archetypes/.gitattributes index ab61785b69b8..5effc5a6eb25 100644 --- a/crates/store/re_types/src/blueprint/archetypes/.gitattributes +++ b/crates/store/re_types/src/blueprint/archetypes/.gitattributes @@ -23,6 +23,7 @@ tensor_scalar_mapping.rs linguist-generated=true tensor_slice_selection.rs linguist-generated=true tensor_view_fit.rs linguist-generated=true time_axis.rs linguist-generated=true +time_panel_blueprint.rs linguist-generated=true view_blueprint.rs linguist-generated=true view_contents.rs linguist-generated=true viewport_blueprint.rs linguist-generated=true diff --git a/crates/store/re_types/src/blueprint/archetypes/mod.rs b/crates/store/re_types/src/blueprint/archetypes/mod.rs index fce171bf4a52..098c5e47702b 100644 --- a/crates/store/re_types/src/blueprint/archetypes/mod.rs +++ b/crates/store/re_types/src/blueprint/archetypes/mod.rs @@ -21,6 +21,7 @@ mod tensor_scalar_mapping; mod tensor_slice_selection; mod tensor_view_fit; mod time_axis; +mod time_panel_blueprint; mod view_blueprint; mod view_contents; mod viewport_blueprint; @@ -49,6 +50,7 @@ pub use self::tensor_scalar_mapping::TensorScalarMapping; pub use self::tensor_slice_selection::TensorSliceSelection; pub use self::tensor_view_fit::TensorViewFit; pub use self::time_axis::TimeAxis; +pub use self::time_panel_blueprint::TimePanelBlueprint; pub use self::view_blueprint::ViewBlueprint; pub use self::view_contents::ViewContents; pub use self::viewport_blueprint::ViewportBlueprint; diff --git a/crates/store/re_types/src/blueprint/archetypes/panel_blueprint.rs b/crates/store/re_types/src/blueprint/archetypes/panel_blueprint.rs index ab36ea7d4b4a..da3b5c36fa6a 100644 --- a/crates/store/re_types/src/blueprint/archetypes/panel_blueprint.rs +++ b/crates/store/re_types/src/blueprint/archetypes/panel_blueprint.rs @@ -24,7 +24,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** #[derive(Clone, Debug, Default)] pub struct PanelBlueprint { - /// Current state of the panels. + /// Current state of the panel. pub state: Option, } @@ -139,7 +139,7 @@ impl PanelBlueprint { } } - /// Current state of the panels. + /// Current state of the panel. #[inline] pub fn with_state( mut self, diff --git a/crates/store/re_types/src/blueprint/archetypes/time_panel_blueprint.rs b/crates/store/re_types/src/blueprint/archetypes/time_panel_blueprint.rs new file mode 100644 index 000000000000..98c331bdb2e9 --- /dev/null +++ b/crates/store/re_types/src/blueprint/archetypes/time_panel_blueprint.rs @@ -0,0 +1,242 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: Time panel specific state. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Default)] +pub struct TimePanelBlueprint { + /// Current state of the panel. + pub state: Option, + + /// What timeline the panel is on. + pub timeline: Option, + + /// What time the time cursor should be on. + pub time: Option, +} + +impl TimePanelBlueprint { + /// Returns the [`ComponentDescriptor`] for [`Self::state`]. + /// + /// The corresponding component is [`crate::blueprint::components::PanelState`]. + #[inline] + pub fn descriptor_state() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:state".into(), + component_type: Some("rerun.blueprint.components.PanelState".into()), + } + } + + /// Returns the [`ComponentDescriptor`] for [`Self::timeline`]. + /// + /// The corresponding component is [`crate::blueprint::components::TimelineName`]. + #[inline] + pub fn descriptor_timeline() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:timeline".into(), + component_type: Some("rerun.blueprint.components.TimelineName".into()), + } + } + + /// Returns the [`ComponentDescriptor`] for [`Self::time`]. + /// + /// The corresponding component is [`crate::blueprint::components::TimeCell`]. + #[inline] + pub fn descriptor_time() -> ComponentDescriptor { + ComponentDescriptor { + archetype: Some("rerun.blueprint.archetypes.TimePanelBlueprint".into()), + component: "TimePanelBlueprint:time".into(), + component_type: Some("rerun.blueprint.components.TimeCell".into()), + } + } +} + +static REQUIRED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static RECOMMENDED_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 0usize]> = + std::sync::LazyLock::new(|| []); + +static OPTIONAL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 3usize]> = + std::sync::LazyLock::new(|| { + [ + TimePanelBlueprint::descriptor_state(), + TimePanelBlueprint::descriptor_timeline(), + TimePanelBlueprint::descriptor_time(), + ] + }); + +static ALL_COMPONENTS: std::sync::LazyLock<[ComponentDescriptor; 3usize]> = + std::sync::LazyLock::new(|| { + [ + TimePanelBlueprint::descriptor_state(), + TimePanelBlueprint::descriptor_timeline(), + TimePanelBlueprint::descriptor_time(), + ] + }); + +impl TimePanelBlueprint { + /// The total number of components in the archetype: 0 required, 0 recommended, 3 optional + pub const NUM_COMPONENTS: usize = 3usize; +} + +impl ::re_types_core::Archetype for TimePanelBlueprint { + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + "rerun.blueprint.archetypes.TimePanelBlueprint".into() + } + + #[inline] + fn display_name() -> &'static str { + "Time panel blueprint" + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentDescriptor]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow_components( + arrow_data: impl IntoIterator, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_descr: ::nohash_hasher::IntMap<_, _> = arrow_data.into_iter().collect(); + let state = arrays_by_descr + .get(&Self::descriptor_state()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_state())); + let timeline = arrays_by_descr + .get(&Self::descriptor_timeline()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_timeline())); + let time = arrays_by_descr + .get(&Self::descriptor_time()) + .map(|array| SerializedComponentBatch::new(array.clone(), Self::descriptor_time())); + Ok(Self { + state, + timeline, + time, + }) + } +} + +impl ::re_types_core::AsComponents for TimePanelBlueprint { + #[inline] + fn as_serialized_batches(&self) -> Vec { + use ::re_types_core::Archetype as _; + [self.state.clone(), self.timeline.clone(), self.time.clone()] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for TimePanelBlueprint {} + +impl TimePanelBlueprint { + /// Create a new `TimePanelBlueprint`. + #[inline] + pub fn new() -> Self { + Self { + state: None, + timeline: None, + time: None, + } + } + + /// Update only some specific fields of a `TimePanelBlueprint`. + #[inline] + pub fn update_fields() -> Self { + Self::default() + } + + /// Clear all the fields of a `TimePanelBlueprint`. + #[inline] + pub fn clear_fields() -> Self { + use ::re_types_core::Loggable as _; + Self { + state: Some(SerializedComponentBatch::new( + crate::blueprint::components::PanelState::arrow_empty(), + Self::descriptor_state(), + )), + timeline: Some(SerializedComponentBatch::new( + crate::blueprint::components::TimelineName::arrow_empty(), + Self::descriptor_timeline(), + )), + time: Some(SerializedComponentBatch::new( + crate::blueprint::components::TimeCell::arrow_empty(), + Self::descriptor_time(), + )), + } + } + + /// Current state of the panel. + #[inline] + pub fn with_state( + mut self, + state: impl Into, + ) -> Self { + self.state = try_serialize_field(Self::descriptor_state(), [state]); + self + } + + /// What timeline the panel is on. + #[inline] + pub fn with_timeline( + mut self, + timeline: impl Into, + ) -> Self { + self.timeline = try_serialize_field(Self::descriptor_timeline(), [timeline]); + self + } + + /// What time the time cursor should be on. + #[inline] + pub fn with_time(mut self, time: impl Into) -> Self { + self.time = try_serialize_field(Self::descriptor_time(), [time]); + self + } +} + +impl ::re_byte_size::SizeBytes for TimePanelBlueprint { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.state.heap_size_bytes() + self.timeline.heap_size_bytes() + self.time.heap_size_bytes() + } +} diff --git a/crates/store/re_types/src/blueprint/components/.gitattributes b/crates/store/re_types/src/blueprint/components/.gitattributes index 5a6ea5228162..73f3c6f9959c 100644 --- a/crates/store/re_types/src/blueprint/components/.gitattributes +++ b/crates/store/re_types/src/blueprint/components/.gitattributes @@ -31,6 +31,7 @@ root_container.rs linguist-generated=true row_share.rs linguist-generated=true selected_columns.rs linguist-generated=true tensor_dimension_index_slider.rs linguist-generated=true +time_cell.rs linguist-generated=true timeline_name.rs linguist-generated=true view_class.rs linguist-generated=true view_fit.rs linguist-generated=true diff --git a/crates/store/re_types/src/blueprint/components/mod.rs b/crates/store/re_types/src/blueprint/components/mod.rs index 5c7cdc14b556..607be00704b5 100644 --- a/crates/store/re_types/src/blueprint/components/mod.rs +++ b/crates/store/re_types/src/blueprint/components/mod.rs @@ -39,6 +39,7 @@ mod row_share; mod selected_columns; mod tensor_dimension_index_slider; mod tensor_dimension_index_slider_ext; +mod time_cell; mod timeline_name; mod timeline_name_ext; mod view_class; @@ -84,6 +85,7 @@ pub use self::root_container::RootContainer; pub use self::row_share::RowShare; pub use self::selected_columns::SelectedColumns; pub use self::tensor_dimension_index_slider::TensorDimensionIndexSlider; +pub use self::time_cell::TimeCell; pub use self::timeline_name::TimelineName; pub use self::view_class::ViewClass; pub use self::view_fit::ViewFit; diff --git a/crates/store/re_types/src/blueprint/components/time_cell.rs b/crates/store/re_types/src/blueprint/components/time_cell.rs new file mode 100644 index 000000000000..b0b4c99a7c6b --- /dev/null +++ b/crates/store/re_types/src/blueprint/components/time_cell.rs @@ -0,0 +1,116 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs". + +#![allow(unused_braces)] +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::SerializationResult; +use ::re_types_core::try_serialize_field; +use ::re_types_core::{ComponentBatch as _, SerializedComponentBatch}; +use ::re_types_core::{ComponentDescriptor, ComponentType}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: A reference to a time. +/// +/// ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** +#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct TimeCell(pub crate::datatypes::TimeInt); + +impl ::re_types_core::Component for TimeCell { + #[inline] + fn name() -> ComponentType { + "rerun.blueprint.components.TimeCell".into() + } +} + +::re_types_core::macros::impl_into_cow!(TimeCell); + +impl ::re_types_core::Loggable for TimeCell { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + crate::datatypes::TimeInt::arrow_datatype() + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult + where + Self: Clone + 'a, + { + crate::datatypes::TimeInt::to_arrow_opt(data.into_iter().map(|datum| { + datum.map(|datum| match datum.into() { + ::std::borrow::Cow::Borrowed(datum) => ::std::borrow::Cow::Borrowed(&datum.0), + ::std::borrow::Cow::Owned(datum) => ::std::borrow::Cow::Owned(datum.0), + }) + })) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + crate::datatypes::TimeInt::from_arrow_opt(arrow_data) + .map(|v| v.into_iter().map(|v| v.map(Self)).collect()) + } + + #[inline] + fn from_arrow(arrow_data: &dyn arrow::array::Array) -> DeserializationResult> + where + Self: Sized, + { + crate::datatypes::TimeInt::from_arrow(arrow_data).map(|v| v.into_iter().map(Self).collect()) + } +} + +impl> From for TimeCell { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for TimeCell { + #[inline] + fn borrow(&self) -> &crate::datatypes::TimeInt { + &self.0 + } +} + +impl std::ops::Deref for TimeCell { + type Target = crate::datatypes::TimeInt; + + #[inline] + fn deref(&self) -> &crate::datatypes::TimeInt { + &self.0 + } +} + +impl std::ops::DerefMut for TimeCell { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::TimeInt { + &mut self.0 + } +} + +impl ::re_byte_size::SizeBytes for TimeCell { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} diff --git a/crates/store/re_types/src/reflection/mod.rs b/crates/store/re_types/src/reflection/mod.rs index 4236ad0efa7a..66bd603284ec 100644 --- a/crates/store/re_types/src/reflection/mod.rs +++ b/crates/store/re_types/src/reflection/mod.rs @@ -326,6 +326,16 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "A reference to a time.\n\n⚠\u{fe0f} **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.**", + deprecation_summary: None, + custom_placeholder: None, + datatype: TimeCell::arrow_datatype(), + verify_arrow_array: TimeCell::verify_arrow_array, + }, + ), ( ::name(), ComponentReflection { @@ -3615,7 +3625,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { name: "state", display_name: "State", component_type: "rerun.blueprint.components.PanelState".into(), - docstring_md: "Current state of the panels.", + docstring_md: "Current state of the panel.", is_required: false, }], }, @@ -3774,6 +3784,38 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { }], }, ), + ( + ArchetypeName::new("rerun.blueprint.archetypes.TimePanelBlueprint"), + ArchetypeReflection { + display_name: "Time panel blueprint", + deprecation_summary: None, + scope: Some("blueprint"), + view_types: &[], + fields: vec![ + ArchetypeFieldReflection { + name: "state", + display_name: "State", + component_type: "rerun.blueprint.components.PanelState".into(), + docstring_md: "Current state of the panel.", + is_required: false, + }, + ArchetypeFieldReflection { + name: "timeline", + display_name: "Timeline", + component_type: "rerun.blueprint.components.TimelineName".into(), + docstring_md: "What timeline the panel is on.", + is_required: false, + }, + ArchetypeFieldReflection { + name: "time", + display_name: "Time", + component_type: "rerun.blueprint.components.TimeCell".into(), + docstring_md: "What time the time cursor should be on.", + is_required: false, + }, + ], + }, + ), ( ArchetypeName::new("rerun.blueprint.archetypes.ViewBlueprint"), ArchetypeReflection { diff --git a/crates/viewer/re_blueprint_tree/tests/blueprint_tree_tests.rs b/crates/viewer/re_blueprint_tree/tests/blueprint_tree_tests.rs index 7b062c2ee838..a78dfae17e69 100644 --- a/crates/viewer/re_blueprint_tree/tests/blueprint_tree_tests.rs +++ b/crates/viewer/re_blueprint_tree/tests/blueprint_tree_tests.rs @@ -6,11 +6,13 @@ use egui_kittest::{OsThreshold, SnapshotOptions}; use re_blueprint_tree::BlueprintTree; use re_chunk_store::RowId; use re_chunk_store::external::re_chunk::ChunkBuilder; -use re_log_types::{Timeline, build_frame_nr}; +use re_log_types::{TimelineName, build_frame_nr}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_types::archetypes::Points3D; -use re_viewer_context::{CollapseScope, RecommendedView, ViewClass as _, ViewId}; +use re_viewer_context::{ + CollapseScope, RecommendedView, TimeBlueprintExt as _, ViewClass as _, ViewId, +}; use re_viewport_blueprint::{ViewBlueprint, ViewportBlueprint}; #[test] @@ -28,7 +30,7 @@ fn basic_blueprint_panel_should_match_snapshot() { }); let blueprint_tree = BlueprintTree::default(); - run_blueprint_panel_and_save_snapshot(test_context, blueprint_tree, "basic_blueprint_panel"); + run_blueprint_panel_and_save_snapshot(&test_context, blueprint_tree, "basic_blueprint_panel"); } // --- @@ -61,7 +63,9 @@ fn collapse_expand_all_blueprint_panel_should_match_snapshot() { let mut blueprint_tree = BlueprintTree::default(); // set the current timeline to the timeline where data was logged to - test_context.set_active_timeline(Timeline::new_sequence("frame_nr")); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(TimelineName::new("frame_nr")); + }); let mut harness = test_context .setup_kittest_for_rendering() @@ -103,7 +107,7 @@ fn blueprint_panel_filter_active_inside_origin_should_match_snapshot() { let (test_context, blueprint_tree) = setup_filter_test(Some("left")); run_blueprint_panel_and_save_snapshot( - test_context, + &test_context, blueprint_tree, "blueprint_panel_filter_active_inside_origin", ); @@ -114,7 +118,7 @@ fn blueprint_panel_filter_active_outside_origin_should_match_snapshot() { let (test_context, blueprint_tree) = setup_filter_test(Some("out")); run_blueprint_panel_and_save_snapshot( - test_context, + &test_context, blueprint_tree, "blueprint_panel_filter_active_outside_origin", ); @@ -125,7 +129,7 @@ fn blueprint_panel_filter_active_above_origin_should_match_snapshot() { let (test_context, blueprint_tree) = setup_filter_test(Some("path")); run_blueprint_panel_and_save_snapshot( - test_context, + &test_context, blueprint_tree, "blueprint_panel_filter_active_above_origin", ); @@ -182,12 +186,14 @@ fn add_point_to_chunk_builder(builder: ChunkBuilder) -> ChunkBuilder { } fn run_blueprint_panel_and_save_snapshot( - mut test_context: TestContext, + test_context: &TestContext, mut blueprint_tree: BlueprintTree, snapshot_name: &str, ) { // set the current timeline to the timeline where data was logged to - test_context.set_active_timeline(Timeline::new_sequence("frame_nr")); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(TimelineName::new("frame_nr")); + }); let mut harness = test_context .setup_kittest_for_rendering() diff --git a/crates/viewer/re_blueprint_tree/tests/range_selection_test.rs b/crates/viewer/re_blueprint_tree/tests/range_selection_test.rs index 25352cf19803..b4a48ea7caba 100644 --- a/crates/viewer/re_blueprint_tree/tests/range_selection_test.rs +++ b/crates/viewer/re_blueprint_tree/tests/range_selection_test.rs @@ -5,11 +5,11 @@ use egui_kittest::{OsThreshold, SnapshotOptions, kittest::Queryable as _}; use re_blueprint_tree::BlueprintTree; use re_chunk_store::RowId; use re_chunk_store::external::re_chunk::ChunkBuilder; -use re_log_types::{Timeline, build_frame_nr}; +use re_log_types::{TimelineName, build_frame_nr}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_types::archetypes::Points3D; -use re_viewer_context::{Contents, ViewClass as _, VisitorControlFlow}; +use re_viewer_context::{Contents, TimeBlueprintExt as _, ViewClass as _, VisitorControlFlow}; use re_viewport_blueprint::{ViewBlueprint, ViewportBlueprint}; #[test] @@ -29,7 +29,9 @@ fn test_range_selection_in_blueprint_tree() { let mut blueprint_tree = BlueprintTree::default(); // set the current timeline to the timeline where data was logged to - test_context.set_active_timeline(Timeline::new_sequence("frame_nr")); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(TimelineName::new("frame_nr")); + }); let mut harness = test_context .setup_kittest_for_rendering() diff --git a/crates/viewer/re_blueprint_tree/tests/view_structure_test.rs b/crates/viewer/re_blueprint_tree/tests/view_structure_test.rs index 930fbe7b5d06..ed0d6dca9ec7 100644 --- a/crates/viewer/re_blueprint_tree/tests/view_structure_test.rs +++ b/crates/viewer/re_blueprint_tree/tests/view_structure_test.rs @@ -11,12 +11,12 @@ use re_blueprint_tree::BlueprintTree; use re_blueprint_tree::data::BlueprintTreeData; use re_chunk_store::RowId; use re_chunk_store::external::re_chunk::ChunkBuilder; -use re_log_types::{EntityPath, Timeline, build_frame_nr}; +use re_log_types::{EntityPath, TimelineName, build_frame_nr}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_types::archetypes::Points3D; use re_ui::filter_widget::FilterState; -use re_viewer_context::{RecommendedView, ViewClass as _, ViewId}; +use re_viewer_context::{RecommendedView, TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewportBlueprint}; const VIEW_ID: &str = "this-is-a-view-id"; @@ -163,7 +163,7 @@ fn test_all_snapshot_test_cases() { } fn run_test_case(test_case: &TestCase, filter_query: Option<&str>) -> Result<(), SnapshotError> { - let mut test_context = test_context(test_case); + let test_context = test_context(test_case); let view_id = ViewId::hashed_from_str(VIEW_ID); let mut blueprint_tree = BlueprintTree::default(); @@ -183,7 +183,9 @@ fn run_test_case(test_case: &TestCase, filter_query: Option<&str>) -> Result<(), } // set the current timeline to the timeline where data was logged to - test_context.set_active_timeline(Timeline::new_sequence("frame_nr")); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(TimelineName::new("frame_nr")); + }); let mut harness = test_context .setup_kittest_for_rendering() diff --git a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/TimeCell_placeholder.png b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/TimeCell_placeholder.png new file mode 100644 index 000000000000..c819ee902683 --- /dev/null +++ b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_narrow/TimeCell_placeholder.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86a195c9871f7a7d6aaed4f3965e8f274b9a3380ec5bd1e4568c016c104b7425 +size 2737 diff --git a/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/TimeCell_placeholder.png b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/TimeCell_placeholder.png new file mode 100644 index 000000000000..453127fdc135 --- /dev/null +++ b/crates/viewer/re_component_ui/tests/snapshots/all_components_list_item_wide/TimeCell_placeholder.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da84247330e6d2ee20e3ff124a1609c5bec1e584c84832d09d2d7191f3fb1a29 +size 3052 diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 4114cfe8136e..f25ae6bce820 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -5,7 +5,7 @@ use re_entity_db::entity_db::EntityDbClass; use re_entity_db::{EntityTree, InstancePath}; use re_format::format_uint; -use re_log_types::{ApplicationId, EntityPath, TableId, TimeInt, TimeType, Timeline, TimelineName}; +use re_log_types::{ApplicationId, EntityPath, TableId, TimeInt, TimeType, TimelineName}; use re_types::{ archetypes::RecordingInfo, components::{Name, Timestamp}, @@ -14,7 +14,8 @@ use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{SyntaxHighlighting as _, UiExt as _, icons, list_item}; use re_viewer_context::open_url::ViewerOpenUrl; use re_viewer_context::{ - HoverHighlight, Item, SystemCommand, SystemCommandSender as _, UiLayout, ViewId, ViewerContext, + HoverHighlight, Item, SystemCommand, SystemCommandSender as _, TimeBlueprintExt as _, UiLayout, + ViewId, ViewerContext, }; use super::DataUi as _; @@ -499,11 +500,7 @@ pub fn time_button( typ.format(value, ctx.app_options().timestamp_format), ); if response.clicked() { - let timeline = Timeline::new(*timeline_name, typ); - ctx.rec_cfg - .time_ctrl - .write() - .set_timeline_and_time(timeline, value); + ctx.set_timeline_and_time(*timeline_name, value); ctx.rec_cfg.time_ctrl.write().pause(); } response @@ -529,9 +526,8 @@ pub fn timeline_button_to( .selectable_label(is_selected, text) .on_hover_text("Click to switch to this timeline"); if response.clicked() { + ctx.set_timeline(*timeline_name); let mut time_ctrl = ctx.rec_cfg.time_ctrl.write(); - let timeline = Timeline::new(*timeline_name, ctx.recording().timeline_type(timeline_name)); - time_ctrl.set_timeline(timeline); time_ctrl.pause(); } response diff --git a/crates/viewer/re_global_context/src/command_sender.rs b/crates/viewer/re_global_context/src/command_sender.rs index c9f796a2de33..69a92224c097 100644 --- a/crates/viewer/re_global_context/src/command_sender.rs +++ b/crates/viewer/re_global_context/src/command_sender.rs @@ -110,11 +110,8 @@ pub enum SystemCommand { /// Set the active timeline and time for the given recording. SetActiveTime { store_id: StoreId, - timeline: re_chunk::Timeline, - time: Option, - - /// If this is true the timeline will persist even if it is invalid at the moment. - pending: bool, + timeline: re_chunk::TimelineName, + time: Option, }, /// Set the loop selection for the given timeline. diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index dd9ab52a44af..10d77bbbe934 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -1098,7 +1098,10 @@ mod tests { TimeType, example_components::{MyPoint, MyPoints}, }; - use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; + use re_test_context::{ + TestContext, + external::egui_kittest::{SnapshotOptions, kittest::Queryable as _}, + }; use re_test_viewport::{TestContextExt as _, TestView}; use re_types::archetypes; use re_viewer_context::{RecommendedView, ViewClass as _, blueprint_timeline}; @@ -1182,10 +1185,7 @@ mod tests { test_context.handle_system_commands(); }); - let raw_input = harness.input_mut(); - raw_input - .events - .push(egui::Event::PointerMoved(egui::Pos2 { x: 120.0, y: 80.0 })); + harness.get_by_label("test_app").hover(); harness.run(); diff --git a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs index 348fb3e22e00..67dc229efe7e 100644 --- a/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs +++ b/crates/viewer/re_selection_panel/src/visible_time_range_ui.rs @@ -9,7 +9,7 @@ use re_types::{ }; use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{TimeDragValue, UiExt as _}; -use re_viewer_context::{QueryRange, ViewClass, ViewState, ViewerContext}; +use re_viewer_context::{BlueprintContext as _, QueryRange, ViewClass, ViewState, ViewerContext}; use re_viewport_blueprint::{ViewBlueprint, entity_path_for_view_property}; pub fn visible_time_range_ui_for_view( diff --git a/crates/viewer/re_selection_panel/src/visualizer_ui.rs b/crates/viewer/re_selection_panel/src/visualizer_ui.rs index 8608ab2be158..1ac474efbdc3 100644 --- a/crates/viewer/re_selection_panel/src/visualizer_ui.rs +++ b/crates/viewer/re_selection_panel/src/visualizer_ui.rs @@ -12,8 +12,8 @@ use re_ui::list_item::ListItemContentButtonsExt as _; use re_ui::{OnResponseExt as _, UiExt as _, design_tokens_of_visuals, list_item}; use re_view::latest_at_with_blueprint_resolved_data; use re_viewer_context::{ - DataResult, QueryContext, UiLayout, ViewContext, ViewSystemIdentifier, VisualizerCollection, - VisualizerSystem, + BlueprintContext as _, DataResult, QueryContext, UiLayout, ViewContext, ViewSystemIdentifier, + VisualizerCollection, VisualizerSystem, }; use re_viewport_blueprint::ViewBlueprint; diff --git a/crates/viewer/re_test_context/src/lib.rs b/crates/viewer/re_test_context/src/lib.rs index 3f4cff6f32fd..de8d0298a3ff 100644 --- a/crates/viewer/re_test_context/src/lib.rs +++ b/crates/viewer/re_test_context/src/lib.rs @@ -14,7 +14,7 @@ use re_chunk_store::LatestAtQuery; use re_entity_db::{EntityDb, InstancePath}; use re_global_context::{AppOptions, DisplayMode, Item, SystemCommandSender as _}; use re_log_types::{ - EntityPath, EntityPathPart, SetStoreInfo, StoreId, StoreInfo, StoreKind, Timeline, + EntityPath, EntityPathPart, SetStoreInfo, StoreId, StoreInfo, StoreKind, external::re_tuid::Tuid, }; use re_types::{Component as _, ComponentDescriptor, archetypes::RecordingInfo}; @@ -22,10 +22,10 @@ use re_types_core::reflection::Reflection; use re_ui::Help; use re_viewer_context::{ - ApplicationSelectionState, CommandReceiver, CommandSender, ComponentUiRegistry, - DataQueryResult, GlobalContext, ItemCollection, RecordingConfig, StoreHub, SystemCommand, - ViewClass, ViewClassRegistry, ViewId, ViewStates, ViewerContext, blueprint_timeline, - command_channel, + ApplicationSelectionState, BlueprintContext, CommandReceiver, CommandSender, + ComponentUiRegistry, DataQueryResult, GlobalContext, ItemCollection, RecordingConfig, StoreHub, + SystemCommand, TimeBlueprintExt as _, ViewClass, ViewClassRegistry, ViewId, ViewStates, + ViewerContext, blueprint_timeline, command_channel, }; pub mod external { @@ -79,6 +79,31 @@ pub struct TestContext { called_setup_kittest_for_rendering: AtomicBool, } +pub struct TestBlueprintCtx<'a> { + command_sender: &'a CommandSender, + current_blueprint: &'a EntityDb, + default_blueprint: Option<&'a EntityDb>, + blueprint_query: &'a re_chunk::LatestAtQuery, +} + +impl BlueprintContext for TestBlueprintCtx<'_> { + fn command_sender(&self) -> &CommandSender { + self.command_sender + } + + fn current_blueprint(&self) -> &EntityDb { + self.current_blueprint + } + + fn default_blueprint(&self) -> Option<&EntityDb> { + self.default_blueprint + } + + fn blueprint_query(&self) -> &re_chunk::LatestAtQuery { + self.blueprint_query + } +} + impl Default for TestContext { fn default() -> Self { Self::new() @@ -159,20 +184,32 @@ impl TestContext { let (command_sender, command_receiver) = command_channel(); - let recording_config = RecordingConfig::default(); - let blueprint_query = LatestAtQuery::latest(blueprint_timeline()); + let recording_config = { + let ctx = TestBlueprintCtx { + command_sender: &command_sender, + current_blueprint: store_hub + .active_blueprint() + .expect("We should have an active blueprint now"), + default_blueprint: store_hub.default_blueprint_for_app( + store_hub + .active_app() + .expect("We should have an active app now"), + ), + blueprint_query: &blueprint_query, + }; + + // ctx.set_timeline("log_tick".into()); + + RecordingConfig::from_blueprint(&ctx) + }; + let component_ui_registry = ComponentUiRegistry::new(); let reflection = re_types::reflection::generate_reflection().expect("Failed to generate reflection"); - recording_config - .time_ctrl - .write() - .set_timeline(Timeline::log_tick()); - Self { app_options: Default::default(), @@ -301,6 +338,46 @@ fn init_shared_renderer_setup() -> SharedWgpuResources { } impl TestContext { + /// Used to get a context with helper functions to write & read from blueprints. + pub fn with_blueprint_ctx(&self, f: impl FnOnce(TestBlueprintCtx<'_>) -> R) -> R { + fn with_blueprint_ctx_inner( + this: &TestContext, + f: impl FnOnce(TestBlueprintCtx<'_>) -> R, + ) -> R { + let store_hub = this + .store_hub + .try_lock() + .expect("Failed to get lock for blueprint ctx"); + + f(TestBlueprintCtx { + command_sender: &this.command_sender, + current_blueprint: store_hub + .active_blueprint() + .expect("The test context should always have an active blueprint"), + default_blueprint: store_hub.default_blueprint_for_app( + store_hub + .active_app() + .expect("The test context should always have an active app"), + ), + blueprint_query: &this.blueprint_query, + }) + } + + let r = with_blueprint_ctx_inner(self, f); + + // Handle system commands directly after updating blueprints to write it to the store. + self.handle_system_commands(); + + with_blueprint_ctx_inner(self, |ctx| { + self.recording_config + .time_ctrl + .write() + .update_from_blueprint(&ctx, None); + }); + + r + } + pub fn setup_kittest_for_rendering(&self) -> egui_kittest::HarnessBuilder<()> { // Egui kittests insists on having a fresh render state for each test. let new_render_state = create_egui_renderstate(); @@ -340,13 +417,6 @@ impl TestContext { .clone() } - pub fn set_active_timeline(&self, timeline: Timeline) { - self.recording_config - .time_ctrl - .write() - .set_timeline(timeline); - } - pub fn edit_selection(&self, edit_fn: impl FnOnce(&mut ApplicationSelectionState)) { let mut selection_state = self.selection_state.lock(); edit_fn(&mut selection_state); @@ -411,6 +481,7 @@ impl TestContext { .callback_resources .get_mut::() .expect("No re_renderer::RenderContext in egui_render_state"); + render_ctx.begin_frame(); let mut selection_state = self.selection_state.lock(); @@ -448,6 +519,11 @@ impl TestContext { drag_and_drop_manager: &drag_and_drop_manager, }; + self.recording_config + .time_ctrl + .write() + .update_from_blueprint(&ctx, Some(store_context.recording.times_per_timeline())); + func(&ctx); // If re_renderer was used, `setup_kittest_for_rendering` should have been called. @@ -564,15 +640,14 @@ impl TestContext { self.command_sender .send_system(SystemCommand::SetActiveTime { store_id, - timeline: re_chunk::Timeline::new(timeline, timecell.typ()), - time: Some(timecell.as_i64().into()), - pending: true, + timeline, + time: Some(timecell.value.into()), }); } } /// Best-effort attempt to meaningfully handle some of the system commands. - pub fn handle_system_commands(&mut self) { + pub fn handle_system_commands(&self) { while let Some(command) = self.command_receiver.recv_system() { let mut handled = true; let command_name = format!("{command:?}"); @@ -585,7 +660,10 @@ impl TestContext { // Ignore this trying to copy to the clipboard. } SystemCommand::AppendToStore(store_id, chunks) => { - let store_hub = self.store_hub.get_mut(); + let mut store_hub = self + .store_hub + .try_lock() + .expect("Failed to lock store hub mutex"); let db = store_hub.entity_db_mut(&store_id); for chunk in chunks { @@ -595,7 +673,10 @@ impl TestContext { } SystemCommand::DropEntity(store_id, entity_path) => { - let store_hub = self.store_hub.get_mut(); + let mut store_hub = self + .store_hub + .try_lock() + .expect("Failed to lock store hub mutex"); assert_eq!(Some(&store_id), store_hub.active_blueprint_id()); store_hub @@ -615,21 +696,18 @@ impl TestContext { store_id: rec_id, timeline, time, - pending, } => { assert_eq!( &rec_id, self.store_hub.lock().active_recording().unwrap().store_id() ); - let mut time_ctrl = self.recording_config.time_ctrl.write(); - if pending { - time_ctrl.set_pending_timeline(timeline); - } else { - time_ctrl.set_timeline(timeline); - } - if let Some(time) = time { - time_ctrl.set_time(time); - } + self.with_blueprint_ctx(|ctx| { + ctx.set_timeline(timeline); + + if let Some(time) = time { + ctx.set_time(time); + } + }); } SystemCommand::AddValidTimeRange { diff --git a/crates/viewer/re_test_viewport/src/lib.rs b/crates/viewer/re_test_viewport/src/lib.rs index 70ef2eca76c1..b5ef7ac43483 100644 --- a/crates/viewer/re_test_viewport/src/lib.rs +++ b/crates/viewer/re_test_viewport/src/lib.rs @@ -25,10 +25,10 @@ pub trait TestContextExt { fn ui_for_single_view(&self, ui: &mut egui::Ui, ctx: &ViewerContext<'_>, view_id: ViewId); /// [`TestContext::run`] inside a central panel that displays the ui for a single given view. - fn run_with_single_view(&mut self, ui: &mut egui::Ui, view_id: ViewId); + fn run_with_single_view(&self, ui: &mut egui::Ui, view_id: ViewId); fn run_view_ui_and_save_snapshot( - &mut self, + &self, view_id: ViewId, snapshot_name: &str, size: egui::Vec2, @@ -167,7 +167,7 @@ impl TestContextExt for TestContext { } /// [`TestContext::run`] for a single view. - fn run_with_single_view(&mut self, ui: &mut egui::Ui, view_id: ViewId) { + fn run_with_single_view(&self, ui: &mut egui::Ui, view_id: ViewId) { self.run_ui(ui, |ctx, ui| { self.ui_for_single_view(ui, ctx, view_id); }); @@ -176,7 +176,7 @@ impl TestContextExt for TestContext { } fn run_view_ui_and_save_snapshot( - &mut self, + &self, view_id: ViewId, snapshot_name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_time_panel/src/time_control_ui.rs b/crates/viewer/re_time_panel/src/time_control_ui.rs index bd45118f4f4e..15225767eddd 100644 --- a/crates/viewer/re_time_panel/src/time_control_ui.rs +++ b/crates/viewer/re_time_panel/src/time_control_ui.rs @@ -6,6 +6,8 @@ use re_ui::{UICommand, UICommandSender as _, UiExt as _, list_item}; use re_viewer_context::{Looping, PlayState, TimeControl, ViewerContext}; +use crate::time_panel::TimeControlExt; + #[derive(serde::Deserialize, serde::Serialize, Default)] pub struct TimeControlUi; @@ -13,11 +15,14 @@ impl TimeControlUi { #[allow(clippy::unused_self)] pub fn timeline_selector_ui( &self, - time_control: &mut TimeControl, + ctx: &ViewerContext<'_>, + time_control: &mut impl TimeControlExt, times_per_timeline: &TimesPerTimeline, ui: &mut egui::Ui, ) { - time_control.select_a_valid_timeline(times_per_timeline); + time_control + .get_mut() + .select_a_valid_timeline(times_per_timeline); ui.scope(|ui| { ui.spacing_mut().button_padding += egui::Vec2::new(2.0, 0.0); @@ -26,13 +31,13 @@ impl TimeControlUi { ui.visuals_mut().widgets.open.expansion = 0.0; let response = egui::ComboBox::from_id_salt("timeline") - .selected_text(time_control.timeline().name().as_str()) + .selected_text(time_control.get().timeline().name().as_str()) .show_ui(ui, |ui| { for timeline_stats in times_per_timeline.timelines_with_stats() { let timeline = &timeline_stats.timeline; if ui .selectable_label( - timeline == time_control.timeline(), + timeline == time_control.get().timeline(), ( timeline.name().as_str(), egui::Atom::grow(), @@ -46,7 +51,7 @@ impl TimeControlUi { ) .clicked() { - time_control.set_timeline(*timeline); + time_control.set_timeline(ctx, *timeline); } } }) @@ -93,7 +98,7 @@ You can also define your own timelines, e.g. for sensor time or camera frame num .at_pointer_fixed() .show(|ui| { if ui.button("Copy timeline name").clicked() { - let timeline = format!("{}", time_control.timeline().name()); + let timeline = format!("{}", time_control.get().timeline().name()); re_log::info!("Copied timeline: {}", timeline); ui.ctx().copy_text(timeline); } @@ -103,8 +108,8 @@ You can also define your own timelines, e.g. for sensor time or camera frame num #[allow(clippy::unused_self)] pub fn fps_ui(&self, time_control: &mut TimeControl, ui: &mut egui::Ui) { - if time_control.time_type() == TimeType::Sequence - && let Some(mut fps) = time_control.fps() + if time_control.get().time_type() == TimeType::Sequence + && let Some(mut fps) = time_control.get().fps() { ui.scope(|ui| { ui.spacing_mut().interact_size -= egui::Vec2::new(0., 4.); @@ -124,16 +129,16 @@ You can also define your own timelines, e.g. for sensor time or camera frame num pub fn play_pause_ui( &self, ctx: &ViewerContext<'_>, - time_control: &mut TimeControl, + time_control: &mut impl TimeControlExt, ui: &mut egui::Ui, ) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; // from figma - self.play_button_ui(ctx, time_control, ui); - self.follow_button_ui(ctx, time_control, ui); - self.pause_button_ui(ctx, time_control, ui); + self.play_button_ui(ctx, time_control.get(), ui); + self.follow_button_ui(ctx, time_control.get(), ui); + self.pause_button_ui(ctx, time_control.get(), ui); self.step_time_button_ui(ctx, ui); - self.loop_button_ui(time_control, ui); + self.loop_button_ui(time_control.get_mut(), ui); }); } @@ -215,7 +220,7 @@ You can also define your own timelines, e.g. for sensor time or camera frame num ui.scope(|ui| { // Loop-button cycles between states: - match time_control.looping() { + match time_control.get().looping() { Looping::Off => { if ui .large_button_selected(icon, false) @@ -252,7 +257,7 @@ You can also define your own timelines, e.g. for sensor time or camera frame num #[allow(clippy::unused_self)] pub fn playback_speed_ui(&self, time_control: &mut TimeControl, ui: &mut egui::Ui) { - let mut speed = time_control.speed(); + let mut speed = time_control.get().speed(); let drag_speed = (speed * 0.02).at_least(0.01); ui.scope(|ui| { ui.spacing_mut().interact_size -= egui::Vec2::new(0., 4.); diff --git a/crates/viewer/re_time_panel/src/time_panel.rs b/crates/viewer/re_time_panel/src/time_panel.rs index 7645222ba974..329cca7d38df 100644 --- a/crates/viewer/re_time_panel/src/time_panel.rs +++ b/crates/viewer/re_time_panel/src/time_panel.rs @@ -11,7 +11,7 @@ use re_data_ui::DataUi as _; use re_data_ui::item_ui::guess_instance_path_icon; use re_entity_db::{EntityDb, InstancePath}; use re_log_types::{ - AbsoluteTimeRange, ApplicationId, ComponentPath, EntityPath, TimeInt, TimeReal, + AbsoluteTimeRange, ApplicationId, ComponentPath, EntityPath, TimeInt, TimeReal, Timeline, }; use re_types::blueprint::components::PanelState; use re_types::reflection::ComponentDescriptorExt as _; @@ -20,9 +20,9 @@ use re_ui::{ContextExt as _, DesignTokens, Help, UiExt as _, filter_widget, icon use re_ui::{IconText, filter_widget::format_matching_text}; use re_viewer_context::open_url::ViewerOpenUrl; use re_viewer_context::{ - CollapseScope, HoverHighlight, Item, ItemCollection, ItemContext, RecordingConfig, - SystemCommand, SystemCommandSender as _, TimeControl, TimeView, UiLayout, ViewerContext, - VisitorControlFlow, + BlueprintTimeControl, CollapseScope, HoverHighlight, Item, ItemCollection, ItemContext, + RecordingConfig, SystemCommand, SystemCommandSender as _, TimeBlueprintExt as _, TimeControl, + TimeView, UiLayout, ViewerContext, VisitorControlFlow, }; use re_viewport_blueprint::ViewportBlueprint; @@ -167,6 +167,52 @@ impl Default for TimePanel { } } +/// This trait is used for the time panel to interact with either a +/// `TimeControl` or a `BlueprintTimeControl` to avoid code duplication. +pub trait TimeControlExt: Clone + PartialEq { + fn get(&self) -> &TimeControl; + fn get_mut(&mut self) -> &mut TimeControl; + + fn set_timeline(&mut self, ctx: &ViewerContext<'_>, timeline: Timeline); + fn set_time(&mut self, ctx: &ViewerContext<'_>, time: TimeInt); +} + +impl TimeControlExt for TimeControl { + fn get(&self) -> &TimeControl { + self + } + + fn get_mut(&mut self) -> &mut TimeControl { + self + } + + fn set_timeline(&mut self, ctx: &ViewerContext<'_>, timeline: Timeline) { + ctx.set_timeline(*timeline.name()); + } + + fn set_time(&mut self, ctx: &ViewerContext<'_>, time: TimeInt) { + ctx.set_time(time); + } +} + +impl TimeControlExt for BlueprintTimeControl { + fn get(&self) -> &TimeControl { + self + } + + fn get_mut(&mut self) -> &mut TimeControl { + self + } + + fn set_timeline(&mut self, _ctx: &ViewerContext<'_>, timeline: Timeline) { + self.set_timeline(timeline); + } + + fn set_time(&mut self, _ctx: &ViewerContext<'_>, time: TimeInt) { + self.set_time(time); + } +} + impl TimePanel { /// Ensures that all required store subscribers are correctly set up. /// @@ -194,7 +240,7 @@ impl TimePanel { ctx: &ViewerContext<'_>, viewport_blueprint: &ViewportBlueprint, entity_db: &re_entity_db::EntityDb, - rec_cfg: &RecordingConfig, + rec_cfg: &RecordingConfig, ui: &mut egui::Ui, state: PanelState, mut panel_frame: egui::Frame, @@ -290,7 +336,7 @@ impl TimePanel { ctx: &ViewerContext<'_>, viewport_blueprint: &ViewportBlueprint, entity_db: &EntityDb, - time_ctrl_after: &mut TimeControl, + time_ctrl_after: &mut impl TimeControlExt, ui: &mut Ui, ) { let tokens = ui.tokens(); @@ -337,11 +383,11 @@ impl TimePanel { ctx: &ViewerContext<'_>, entity_db: &re_entity_db::EntityDb, ui: &mut egui::Ui, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ) { ui.spacing_mut().item_spacing.x = 18.0; // from figma - let time_range = entity_db.time_range_for(time_ctrl.timeline().name()); + let time_range = entity_db.time_range_for(time_ctrl.get().timeline().name()); let has_more_than_one_time_point = time_range.is_some_and(|time_range| time_range.min() != time_range.max()); @@ -352,12 +398,14 @@ impl TimePanel { ui.horizontal(|ui| { self.time_control_ui.play_pause_ui(ctx, time_ctrl, ui); - self.time_control_ui.playback_speed_ui(time_ctrl, ui); - self.time_control_ui.fps_ui(time_ctrl, ui); + self.time_control_ui + .playback_speed_ui(time_ctrl.get_mut(), ui); + self.time_control_ui.fps_ui(time_ctrl.get_mut(), ui); }); } ui.horizontal(|ui| { self.time_control_ui.timeline_selector_ui( + ctx, time_ctrl, entity_db.times_per_timeline(), ui, @@ -374,11 +422,12 @@ impl TimePanel { } self.time_control_ui - .timeline_selector_ui(time_ctrl, times_per_timeline, ui); + .timeline_selector_ui(ctx, time_ctrl, times_per_timeline, ui); if has_more_than_one_time_point { - self.time_control_ui.playback_speed_ui(time_ctrl, ui); - self.time_control_ui.fps_ui(time_ctrl, ui); + self.time_control_ui + .playback_speed_ui(time_ctrl.get_mut(), ui); + self.time_control_ui.fps_ui(time_ctrl.get_mut(), ui); } self.collapsed_time_marker_and_time(ui, ctx, entity_db, time_ctrl); @@ -391,16 +440,16 @@ impl TimePanel { viewport_blueprint: &ViewportBlueprint, entity_db: &re_entity_db::EntityDb, ui: &mut egui::Ui, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ) { re_tracing::profile_function!(); - if time_ctrl.is_pending() { + if time_ctrl.get().is_pending() { ui.loading_screen_ui(|ui| { ui.label( egui::RichText::from(format!( "Waiting for timeline: {}", - time_ctrl.timeline().name() + time_ctrl.get().timeline().name() )) .heading() .strong(), @@ -412,7 +461,7 @@ impl TimePanel { ) .clicked() { - time_ctrl.set_timeline(*time_ctrl.timeline()); + ctx.clear_timeline(); } }); @@ -449,12 +498,12 @@ impl TimePanel { let side_margin = 26.0; // chosen so that the scroll bar looks approximately centered in the default gap self.time_ranges_ui = initialize_time_ranges_ui( entity_db, - time_ctrl, + time_ctrl.get(), Rangef::new( time_fg_x_range.min + side_margin, time_fg_x_range.max - side_margin, ), - time_ctrl.time_view(), + time_ctrl.get().time_view(), ); let full_y_range = Rangef::new(ui.max_rect().top(), ui.max_rect().bottom()); @@ -501,7 +550,7 @@ impl TimePanel { let time_bg_area_painter = ui.painter().with_clip_rect(time_bg_area_rect); let time_area_painter = ui.painter().with_clip_rect(time_fg_area_rect); - if let Some(highlighted_range) = time_ctrl.highlighted_range { + if let Some(highlighted_range) = time_ctrl.get().highlighted_range { paint_range_highlight( highlighted_range, &self.time_ranges_ui, @@ -521,7 +570,7 @@ impl TimePanel { ui, &time_area_painter, timeline_rect.top()..=timeline_rect.bottom(), - time_ctrl.time_type(), + time_ctrl.get().time_type(), ctx.app_options().timestamp_format, ); paint_time_ranges_gaps( @@ -531,7 +580,7 @@ impl TimePanel { full_y_range, ); time_selection_ui::loop_selection_ui( - time_ctrl, + time_ctrl.get_mut(), &self.time_ranges_ui, ui, &time_bg_area_painter, @@ -539,7 +588,7 @@ impl TimePanel { ); let time_area_response = interact_with_streams_rect( &self.time_ranges_ui, - time_ctrl, + time_ctrl.get_mut(), ui, &time_bg_area_rect, &streams_rect, @@ -558,7 +607,7 @@ impl TimePanel { ctx, viewport_blueprint, entity_db, - time_ctrl, + time_ctrl.get_mut(), &time_area_response, &lower_time_area_painter, ui, @@ -598,7 +647,7 @@ impl TimePanel { &timeline_rect, ); - self.time_ranges_ui.snap_time_control(time_ctrl); + self.time_ranges_ui.snap_time_control(ctx, time_ctrl); // remember where to show the time for next frame: self.prev_col_width = self.next_col_right - ui.min_rect().left(); @@ -1276,7 +1325,7 @@ impl TimePanel { ctx: &ViewerContext<'_>, entity_db: &re_entity_db::EntityDb, ui: &mut egui::Ui, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ) { ui.spacing_mut().item_spacing.x = 18.0; // from figma @@ -1285,11 +1334,13 @@ impl TimePanel { ui.vertical(|ui| { ui.horizontal(|ui| { self.time_control_ui.play_pause_ui(ctx, time_ctrl, ui); - self.time_control_ui.playback_speed_ui(time_ctrl, ui); - self.time_control_ui.fps_ui(time_ctrl, ui); + self.time_control_ui + .playback_speed_ui(time_ctrl.get_mut(), ui); + self.time_control_ui.fps_ui(time_ctrl.get_mut(), ui); }); ui.horizontal(|ui| { self.time_control_ui.timeline_selector_ui( + ctx, time_ctrl, entity_db.times_per_timeline(), ui, @@ -1308,9 +1359,10 @@ impl TimePanel { self.time_control_ui.play_pause_ui(ctx, time_ctrl, ui); self.time_control_ui - .timeline_selector_ui(time_ctrl, times_per_timeline, ui); - self.time_control_ui.playback_speed_ui(time_ctrl, ui); - self.time_control_ui.fps_ui(time_ctrl, ui); + .timeline_selector_ui(ctx, time_ctrl, times_per_timeline, ui); + self.time_control_ui + .playback_speed_ui(time_ctrl.get_mut(), ui); + self.time_control_ui.fps_ui(time_ctrl.get_mut(), ui); self.current_time_ui(ctx, ui, time_ctrl); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -1340,9 +1392,9 @@ impl TimePanel { ui: &mut egui::Ui, ctx: &ViewerContext<'_>, entity_db: &re_entity_db::EntityDb, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ) { - let timeline = time_ctrl.timeline(); + let timeline = time_ctrl.get().timeline(); let Some(time_range) = entity_db.time_range_for(timeline.name()) else { // We have no data on this timeline @@ -1366,15 +1418,15 @@ impl TimePanel { let time_ranges_ui = initialize_time_ranges_ui( entity_db, - time_ctrl, + time_ctrl.get(), time_range_rect.x_range(), None, ); - time_ranges_ui.snap_time_control(time_ctrl); + time_ranges_ui.snap_time_control(ctx, time_ctrl); let painter = ui.painter_at(time_range_rect.expand(4.0)); - if let Some(highlighted_range) = time_ctrl.highlighted_range { + if let Some(highlighted_range) = time_ctrl.get().highlighted_range { paint_range_highlight( highlighted_range, &time_ranges_ui, @@ -1384,7 +1436,7 @@ impl TimePanel { } time_selection_ui::collapsed_loop_selection_ui( - time_ctrl, + time_ctrl.get_mut(), &painter, &time_ranges_ui, ui, @@ -1400,7 +1452,7 @@ impl TimePanel { data_density_graph::data_density_graph_ui( &mut self.data_density_graph_painter, ctx, - time_ctrl, + time_ctrl.get(), entity_db, ui.painter(), ui, @@ -1429,12 +1481,12 @@ impl TimePanel { &mut self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ) { - if let Some(time_int) = time_ctrl.time_int() - && let Some(time) = time_ctrl.time() + if let Some(time_int) = time_ctrl.get().time_int() + && let Some(time) = time_ctrl.get().time() { - let time_type = time_ctrl.time_type(); + let time_type = time_ctrl.get().time_type(); let mut time_str = self .time_edit_string @@ -1451,7 +1503,7 @@ impl TimePanel { if let Some(time_int) = time_type.parse_time(&time_str, ctx.app_options().timestamp_format) { - time_ctrl.set_time(time_int); + time_ctrl.set_time(ctx, time_int); } self.time_edit_string = None; } @@ -1907,7 +1959,7 @@ fn copy_time_properties_context_menu(ui: &mut egui::Ui, time: TimeReal) { /// A vertical line that shows the current time. fn time_marker_ui( time_ranges_ui: &TimeRangesUi, - time_ctrl: &mut TimeControl, + time_ctrl: &mut impl TimeControlExt, ui: &egui::Ui, ctx: &ViewerContext<'_>, time_area_response: Option<&egui::Response>, @@ -1927,7 +1979,7 @@ fn time_marker_ui( let mut is_hovering_time_cursor = false; // show current time as a line: - if let Some(time) = time_ctrl.time() + if let Some(time) = time_ctrl.get().time() && let Some(mut x) = time_ranges_ui.x_from_time_f32(time) && timeline_rect.x_range().contains(x) { @@ -1951,8 +2003,8 @@ fn time_marker_ui( && let Some(time) = time_ranges_ui.time_from_x_f32(pointer_pos.x) { let time = time_ranges_ui.clamp_time(time); - time_ctrl.set_time(time); - time_ctrl.pause(); + time_ctrl.set_time(ctx, time.floor()); + time_ctrl.get_mut().pause(); x = pointer_pos.x; // avoid frame-delay } @@ -2019,8 +2071,8 @@ fn time_marker_ui( let mut set_time_to_pointer = || { if let Some(time) = hovered_time { let time = time_ranges_ui.clamp_time(time); - time_ctrl.set_time(time); - time_ctrl.pause(); + time_ctrl.set_time(ctx, time.floor()); + time_ctrl.get_mut().pause(); } }; @@ -2035,7 +2087,7 @@ fn time_marker_ui( ui.ctx().set_dragged_id(time_drag_id); } else if is_pointer_in_time_area_rect { if time_area_response.double_clicked() { - time_ctrl.reset_time_view(); + time_ctrl.get_mut().reset_time_view(); } else if time_area_response.clicked() && !is_anything_being_dragged { set_time_to_pointer(); } @@ -2050,7 +2102,7 @@ fn time_marker_ui( if egui::Popup::context_menu(&time_area_response) .width(300.0) .show(|ui| { - copy_timeline_properties_context_menu(ui, ctx, time_ctrl, hovered_time); + copy_timeline_properties_context_menu(ui, ctx, time_ctrl.get(), hovered_time); }) .is_some() { diff --git a/crates/viewer/re_time_panel/src/time_ranges_ui.rs b/crates/viewer/re_time_panel/src/time_ranges_ui.rs index 922815c10bbe..8b2d86d5f5f6 100644 --- a/crates/viewer/re_time_panel/src/time_ranges_ui.rs +++ b/crates/viewer/re_time_panel/src/time_ranges_ui.rs @@ -11,7 +11,9 @@ use egui::{NumExt as _, lerp, remap}; use itertools::Itertools as _; use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, TimeInt, TimeReal}; -use re_viewer_context::{PlayState, TimeControl, TimeView}; +use re_viewer_context::{PlayState, TimeView, ViewerContext}; + +use crate::time_panel::TimeControlExt; /// The ideal gap between time segments. /// @@ -281,16 +283,16 @@ impl TimeRangesUi { } // Make sure playback time doesn't get stuck between non-continuous regions: - pub fn snap_time_control(&self, time_ctrl: &mut TimeControl) { - if time_ctrl.play_state() != PlayState::Playing { + pub fn snap_time_control(&self, ctx: &ViewerContext<'_>, time_ctrl: &mut impl TimeControlExt) { + if time_ctrl.get().play_state() != PlayState::Playing { return; } // Make sure time doesn't get stuck between non-continuous regions: - if let Some(time) = time_ctrl.time() { + if let Some(time) = time_ctrl.get().time() { let time = self.snap_time_to_segments(time); - time_ctrl.set_time(time); - } else if let Some(selection) = time_ctrl.loop_selection() { + time_ctrl.set_time(ctx, time.floor()); + } else if let Some(selection) = time_ctrl.get().loop_selection() { let snapped_min = self.snap_time_to_segments(selection.min); let snapped_max = self.snap_time_to_segments(selection.max); @@ -302,10 +304,12 @@ impl TimeRangesUi { } // Keeping max works better when looping - time_ctrl.set_loop_selection(AbsoluteTimeRangeF::new( - snapped_max - selection.length(), - snapped_max, - )); + time_ctrl + .get_mut() + .set_loop_selection(AbsoluteTimeRangeF::new( + snapped_max - selection.length(), + snapped_max, + )); } } diff --git a/crates/viewer/re_time_panel/tests/time_panel_filter_tests.rs b/crates/viewer/re_time_panel/tests/time_panel_filter_tests.rs index f111d86d0f92..cef50d5b2a33 100644 --- a/crates/viewer/re_time_panel/tests/time_panel_filter_tests.rs +++ b/crates/viewer/re_time_panel/tests/time_panel_filter_tests.rs @@ -44,7 +44,7 @@ pub fn test_various_filter_ui_snapshot() { } run_time_panel_and_save_snapshot( - test_context, + &test_context, time_panel, &format!( "various_filters-{}", @@ -116,7 +116,7 @@ fn add_point_to_chunk_builder(builder: ChunkBuilder) -> ChunkBuilder { } fn run_time_panel_and_save_snapshot( - mut test_context: TestContext, + test_context: &TestContext, mut time_panel: TimePanel, snapshot_name: &str, ) { diff --git a/crates/viewer/re_time_panel/tests/time_panel_tests.rs b/crates/viewer/re_time_panel/tests/time_panel_tests.rs index 656037975f4e..23aee45169a2 100644 --- a/crates/viewer/re_time_panel/tests/time_panel_tests.rs +++ b/crates/viewer/re_time_panel/tests/time_panel_tests.rs @@ -5,13 +5,14 @@ use egui::Vec2; use re_chunk_store::{LatestAtQuery, RowId}; use re_entity_db::InstancePath; use re_log_types::{ - AbsoluteTimeRange, EntityPath, TimeInt, TimePoint, TimeType, Timeline, build_frame_nr, + AbsoluteTimeRange, EntityPath, TimeInt, TimePoint, TimeType, Timeline, TimelineName, + build_frame_nr, example_components::{MyPoint, MyPoints}, }; use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; use re_time_panel::TimePanel; use re_types::archetypes::Points2D; -use re_viewer_context::{CollapseScope, TimeView, blueprint_timeline}; +use re_viewer_context::{CollapseScope, TimeBlueprintExt as _, TimeView, blueprint_timeline}; use re_viewport_blueprint::ViewportBlueprint; fn add_sparse_data(test_context: &mut TestContext) { @@ -39,7 +40,7 @@ pub fn time_panel_two_sections() { add_sparse_data(&mut test_context); run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -63,7 +64,7 @@ pub fn time_panel_two_sections_with_valid_range() { } run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -79,7 +80,7 @@ pub fn time_panel_two_sections_with_valid_range() { }); } run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -109,7 +110,7 @@ pub fn time_panel_two_sections_with_two_valid_ranges() { } run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -125,7 +126,7 @@ pub fn time_panel_two_sections_with_two_valid_ranges() { }); } run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -165,7 +166,7 @@ pub fn time_panel_dense_data() { }); run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, TimePanel::default(), 300.0, false, @@ -225,7 +226,7 @@ pub fn run_time_panel_filter_tests(filter_active: bool, query: &str, snapshot_na time_panel.activate_filter(query); } - run_time_panel_and_save_snapshot(&mut test_context, time_panel, 300.0, false, snapshot_name); + run_time_panel_and_save_snapshot(&test_context, time_panel, 300.0, false, snapshot_name); } // -- @@ -243,11 +244,12 @@ pub fn test_various_entity_kinds_in_time_panel() { log_data_for_various_entity_kinds_tests(&mut test_context); - test_context - .recording_config - .time_ctrl - .write() - .set_timeline_and_time(Timeline::new(timeline, TimeType::Sequence), time); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline_and_time( + TimelineName::new(timeline), + TimeInt::saturated_temporal_i64(time), + ); + }); test_context .recording_config @@ -261,7 +263,7 @@ pub fn test_various_entity_kinds_in_time_panel() { let time_panel = TimePanel::default(); run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, time_panel, 1200.0, true, @@ -285,7 +287,7 @@ pub fn test_focused_item_is_focused() { let time_panel = TimePanel::default(); run_time_panel_and_save_snapshot( - &mut test_context, + &test_context, time_panel, 200.0, false, @@ -358,7 +360,7 @@ pub fn log_static_data(test_context: &mut TestContext, entity_path: impl Into { self.ctx.command_sender().send_system( re_viewer_context::SystemCommand::SetActiveTime { store_id: self.ctx.store_id().clone(), - timeline: descr.timeline(), + timeline: *descr.timeline().name(), time: None, - pending: false, }, ); } diff --git a/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs b/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs index ac29b628e4f4..fcda41a8ffb4 100644 --- a/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs +++ b/crates/viewer/re_view_dataframe/src/view_query/blueprint.rs @@ -307,7 +307,7 @@ mod test { /// Simple test to demo round-trip testing using [`TestContext::run_and_handle_system_commands`]. #[test] fn test_latest_at_enabled() { - let mut test_context = TestContext::new(); + let test_context = TestContext::new(); let view_id = ViewId::random(); diff --git a/crates/viewer/re_view_dataframe/tests/basic.rs b/crates/viewer/re_view_dataframe/tests/basic.rs index 3246cf7c952c..0460e22bc267 100644 --- a/crates/viewer/re_view_dataframe/tests/basic.rs +++ b/crates/viewer/re_view_dataframe/tests/basic.rs @@ -59,7 +59,7 @@ pub fn test_unknown_timeline() { ); run_view_selection_panel_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "unknown_timeline_selection_panel_ui", egui::vec2(300.0, 450.0), @@ -90,7 +90,7 @@ fn setup_blueprint(test_context: &mut TestContext, timeline_name: &TimelineName) } fn run_view_selection_panel_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png b/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png index 8a0a4b951574..60bf2903c155 100644 --- a/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png +++ b/crates/viewer/re_view_dataframe/tests/snapshots/null_timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1c787ad2378678d280c1bc55e4076ada6f3a748e9967ed9e4c32c0e708ee5a0 -size 13413 +oid sha256:87e12c7c8ba68c3d07fa1e1fff02b72d5fa016d4082a4d6e282592243399c94e +size 13410 diff --git a/crates/viewer/re_view_spatial/tests/annotations.rs b/crates/viewer/re_view_spatial/tests/annotations.rs index a976fc30a208..b0a55e00f83b 100644 --- a/crates/viewer/re_view_spatial/tests/annotations.rs +++ b/crates/viewer/re_view_spatial/tests/annotations.rs @@ -59,7 +59,7 @@ pub fn test_annotations() { )) }); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "annotations", // We need quite a bunch of pixels to be able to stack the double hover pop-ups. @@ -79,7 +79,7 @@ fn get_test_context() -> TestContext { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_spatial/tests/bgr_images.rs b/crates/viewer/re_view_spatial/tests/bgr_images.rs index ac5eaf164411..4b14b10ed29e 100644 --- a/crates/viewer/re_view_spatial/tests/bgr_images.rs +++ b/crates/viewer/re_view_spatial/tests/bgr_images.rs @@ -13,7 +13,7 @@ use re_types::{ Archetype as _, blueprint::components::BackgroundKind, datatypes::ColorModel, image::ImageChannelType, }; -use re_viewer_context::ViewClass as _; +use re_viewer_context::{BlueprintContext as _, ViewClass as _}; use re_viewport_blueprint::ViewBlueprint; fn convert_pixels_to + Copy>(u8s: &[u8]) -> Vec { diff --git a/crates/viewer/re_view_spatial/tests/blueprint_2d.rs b/crates/viewer/re_view_spatial/tests/blueprint_2d.rs index 741a414db817..083f994ea23d 100644 --- a/crates/viewer/re_view_spatial/tests/blueprint_2d.rs +++ b/crates/viewer/re_view_spatial/tests/blueprint_2d.rs @@ -4,7 +4,7 @@ use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_types::{Archetype as _, archetypes}; use re_view_spatial::SpatialView2D; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewContents}; const SNAPSHOT_SIZE: egui::Vec2 = egui::vec2(400.0, 180.0); diff --git a/crates/viewer/re_view_spatial/tests/draw_order.rs b/crates/viewer/re_view_spatial/tests/draw_order.rs index 0ef78cfba6c2..eb50f63d628b 100644 --- a/crates/viewer/re_view_spatial/tests/draw_order.rs +++ b/crates/viewer/re_view_spatial/tests/draw_order.rs @@ -149,7 +149,7 @@ pub fn test_draw_order() { )) }); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "draw_order", egui::vec2(300.0, 150.0) * 2.0, @@ -157,7 +157,7 @@ pub fn test_draw_order() { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_spatial/tests/latest_at_partial_updates.rs b/crates/viewer/re_view_spatial/tests/latest_at_partial_updates.rs index e8f6bfe2ea88..f3b721e159b8 100644 --- a/crates/viewer/re_view_spatial/tests/latest_at_partial_updates.rs +++ b/crates/viewer/re_view_spatial/tests/latest_at_partial_updates.rs @@ -4,7 +4,7 @@ use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; use re_test_viewport::TestContextExt as _; use re_types::{Archetype as _, archetypes}; use re_view_spatial::SpatialView2D; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::ViewBlueprint; #[test] @@ -74,7 +74,7 @@ fn test_latest_at_partial_update() { let view_id = setup_blueprint(&mut test_context); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "latest_at_partial_updates", egui::vec2(200.0, 200.0), @@ -110,12 +110,12 @@ fn setup_blueprint(test_context: &mut TestContext) -> ViewId { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, ) { - let rec_config = test_context.recording_config.clone(); + let (timeline, _) = build_frame_nr(42); let mut harness = test_context .setup_kittest_for_rendering() @@ -123,6 +123,7 @@ fn run_view_ui_and_save_snapshot( .build_ui(|ui| { test_context.run_with_single_view(ui, view_id); }); + { let broken_pixels_fraction = 0.004; let options = SnapshotOptions::new() @@ -134,10 +135,10 @@ fn run_view_ui_and_save_snapshot( let mut success = true; for frame_nr in 42..=46 { { - let (timeline, time) = build_frame_nr(frame_nr); - let mut time_ctrl = rec_config.time_ctrl.write(); - time_ctrl.set_timeline(timeline); - time_ctrl.set_time(time); + let (_, time) = build_frame_nr(frame_nr); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline_and_time(*timeline.name(), time); + }); } harness.run_steps(8); diff --git a/crates/viewer/re_view_spatial/tests/pinhole_camera.rs b/crates/viewer/re_view_spatial/tests/pinhole_camera.rs index 4ddb65d1fe92..6b6c6b39b29b 100644 --- a/crates/viewer/re_view_spatial/tests/pinhole_camera.rs +++ b/crates/viewer/re_view_spatial/tests/pinhole_camera.rs @@ -22,7 +22,7 @@ pub fn test_pinhole_camera() { }); let view_id = setup_blueprint(&mut test_context); - run_view_ui_and_save_snapshot(&mut test_context, view_id, egui::vec2(300.0, 300.0)); + run_view_ui_and_save_snapshot(&test_context, view_id, egui::vec2(300.0, 300.0)); } #[allow(clippy::unwrap_used)] @@ -44,11 +44,7 @@ fn setup_blueprint(test_context: &mut TestContext) -> ViewId { }) } -fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, - view_id: ViewId, - size: egui::Vec2, -) { +fn run_view_ui_and_save_snapshot(test_context: &TestContext, view_id: ViewId, size: egui::Vec2) { let mut harness = test_context .setup_kittest_for_rendering() .with_size(size) diff --git a/crates/viewer/re_view_spatial/tests/static_overwrite.rs b/crates/viewer/re_view_spatial/tests/static_overwrite.rs index 650a48334caf..f405a6b89699 100644 --- a/crates/viewer/re_view_spatial/tests/static_overwrite.rs +++ b/crates/viewer/re_view_spatial/tests/static_overwrite.rs @@ -8,7 +8,7 @@ use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; use re_test_viewport::TestContextExt as _; use re_types::archetypes; use re_view_spatial::SpatialView3D; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewContents}; const SNAPSHOT_SIZE: egui::Vec2 = egui::vec2(300.0, 300.0); @@ -65,7 +65,7 @@ pub fn test_static_overwrite_original() { let view_id = setup_blueprint(&mut test_context, &entity_path, None, None); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "static_overwrite_original", SNAPSHOT_SIZE, @@ -84,7 +84,7 @@ pub fn test_static_overwrite_radius_default() { let view_id = setup_blueprint(&mut test_context, &entity_path, Some(&radius_default), None); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "static_overwrite_radius_default", SNAPSHOT_SIZE, @@ -105,7 +105,7 @@ pub fn test_static_overwrite_color_override() { let view_id = setup_blueprint(&mut test_context, &entity_path, None, Some(&color_override)); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, "static_overwrite_color_override", SNAPSHOT_SIZE, @@ -113,7 +113,7 @@ pub fn test_static_overwrite_color_override() { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_spatial/tests/transform_clamping.rs b/crates/viewer/re_view_spatial/tests/transform_clamping.rs index 365f38c7f80c..0cf3cadabffd 100644 --- a/crates/viewer/re_view_spatial/tests/transform_clamping.rs +++ b/crates/viewer/re_view_spatial/tests/transform_clamping.rs @@ -136,7 +136,7 @@ pub fn test_transform_clamping() { let view_ids = setup_blueprint(&mut test_context); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_ids, "transform_clamping", egui::vec2(300.0, 300.0), @@ -176,7 +176,7 @@ fn setup_blueprint(test_context: &mut TestContext) -> (ViewId, ViewId) { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, (view_id_boxes, view_id_spheres): (ViewId, ViewId), name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs b/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs index c8c994f26aa6..fa325b2841e5 100644 --- a/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs +++ b/crates/viewer/re_view_spatial/tests/transform_hierarchy.rs @@ -1,8 +1,8 @@ use re_chunk_store::RowId; -use re_log_types::{EntityPath, TimePoint, Timeline}; +use re_log_types::{EntityPath, TimeInt, TimePoint, Timeline}; use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; use re_test_viewport::TestContextExt as _; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::ViewBlueprint; #[test] @@ -135,7 +135,7 @@ pub fn test_transform_hierarchy() { let view_id = setup_blueprint(&mut test_context); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, timeline_step, view_id, "transform_hierarchy", @@ -156,15 +156,15 @@ fn setup_blueprint(test_context: &mut TestContext) -> ViewId { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, timeline: Timeline, view_id: ViewId, name: &str, size: egui::Vec2, ) { - test_context.set_active_timeline(timeline); - - let rec_cfg = test_context.recording_config.clone(); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); let mut harness = test_context .setup_kittest_for_rendering() @@ -201,10 +201,9 @@ fn run_view_ui_and_save_snapshot( for time in 0..=7 { let name = format!("{name}_{}_{time}", timeline.name()); - rec_cfg - .time_ctrl - .write() - .set_time_for_timeline(timeline, time); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_time(TimeInt::saturated_temporal_i64(time as i64)); + }); harness.run_steps(8); diff --git a/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs b/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs index fbcd498eaa8c..eac9bd0b8b7a 100644 --- a/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs +++ b/crates/viewer/re_view_spatial/tests/transform_tree_origins.rs @@ -143,7 +143,7 @@ pub fn test_transform_tree_origins() { for origin in ["/sun", "/sun/planet", "/sun/planet/moon"] { let view_id = setup_blueprint(&mut test_context, origin); run_view_ui_and_save_snapshot( - &mut test_context, + &test_context, view_id, &format!("transform_tree_origins_{}", origin.replace('/', "_")), egui::vec2(400.0, 250.0), @@ -177,7 +177,7 @@ fn setup_blueprint(test_context: &mut TestContext, origin: &str) -> ViewId { #[track_caller] fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_spatial/tests/video.rs b/crates/viewer/re_view_spatial/tests/video.rs index 7145450060b8..5a4ef6e43cb3 100644 --- a/crates/viewer/re_view_spatial/tests/video.rs +++ b/crates/viewer/re_view_spatial/tests/video.rs @@ -1,7 +1,5 @@ #![expect(clippy::unwrap_used)] // It's a test! -use std::cell::Cell; - use re_chunk_store::RowId; use re_log_types::{NonMinI64, TimeInt, TimePoint}; use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; @@ -12,7 +10,7 @@ use re_types::{ datatypes, }; use re_video::{VideoCodec, VideoDataDescription}; -use re_viewer_context::ViewClass as _; +use re_viewer_context::{TimeBlueprintExt as _, ViewClass as _}; use re_viewport_blueprint::ViewBlueprint; fn workspace_dir() -> std::path::PathBuf { @@ -273,29 +271,26 @@ fn test_video(video_type: VideoType, codec: VideoCodec) { let step_dt_seconds = 1.0 / 4.0; // This is also the current egui_kittest default, but let's be explicit since we use `try_run_realtime`. let max_total_time_seconds = 60.0; - // Using a single harness for all frames - we want to make sure that we use the same decoder, - // not tearing down the video player! - let desired_seek_ns = Cell::new(0); let mut harness = test_context .setup_kittest_for_rendering() .with_step_dt(step_dt_seconds) .with_max_steps((max_total_time_seconds / step_dt_seconds) as u64) .with_size(egui::vec2(300.0, 200.0)) .build_ui(|ui| { - // Since we can't access `test_context` after creating `harness`, we have to do the seeking in here. - { - let mut time_ctrl = test_context.recording_config.time_ctrl.write(); - time_ctrl.set_time(TimeInt::from_nanos( - NonMinI64::new(desired_seek_ns.get()).unwrap(), - )); - } test_context.run_with_single_view(ui, view_id); std::thread::sleep(std::time::Duration::from_millis(20)); }); for seek_location in VideoTestSeekLocation::ALL { - desired_seek_ns.set(seek_location.get_time_ns(&frame_timestamps_nanos)); + // Using a single harness for all frames - we want to make sure that we use the same decoder, + // not tearing down the video player! + let desired_seek_ns = seek_location.get_time_ns(&frame_timestamps_nanos); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_time(TimeInt::from_nanos( + NonMinI64::new(desired_seek_ns).unwrap(), + )); + }); // Video decoding happens in a different thread, so it's important that we give it time // and don't busy loop. diff --git a/crates/viewer/re_view_spatial/tests/visible_time_range.rs b/crates/viewer/re_view_spatial/tests/visible_time_range.rs index 37b755e10692..da7126aa7699 100644 --- a/crates/viewer/re_view_spatial/tests/visible_time_range.rs +++ b/crates/viewer/re_view_spatial/tests/visible_time_range.rs @@ -3,12 +3,12 @@ #![expect(clippy::unnecessary_fallible_conversions)] use re_chunk_store::RowId; -use re_log_types::{EntityPath, TimeInt, TimePoint, TimeReal, Timeline}; +use re_log_types::{EntityPath, TimeInt, TimePoint, Timeline}; use re_test_context::TestContext; use re_test_viewport::TestContextExt as _; use re_types::{Archetype as _, archetypes::Points2D, datatypes::VisibleTimeRange}; use re_view_spatial::SpatialView2D; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::ViewBlueprint; fn intra_timestamp_data(test_context: &mut TestContext) { @@ -104,8 +104,9 @@ fn intra_timestamp_data(test_context: &mut TestContext) { ) }); - let mut time_ctrl = test_context.recording_config.time_ctrl.write(); - time_ctrl.set_timeline(timeline); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); } #[test] @@ -223,9 +224,9 @@ fn visible_timerange_data(test_context: &mut TestContext) { } } - let mut time_ctrl = test_context.recording_config.time_ctrl.write(); - time_ctrl.set_timeline(timeline); - time_ctrl.set_time(TimeReal::from_secs(4.5)); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline_and_time(*timeline.name(), TimeInt::from_secs(4.5)); + }); } #[test] diff --git a/crates/viewer/re_view_tensor/tests/tensor_2d.rs b/crates/viewer/re_view_tensor/tests/tensor_2d.rs index ed8b1d639f75..9396c3ecbde8 100644 --- a/crates/viewer/re_view_tensor/tests/tensor_2d.rs +++ b/crates/viewer/re_view_tensor/tests/tensor_2d.rs @@ -57,7 +57,7 @@ fn test_tensor() { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_text_document/tests/text_document_test.rs b/crates/viewer/re_view_text_document/tests/text_document_test.rs index cd773bd19a60..86679ed22e62 100644 --- a/crates/viewer/re_view_text_document/tests/text_document_test.rs +++ b/crates/viewer/re_view_text_document/tests/text_document_test.rs @@ -53,7 +53,7 @@ fn test_text_documents() { } fn run_view_ui_and_save_snapshot( - test_context: &mut TestContext, + test_context: &TestContext, view_id: ViewId, name: &str, size: egui::Vec2, diff --git a/crates/viewer/re_view_time_series/src/view_class.rs b/crates/viewer/re_view_time_series/src/view_class.rs index 55400bbcdffe..1c2cad6b2032 100644 --- a/crates/viewer/re_view_time_series/src/view_class.rs +++ b/crates/viewer/re_view_time_series/src/view_class.rs @@ -5,7 +5,7 @@ use smallvec::SmallVec; use re_chunk_store::TimeType; use re_format::next_grid_tick_magnitude_nanos; -use re_log_types::{EntityPath, TimeInt}; +use re_log_types::{EntityPath, NonMinI64, TimeInt}; use re_types::{ ComponentBatch as _, View as _, ViewClassIdentifier, archetypes::{SeriesLines, SeriesPoints}, @@ -22,12 +22,12 @@ use re_view::{ view_property_ui, }; use re_viewer_context::{ - IdentifiedViewSystem as _, IndicatedEntities, MaybeVisualizableEntities, PerVisualizer, - QueryRange, RecommendedView, SmallVisualizerSet, SystemExecutionOutput, - TypedComponentFallbackProvider, ViewClass, ViewClassExt as _, ViewClassRegistryError, - ViewHighlights, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt as _, - ViewSystemExecutionError, ViewSystemIdentifier, ViewerContext, VisualizableEntities, - external::re_entity_db::InstancePath, + BlueprintContext as _, IdentifiedViewSystem as _, IndicatedEntities, MaybeVisualizableEntities, + PerVisualizer, QueryRange, RecommendedView, SmallVisualizerSet, SystemExecutionOutput, + TimeBlueprintExt as _, TypedComponentFallbackProvider, ViewClass, ViewClassExt as _, + ViewClassRegistryError, ViewHighlights, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, + ViewStateExt as _, ViewSystemExecutionError, ViewSystemIdentifier, ViewerContext, + VisualizableEntities, external::re_entity_db::InstancePath, }; use re_viewport_blueprint::ViewProperty; @@ -547,9 +547,11 @@ impl ViewClass for TimeSeriesView { if plot_ui.response().secondary_clicked() && let Some(pointer) = plot_ui.pointer_coordinate() { + ctx.set_time(NonMinI64::saturating_from_i64( + pointer.x as i64 + time_offset, + )); + let mut time_ctrl_write = ctx.rec_cfg.time_ctrl.write(); - let timeline = *time_ctrl_write.timeline(); - time_ctrl_write.set_timeline_and_time(timeline, pointer.x as i64 + time_offset); time_ctrl_write.pause(); } @@ -669,8 +671,9 @@ impl ViewClass for TimeSeriesView { // Avoid frame-delay: time_x = pointer_pos.x; + ctx.set_time(TimeInt::saturated_temporal_i64(new_time)); + let mut time_ctrl = ctx.rec_cfg.time_ctrl.write(); - time_ctrl.set_time(new_time); time_ctrl.pause(); state.is_dragging_time_cursor = true; diff --git a/crates/viewer/re_view_time_series/tests/basic.rs b/crates/viewer/re_view_time_series/tests/basic.rs index c60daac88f7e..a9cac8409f90 100644 --- a/crates/viewer/re_view_time_series/tests/basic.rs +++ b/crates/viewer/re_view_time_series/tests/basic.rs @@ -3,7 +3,7 @@ use re_log_types::{EntityPath, TimePoint, Timeline}; use re_test_context::{TestContext, external::egui_kittest::SnapshotOptions}; use re_test_viewport::TestContextExt as _; use re_view_time_series::TimeSeriesView; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewContents}; fn color_gradient0(step: i64) -> re_types::components::Color { @@ -24,6 +24,8 @@ pub fn test_clear_series_points_and_line() { fn test_clear_series_points_and_line_impl(two_series_per_entity: bool) { let mut test_context = TestContext::new_with_view_class::(); + let timeline = Timeline::log_tick(); + // TODO(#10512): Potentially fix up this after we have "markers". // There are some intricacies involved with this test. `SeriesLines` and // `SeriesPoints` can both be logged without any associated data (all @@ -50,7 +52,7 @@ fn test_clear_series_points_and_line_impl(two_series_per_entity: bool) { }); for i in 0..32 { - let timepoint = TimePoint::from([(test_context.active_timeline(), i)]); + let timepoint = TimePoint::from([(timeline, i)]); match i { 15 => { @@ -85,6 +87,10 @@ fn test_clear_series_points_and_line_impl(two_series_per_entity: bool) { } } + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); + let allowed_broken_pixels = if two_series_per_entity { 5 } else { 2 }; let view_id = setup_blueprint(&mut test_context); test_context.run_view_ui_and_save_snapshot( @@ -136,6 +142,8 @@ fn test_line_properties() { fn test_line_properties_impl(multiple_properties: bool, multiple_scalars: bool) { let mut test_context = TestContext::new_with_view_class::(); + let timeline = Timeline::log_tick(); + let properties_static = if multiple_properties { re_types::archetypes::SeriesLines::new() .with_widths([4.0, 8.0]) @@ -155,7 +163,7 @@ fn test_line_properties_impl(multiple_properties: bool, multiple_scalars: bool) }); for step in 0..32 { - let timepoint = TimePoint::from([(test_context.active_timeline(), step)]); + let timepoint = TimePoint::from([(timeline, step)]); let properties = if multiple_properties { re_types::archetypes::SeriesLines::new() @@ -181,6 +189,10 @@ fn test_line_properties_impl(multiple_properties: bool, multiple_scalars: bool) }); } + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); + let view_id = setup_blueprint(&mut test_context); let mut name = "line_properties".to_owned(); if multiple_properties { @@ -208,6 +220,8 @@ fn test_per_series_visibility() { ] { let mut test_context = TestContext::new_with_view_class::(); + let timeline = Timeline::log_tick(); + test_context.log_entity("plots", |builder| { builder.with_archetype( RowId::new(), @@ -217,13 +231,17 @@ fn test_per_series_visibility() { }); for step in 0..32 { - let timepoint = TimePoint::from([(test_context.active_timeline(), step)]); + let timepoint = TimePoint::from([(timeline, step)]); let (scalars, _) = scalars_for_properties_test(step, true); test_context.log_entity("plots", |builder| { builder.with_archetype(RowId::new(), timepoint.clone(), &scalars) }); } + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); + let view_id = setup_blueprint(&mut test_context); test_context.run_view_ui_and_save_snapshot(view_id, name, egui::vec2(300.0, 300.0), None); } @@ -253,6 +271,8 @@ fn test_point_properties() { fn test_point_properties_impl(multiple_properties: bool, multiple_scalars: bool) { let mut test_context = TestContext::new_with_view_class::(); + let timeline = Timeline::log_tick(); + let static_props = if multiple_properties { re_types::archetypes::SeriesPoints::new() .with_marker_sizes([4.0, 8.0]) @@ -278,7 +298,7 @@ fn test_point_properties_impl(multiple_properties: bool, multiple_scalars: bool) }); for step in 0..32 { - let timepoint = TimePoint::from([(test_context.active_timeline(), step)]); + let timepoint = TimePoint::from([(timeline, step)]); let properties = if multiple_properties { re_types::archetypes::SeriesPoints::new() @@ -308,6 +328,10 @@ fn test_point_properties_impl(multiple_properties: bool, multiple_scalars: bool) }); } + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); + let view_id = setup_blueprint(&mut test_context); let mut name = "point_properties".to_owned(); if multiple_properties { @@ -399,6 +423,10 @@ fn test_bootstrapped_secondaries_impl(partial_range: bool) { blueprint.add_view_at_root(view) }); + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*Timeline::log_tick().name()); + }); + let name = if partial_range { "bootstrapped_secondaries_partial" } else { diff --git a/crates/viewer/re_view_time_series/tests/blueprint.rs b/crates/viewer/re_view_time_series/tests/blueprint.rs index 6faebe3227fb..14cf51015abb 100644 --- a/crates/viewer/re_view_time_series/tests/blueprint.rs +++ b/crates/viewer/re_view_time_series/tests/blueprint.rs @@ -8,15 +8,17 @@ use re_types::{ blueprint, components, }; use re_view_time_series::TimeSeriesView; -use re_viewer_context::{ViewClass as _, ViewId}; +use re_viewer_context::{BlueprintContext as _, TimeBlueprintExt as _, ViewClass as _, ViewId}; use re_viewport_blueprint::{ViewBlueprint, ViewContents}; #[test] pub fn test_blueprint_overrides_and_defaults_with_time_series() { let mut test_context = TestContext::new_with_view_class::(); + let timeline = re_log_types::Timeline::log_tick(); + for i in 0..32 { - let timepoint = TimePoint::from([(test_context.active_timeline(), i)]); + let timepoint = TimePoint::from([(timeline, i)]); let t = i as f64 / 8.0; test_context.log_entity("plots/sin", |builder| { builder.with_archetype(RowId::new(), timepoint.clone(), &Scalars::single(t.sin())) @@ -26,6 +28,10 @@ pub fn test_blueprint_overrides_and_defaults_with_time_series() { }); } + test_context.with_blueprint_ctx(|ctx| { + ctx.set_timeline(*timeline.name()); + }); + let view_id = setup_blueprint(&mut test_context); test_context.run_view_ui_and_save_snapshot( view_id, diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 81e2ba758f8a..9532d049ed39 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -15,10 +15,11 @@ use re_renderer::WgpuResourcePoolStatistics; use re_smart_channel::{ReceiveSet, SmartChannelSource}; use re_ui::{ContextExt as _, UICommand, UICommandSender as _, UiExt as _, notifications}; use re_viewer_context::{ - AppOptions, AsyncRuntimeHandle, BlueprintUndoState, CommandReceiver, CommandSender, - ComponentUiRegistry, DisplayMode, Item, PlayState, RecordingConfig, RecordingOrTable, - StorageContext, StoreContext, SystemCommand, SystemCommandSender as _, TableStore, ViewClass, - ViewClassRegistry, ViewClassRegistryError, command_channel, + AppOptions, AsyncRuntimeHandle, BlueprintContext, BlueprintUndoState, CommandReceiver, + CommandSender, ComponentUiRegistry, DisplayMode, Item, PlayState, RecordingConfig, + RecordingOrTable, StorageContext, StoreContext, SystemCommand, SystemCommandSender as _, + TableStore, TimeBlueprintExt as _, ViewClass, ViewClassRegistry, ViewClassRegistryError, + command_channel, open_url::{OpenUrlOptions, ViewerOpenUrl, combine_with_base_url}, santitize_file_name, store_hub::{BlueprintPersistence, StoreHub, StoreHubStats}, @@ -820,7 +821,8 @@ impl App { let db = store_hub.entity_db_mut(&store_id); - if store_id.is_blueprint() { + // No need to clear undo buffer if we're just appending static data. + if store_id.is_blueprint() && chunks.iter().any(|c| !c.is_static()) { self.state .blueprint_undo_state .entry(store_id) @@ -913,22 +915,20 @@ impl App { store_id, timeline, time, - pending, } => { - if let Some(rec_cfg) = self.recording_config_mut(store_hub, &store_id) { - let mut time_ctrl = rec_cfg.time_ctrl.write(); - - if pending { - time_ctrl.set_pending_timeline(timeline); - } else { - time_ctrl.set_timeline(timeline); - } + if let Some(blueprint_ctx) = + active_blueprint_ctx(&mut self.state, &self.command_sender, store_hub) + { + blueprint_ctx.set_timeline(timeline); if let Some(time) = time { - time_ctrl.set_time(time); + blueprint_ctx.set_time(time); } - time_ctrl.pause(); + if let Some(rec_cfg) = self.recording_config_mut(store_hub, &store_id) { + let mut time_ctrl = rec_cfg.time_ctrl.write(); + time_ctrl.pause(); + } } else { re_log::debug!( "SystemCommand::SetActiveTime ignored: unknown store ID '{store_id:?}'" @@ -941,11 +941,16 @@ impl App { timeline, time_range, } => { - if let Some(rec_cfg) = self.recording_config_mut(store_hub, &store_id) { - let mut guard = rec_cfg.time_ctrl.write(); - guard.set_timeline(timeline); - guard.set_loop_selection(time_range); - guard.set_looping(re_viewer_context::Looping::Selection); + if let Some(blueprint_ctx) = + active_blueprint_ctx(&mut self.state, &self.command_sender, store_hub) + { + blueprint_ctx.set_timeline(*timeline.name()); + + if let Some(rec_cfg) = self.recording_config_mut(store_hub, &store_id) { + let mut guard = rec_cfg.time_ctrl.write(); + guard.set_loop_selection(time_range); + guard.set_looping(re_viewer_context::Looping::Selection); + } } else { re_log::debug!( "SystemCommand::SetLoopSelection ignored: unknown store ID '{store_id:?}'" @@ -1017,14 +1022,22 @@ impl App { url: url.to_string(), follow: *follow, }; + let Some(blueprint_ctx) = + active_blueprint_ctx(&mut self.state, &self.command_sender, store_hub) + else { + return; + }; + if all_sources.any(|source| source.is_same_ignoring_uri_fragments(&new_source)) { if let Some(entity_db) = store_hub.find_recording_store_by_source(&new_source) { if *follow { - let rec_cfg = self.state.recording_config_mut(entity_db); + let rec_cfg = + self.state.recording_config_mut(entity_db, &blueprint_ctx); let time_ctrl = rec_cfg.time_ctrl.get_mut(); time_ctrl.set_play_state( entity_db.times_per_timeline(), PlayState::Following, + &blueprint_ctx, ); } @@ -1149,9 +1162,8 @@ impl App { self.command_sender .send_system(SystemCommand::SetActiveTime { store_id, - timeline: re_chunk::Timeline::new(timeline, timecell.typ()), - time: Some(timecell.as_i64().into()), - pending: true, + timeline, + time: Some(timecell.value.into()), }); } } @@ -1672,29 +1684,39 @@ impl App { store_context: Option<&StoreContext<'_>>, command: TimeControlCommand, ) { - let Some(entity_db) = store_context.as_ref().map(|ctx| ctx.recording) else { + let Some((entity_db, blueprint_ctx)) = store_context.as_ref().map(|ctx| { + ( + ctx.recording, + AppBlueprintCtx { + command_sender: &self.command_sender, + current_blueprint: ctx.blueprint, + default_blueprint: ctx.default_blueprint, + blueprint_query: self.state.blueprint_query_for_viewer(ctx.blueprint), + }, + ) + }) else { return; }; - let rec_cfg = self.state.recording_config_mut(entity_db); + let rec_cfg = self.state.recording_config_mut(entity_db, &blueprint_ctx); let time_ctrl = rec_cfg.time_ctrl.get_mut(); let times_per_timeline = entity_db.times_per_timeline(); match command { TimeControlCommand::TogglePlayPause => { - time_ctrl.toggle_play_pause(times_per_timeline); + time_ctrl.toggle_play_pause(times_per_timeline, &blueprint_ctx); } TimeControlCommand::Follow => { - time_ctrl.set_play_state(times_per_timeline, PlayState::Following); + time_ctrl.set_play_state(times_per_timeline, PlayState::Following, &blueprint_ctx); } TimeControlCommand::StepBack => { - time_ctrl.step_time_back(times_per_timeline); + time_ctrl.step_time_back(times_per_timeline, &blueprint_ctx); } TimeControlCommand::StepForward => { - time_ctrl.step_time_fwd(times_per_timeline); + time_ctrl.step_time_fwd(times_per_timeline, &blueprint_ctx); } TimeControlCommand::Restart => { - time_ctrl.restart(times_per_timeline); + time_ctrl.restart(times_per_timeline, &blueprint_ctx); } } } @@ -2351,8 +2373,11 @@ impl App { store_hub: &StoreHub, store_id: &StoreId, ) -> Option<&mut RecordingConfig> { - if let Some(entity_db) = store_hub.store_bundle().get(store_id) { - Some(self.state.recording_config_mut(entity_db)) + if let Some(entity_db) = store_hub.store_bundle().get(store_id) + && let Some(blueprint_ctx) = + active_blueprint_ctx(&mut self.state, &self.command_sender, store_hub) + { + Some(self.state.recording_config_mut(entity_db, &blueprint_ctx)) } else { re_log::debug!("Failed to find recording '{store_id:?}' in store hub"); None @@ -2596,6 +2621,25 @@ impl App { self.screenshotter.save(&self.egui_ctx, image); } } + + /// Get a helper struct to interact with the given recording. + pub fn blueprint_ctx<'a>(&'a self, recording_id: &StoreId) -> Option> { + let hub = self.store_hub.as_ref()?; + + let blueprint = hub.active_blueprint_for_app(recording_id.application_id())?; + + let default_blueprint = hub.default_blueprint_for_app(recording_id.application_id()); + + let blueprint_query = + re_chunk::LatestAtQuery::latest(re_viewer_context::blueprint_timeline()); + + Some(AppBlueprintCtx { + command_sender: &self.command_sender, + current_blueprint: blueprint, + default_blueprint, + blueprint_query, + }) + } } #[cfg(target_arch = "wasm32")] @@ -3319,3 +3363,49 @@ async fn async_save_dialog( )?; file_handle.write(&bytes).await.context("Failed to save") } + +pub struct AppBlueprintCtx<'a> { + pub command_sender: &'a CommandSender, + pub current_blueprint: &'a EntityDb, + pub default_blueprint: Option<&'a EntityDb>, + pub blueprint_query: re_chunk::LatestAtQuery, +} + +impl BlueprintContext for AppBlueprintCtx<'_> { + fn command_sender(&self) -> &CommandSender { + self.command_sender + } + + fn current_blueprint(&self) -> &EntityDb { + self.current_blueprint + } + + fn default_blueprint(&self) -> Option<&EntityDb> { + self.default_blueprint + } + + fn blueprint_query(&self) -> &re_chunk::LatestAtQuery { + &self.blueprint_query + } +} + +/// Build a helper struct to interact with the active blueprint. +pub fn active_blueprint_ctx<'a>( + app_state: &mut AppState, + command_sender: &'a CommandSender, + hub: &'a StoreHub, +) -> Option> { + let active_app = hub.active_app()?; + let current_blueprint = hub.active_blueprint_for_app(active_app)?; + + let default_blueprint = hub.default_blueprint_for_app(active_app); + + let blueprint_query = app_state.blueprint_query_for_viewer(current_blueprint); + + Some(AppBlueprintCtx { + command_sender, + current_blueprint, + default_blueprint, + blueprint_query, + }) +} diff --git a/crates/viewer/re_viewer/src/app_blueprint.rs b/crates/viewer/re_viewer/src/app_blueprint.rs index 010605397b27..eaf1b7d14060 100644 --- a/crates/viewer/re_viewer/src/app_blueprint.rs +++ b/crates/viewer/re_viewer/src/app_blueprint.rs @@ -1,18 +1,24 @@ use std::sync::Arc; -use re_chunk::{Chunk, RowId}; +use re_chunk::{Chunk, RowId, TimePoint}; use re_chunk_store::LatestAtQuery; use re_entity_db::EntityDb; use re_log_types::EntityPath; -use re_types::blueprint::{archetypes::PanelBlueprint, components::PanelState}; +use re_types::{ + AsComponents, + blueprint::{ + archetypes::{PanelBlueprint, TimePanelBlueprint}, + components::PanelState, + }, +}; use re_viewer_context::{ - CommandSender, SystemCommand, SystemCommandSender as _, blueprint_timepoint_for_writes, + CommandSender, SystemCommand, SystemCommandSender as _, TIME_PANEL_PATH, + blueprint_timepoint_for_writes, }; const TOP_PANEL_PATH: &str = "top_panel"; const BLUEPRINT_PANEL_PATH: &str = "blueprint_panel"; const SELECTION_PANEL_PATH: &str = "selection_panel"; -const TIME_PANEL_PATH: &str = "time_panel"; /// Blueprint for top-level application pub struct AppBlueprint<'a> { @@ -22,7 +28,7 @@ pub struct AppBlueprint<'a> { overrides: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct PanelStates { pub top: PanelState, pub blueprint: PanelState, @@ -197,19 +203,9 @@ pub fn setup_welcome_screen_blueprint(welcome_screen_blueprint: &mut EntityDb) { (SELECTION_PANEL_PATH, PanelState::Hidden), (TIME_PANEL_PATH, PanelState::Hidden), ] { - let entity_path = EntityPath::from(panel_name); - let timepoint = re_viewer_context::blueprint_timepoint_for_writes(welcome_screen_blueprint); - let chunk = Chunk::builder(entity_path) - .with_archetype( - RowId::new(), - timepoint, - &PanelBlueprint::update_fields().with_state(value), - ) - .build() - // All builtin types, no reason for this to ever fail. - .expect("Failed to build chunk."); + let chunk = get_panel_state_chunk(panel_name, timepoint, value); welcome_screen_blueprint .add_chunk(&Arc::new(chunk)) @@ -227,19 +223,9 @@ impl AppBlueprint<'_> { command_sender: &CommandSender, ) { if let Some(blueprint_db) = self.blueprint_db { - let entity_path = EntityPath::from(panel_name); - let timepoint = blueprint_timepoint_for_writes(blueprint_db); - let chunk = Chunk::builder(entity_path) - .with_archetype( - RowId::new(), - timepoint, - &PanelBlueprint::update_fields().with_state(value), - ) - .build() - // All builtin types, no reason for this to ever fail. - .expect("Failed to build chunk."); + let chunk = get_panel_state_chunk(panel_name, timepoint, value); command_sender.send_system(SystemCommand::AppendToStore( blueprint_db.store_id().clone(), @@ -249,13 +235,35 @@ impl AppBlueprint<'_> { } } +fn get_panel_state_chunk(panel_name: &str, timepoint: TimePoint, value: PanelState) -> Chunk { + let entity_path = EntityPath::from(panel_name); + + let component_update: &dyn AsComponents = if panel_name == TIME_PANEL_PATH { + &TimePanelBlueprint::update_fields().with_state(value) + } else { + &PanelBlueprint::update_fields().with_state(value) + }; + + Chunk::builder(entity_path) + .with_archetype(RowId::new(), timepoint, component_update) + .build() + // All builtin types, no reason for this to ever fail. + .expect("Failed to build chunk.") +} + fn load_panel_state( path: &EntityPath, blueprint_db: &re_entity_db::EntityDb, query: &LatestAtQuery, ) -> Option { re_tracing::profile_function!(); + let descriptor = if path == &TIME_PANEL_PATH.into() { + TimePanelBlueprint::descriptor_state() + } else { + PanelBlueprint::descriptor_state() + }; + blueprint_db - .latest_at_component_quiet::(path, query, &PanelBlueprint::descriptor_state()) + .latest_at_component_quiet::(path, query, &descriptor) .map(|(_index, p)| p) } diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index adebfc1491e0..293810f847a5 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -13,11 +13,11 @@ use re_smart_channel::ReceiveSet; use re_types::blueprint::components::PanelState; use re_ui::{ContextExt as _, UiExt as _}; use re_viewer_context::{ - AppOptions, ApplicationSelectionState, AsyncRuntimeHandle, BlueprintUndoState, CommandSender, - ComponentUiRegistry, DisplayMode, DragAndDropManager, GlobalContext, Item, PlayState, - RecordingConfig, SelectionChange, StorageContext, StoreContext, StoreHub, SystemCommand, - SystemCommandSender as _, TableStore, ViewClassRegistry, ViewStates, ViewerContext, - blueprint_timeline, + AppOptions, ApplicationSelectionState, AsyncRuntimeHandle, BlueprintContext, + BlueprintTimeControl, BlueprintUndoState, CommandSender, ComponentUiRegistry, DisplayMode, + DragAndDropManager, GlobalContext, Item, PlayState, RecordingConfig, SelectionChange, + StorageContext, StoreContext, StoreHub, SystemCommand, SystemCommandSender as _, TableStore, + ViewClassRegistry, ViewStates, ViewerContext, blueprint_timeline, open_url::{self, ViewerOpenUrl}, }; use re_viewport::ViewportUi; @@ -25,8 +25,9 @@ use re_viewport_blueprint::ViewportBlueprint; use re_viewport_blueprint::ui::add_view_or_container_modal_ui; use crate::{ - StartupOptions, app_blueprint::AppBlueprint, event::ViewerEventDispatcher, - navigation::Navigation, open_url_description::ViewerOpenUrlDescription, ui::settings_screen_ui, + StartupOptions, app::AppBlueprintCtx, app_blueprint::AppBlueprint, + event::ViewerEventDispatcher, navigation::Navigation, + open_url_description::ViewerOpenUrlDescription, ui::settings_screen_ui, }; const WATERMARK: bool = false; // Nice for recording media material @@ -39,7 +40,7 @@ pub struct AppState { /// Configuration for the current recording (found in [`EntityDb`]). pub recording_configs: HashMap, - pub blueprint_cfg: RecordingConfig, + pub blueprint_cfg: RecordingConfig, /// Maps blueprint id to the current undo state for it. #[serde(skip)] @@ -311,7 +312,16 @@ impl AppState { .collect::<_>() }; - let rec_cfg = recording_config_entry(recording_configs, recording); + let app_blueprint_ctx = AppBlueprintCtx { + command_sender, + current_blueprint: store_context.blueprint, + default_blueprint: store_context.default_blueprint, + blueprint_query, + }; + let rec_cfg = + recording_config_entry(recording_configs, recording, &app_blueprint_ctx); + let blueprint_query = app_blueprint_ctx.blueprint_query; + let egui_ctx = ui.ctx().clone(); let display_mode = self.navigation.peek(); let ctx = ViewerContext { @@ -722,8 +732,12 @@ impl AppState { self.recording_configs.get(rec_id) } - pub fn recording_config_mut(&mut self, entity_db: &EntityDb) -> &mut RecordingConfig { - recording_config_entry(&mut self.recording_configs, entity_db) + pub fn recording_config_mut( + &mut self, + entity_db: &EntityDb, + blueprint_ctx: &impl BlueprintContext, + ) -> &mut RecordingConfig { + recording_config_entry(&mut self.recording_configs, entity_db, blueprint_ctx) } pub fn cleanup(&mut self, store_hub: &StoreHub) { @@ -795,6 +809,7 @@ fn move_time( // The state diffs are used to trigger callbacks if they are configured. // Unless we have a real recording open, we should not actually trigger any callbacks. should_diff_time_ctrl, + ctx, ); handle_time_ctrl_event(recording, events, &recording_time_ctrl_response); @@ -849,8 +864,12 @@ fn handle_time_ctrl_event( pub(crate) fn recording_config_entry<'cfgs>( configs: &'cfgs mut HashMap, entity_db: &'_ EntityDb, + blueprint_ctx: &'_ impl BlueprintContext, ) -> &'cfgs mut RecordingConfig { - fn new_recording_config(entity_db: &'_ EntityDb) -> RecordingConfig { + fn new_recording_config( + entity_db: &'_ EntityDb, + blueprint_ctx: &'_ impl BlueprintContext, + ) -> RecordingConfig { let play_state = if let Some(data_source) = &entity_db.data_source { match data_source { // Play files from the start by default - it feels nice and alive. @@ -871,19 +890,20 @@ pub(crate) fn recording_config_entry<'cfgs>( PlayState::Following // No known source 🤷‍♂️ }; - let mut rec_cfg = RecordingConfig::default(); + let mut rec_cfg = RecordingConfig::from_blueprint(blueprint_ctx); - rec_cfg - .time_ctrl - .get_mut() - .set_play_state(entity_db.times_per_timeline(), play_state); + rec_cfg.time_ctrl.get_mut().set_play_state( + entity_db.times_per_timeline(), + play_state, + blueprint_ctx, + ); rec_cfg } configs .entry(entity_db.store_id().clone()) - .or_insert_with(|| new_recording_config(entity_db)) + .or_insert_with(|| new_recording_config(entity_db, blueprint_ctx)) } /// Handles all kind of links that can be opened within the viewer. diff --git a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs index 85a135ffd235..bdbd4d2fa96c 100644 --- a/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs +++ b/crates/viewer/re_viewer/src/blueprint/validation_gen/mod.rs @@ -33,6 +33,7 @@ pub use re_types::blueprint::components::RootContainer; pub use re_types::blueprint::components::RowShare; pub use re_types::blueprint::components::SelectedColumns; pub use re_types::blueprint::components::TensorDimensionIndexSlider; +pub use re_types::blueprint::components::TimeCell; pub use re_types::blueprint::components::TimelineName; pub use re_types::blueprint::components::ViewClass; pub use re_types::blueprint::components::ViewFit; @@ -77,6 +78,7 @@ pub fn is_valid_blueprint(blueprint: &EntityDb) -> bool { && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) + && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) && validate_component::(blueprint) diff --git a/crates/viewer/re_viewer/src/ui/share_modal.rs b/crates/viewer/re_viewer/src/ui/share_modal.rs index 8d841f081540..9cdf7b213185 100644 --- a/crates/viewer/re_viewer/src/ui/share_modal.rs +++ b/crates/viewer/re_viewer/src/ui/share_modal.rs @@ -374,16 +374,27 @@ mod tests { use re_chunk::EntityPath; use re_log_types::{AbsoluteTimeRangeF, TimeCell, external::re_tuid}; use re_test_context::TestContext; - use re_viewer_context::{DisplayMode, Item, ItemCollection, open_url::ViewerOpenUrl}; + use re_viewer_context::{ + DisplayMode, Item, ItemCollection, TimeBlueprintExt as _, open_url::ViewerOpenUrl, + }; use crate::ui::ShareModal; #[test] fn test_share_modal() { - let test_ctx = TestContext::new(); + let mut test_ctx = TestContext::new(); let timeline = re_log_types::Timeline::new_timestamp("pictime"); + // Log some entity so our timeline exists. + test_ctx.log_entity(EntityPath::from("points"), |builder| { + builder.with_archetype( + re_chunk::RowId::new(), + [(timeline, re_chunk::TimeInt::ZERO)], + &re_types::archetypes::Points2D::new([(0., 0.), (1., 1.)]), + ) + }); + let selection = Item::from(EntityPath::parse_forgiving("entity/path")); let origin = re_uri::Origin::from_str("rerun+http://example.com").unwrap(); let dataset_id = re_tuid::Tuid::from_u128(0x182342300c5f8c327a7b4a6e5a379ac4); @@ -428,7 +439,9 @@ mod tests { // Set the timeline so it shows up on the dialog. { - test_ctx.set_active_timeline(timeline); + test_ctx.with_blueprint_ctx(|ctx| { + ctx.set_timeline_and_time(*timeline.name(), re_chunk::TimeInt::ZERO); + }); let mut time_ctrl = test_ctx.recording_config.time_ctrl.write(); time_ctrl.set_loop_selection(AbsoluteTimeRangeF::new(0.0, 1000.0)); } diff --git a/crates/viewer/re_viewer/src/web.rs b/crates/viewer/re_viewer/src/web.rs index af0a7277a443..cf1817d0fbd8 100644 --- a/crates/viewer/re_viewer/src/web.rs +++ b/crates/viewer/re_viewer/src/web.rs @@ -11,10 +11,11 @@ use serde::Deserialize; use wasm_bindgen::prelude::*; use re_log::ResultExt as _; -use re_log_types::{TableId, TableMsg}; +use re_log_types::{TableId, TableMsg, TimeReal}; use re_memory::AccountingAllocator; -use re_viewer_context::{AsyncRuntimeHandle, open_url}; +use re_viewer_context::{AsyncRuntimeHandle, TimeBlueprintExt as _, open_url}; +use crate::app::AppBlueprintCtx; use crate::app_state::recording_config_entry; use crate::history::install_popstate_listener; use crate::web_tools::{Callback, JsResultExt as _, StringOrStringArray}; @@ -438,35 +439,24 @@ impl WebHandle { //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) #[wasm_bindgen] pub fn set_active_timeline(&self, recording_id: &str, timeline_name: &str) { - let Some(mut app) = self.runner.app_mut::() else { - return; - }; - let crate::App { - store_hub: Some(hub), - state, - egui_ctx, - .. - } = &mut *app - else { + let Some(app) = self.runner.app_mut::() else { return; }; - let Some(store_id) = store_id_from_recording_id(hub, recording_id) else { + let Some(hub) = &app.store_hub else { return; }; - let Some(recording) = hub.store_bundle().get(&store_id) else { + + let Some(recording_id) = store_id_from_recording_id(hub, recording_id) else { return; }; - let rec_cfg = recording_config_entry(&mut state.recording_configs, recording); - let Some(timeline) = recording.timelines().get(&timeline_name.into()).copied() else { - re_log::warn!("Failed to find timeline '{timeline_name}' in {store_id:?}"); + let Some(ctx) = app.blueprint_ctx(&recording_id) else { return; }; - rec_cfg.time_ctrl.write().set_timeline(timeline); - - egui_ctx.request_repaint(); + ctx.set_timeline(timeline_name.into()); + app.egui_ctx.request_repaint(); } //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) @@ -486,36 +476,24 @@ impl WebHandle { //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) #[wasm_bindgen] pub fn set_time_for_timeline(&self, recording_id: &str, timeline_name: &str, time: f64) { - let Some(mut app) = self.runner.app_mut::() else { - return; - }; - let crate::App { - store_hub: Some(hub), - state, - egui_ctx, - .. - } = &mut *app - else { + let Some(app) = self.runner.app_mut::() else { return; }; - let Some(store_id) = store_id_from_recording_id(hub, recording_id) else { + let Some(hub) = &app.store_hub else { return; }; - let Some(recording) = hub.store_bundle().get(&store_id) else { + + let Some(recording_id) = store_id_from_recording_id(hub, recording_id) else { return; }; - let rec_cfg = recording_config_entry(&mut state.recording_configs, recording); - let Some(timeline) = recording.timelines().get(&timeline_name.into()).copied() else { - re_log::warn!("Failed to find timeline '{timeline_name}' in {store_id:?}"); + + let Some(ctx) = app.blueprint_ctx(&recording_id) else { return; }; - rec_cfg - .time_ctrl - .write() - .set_timeline_and_time(timeline, time); - egui_ctx.request_repaint(); + ctx.set_timeline_and_time(timeline_name.into(), TimeReal::from(time).floor()); + app.egui_ctx.request_repaint(); } //TODO(#10737): we should refer to logical recordings using store id (recording id is ambibuous) @@ -586,6 +564,7 @@ impl WebHandle { store_hub, state, egui_ctx, + command_sender, .. } = &mut *app; @@ -598,7 +577,25 @@ impl WebHandle { let Some(recording) = hub.store_bundle().get(&store_id) else { return; }; - let rec_cfg = recording_config_entry(&mut state.recording_configs, recording); + + let Some(blueprint) = hub.active_blueprint_for_app(store_id.application_id()) else { + return; + }; + + let default_blueprint = hub.default_blueprint_for_app(store_id.application_id()); + + let blueprint_query = + re_chunk::LatestAtQuery::latest(re_viewer_context::blueprint_timeline()); + + // Can't use `app.blueprint_ctx` here because of borrow issues. + let ctx = AppBlueprintCtx { + command_sender, + current_blueprint: blueprint, + default_blueprint, + blueprint_query, + }; + + let rec_cfg = recording_config_entry(&mut state.recording_configs, recording, &ctx); let play_state = if value { re_viewer_context::PlayState::Playing @@ -609,7 +606,7 @@ impl WebHandle { rec_cfg .time_ctrl .write() - .set_play_state(recording.times_per_timeline(), play_state); + .set_play_state(recording.times_per_timeline(), play_state, &ctx); egui_ctx.request_repaint(); } } diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png index 2b352bf06ae2..2b3cc95c9dd1 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Dark/Dataframe.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e144e1e05c6111e4c0c8ee9b4472205368c4502fcaba6f6231fd07f211d8b837 -size 27742 +oid sha256:89674e3114ddac4e468c8b9772af36e321e9d05004e97fd23796f70d696592d0 +size 27743 diff --git a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png index d6c8d6c41a2c..41f7b8084039 100644 --- a/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png +++ b/crates/viewer/re_viewer/tests/snapshots/all_view_selection_uis/Light/Dataframe.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe725d26c347745cdd66d782c3a2f30f6276f176485692765c04c0918f79984e -size 27521 +oid sha256:9a544590a43052920acf75115c7cbd351a036ea349165262c59e248fdf27d890 +size 27530 diff --git a/crates/viewer/re_viewer_context/src/blueprint_helpers.rs b/crates/viewer/re_viewer_context/src/blueprint_helpers.rs index 8ee4d035443f..3735e6adde33 100644 --- a/crates/viewer/re_viewer_context/src/blueprint_helpers.rs +++ b/crates/viewer/re_viewer_context/src/blueprint_helpers.rs @@ -1,6 +1,8 @@ use arrow::array::ArrayRef; -use re_chunk::{RowId, TimelineName}; +use re_chunk::{LatestAtQuery, RowId, TimelineName}; use re_chunk_store::external::re_chunk::Chunk; +use re_entity_db::EntityDb; +use re_global_context::CommandSender; use re_log_types::{EntityPath, StoreId, TimeInt, TimePoint, Timeline}; use re_types::{AsComponents, ComponentBatch, ComponentDescriptor, SerializedComponentBatch}; @@ -32,9 +34,19 @@ impl StoreContext<'_> { } } -impl ViewerContext<'_> { - pub fn save_blueprint_archetype(&self, entity_path: EntityPath, components: &dyn AsComponents) { - let timepoint = self.store_context.blueprint_timepoint_for_writes(); +/// Helper trait for writing & reading blueprints. +pub trait BlueprintContext { + fn command_sender(&self) -> &CommandSender; + + fn current_blueprint(&self) -> &EntityDb; + + fn default_blueprint(&self) -> Option<&EntityDb>; + + fn blueprint_query(&self) -> &LatestAtQuery; + + fn save_blueprint_archetype(&self, entity_path: EntityPath, components: &dyn AsComponents) { + let blueprint = self.current_blueprint(); + let timepoint = blueprint_timepoint_for_writes(blueprint); let chunk = match Chunk::builder(entity_path) .with_archetype(RowId::new(), timepoint.clone(), components) @@ -49,12 +61,12 @@ impl ViewerContext<'_> { self.command_sender() .send_system(SystemCommand::AppendToStore( - self.store_context.blueprint.store_id().clone(), + blueprint.store_id().clone(), vec![chunk], )); } - pub fn save_blueprint_component( + fn save_blueprint_component( &self, entity_path: EntityPath, component_descr: &ComponentDescriptor, @@ -68,12 +80,52 @@ impl ViewerContext<'_> { self.save_serialized_blueprint_component(entity_path, serialized); } - pub fn save_serialized_blueprint_component( + fn save_static_blueprint_component( + &self, + entity_path: EntityPath, + component_descr: &ComponentDescriptor, + component_batch: &dyn ComponentBatch, + ) { + let Some(serialized) = component_batch.serialized(component_descr.clone()) else { + re_log::warn!("could not serialize components with descriptor `{component_descr}`"); + return; + }; + + self.save_serialized_static_blueprint_component(entity_path, serialized); + } + + fn save_serialized_static_blueprint_component( &self, entity_path: EntityPath, component_batch: SerializedComponentBatch, ) { - let timepoint = self.store_context.blueprint_timepoint_for_writes(); + let blueprint = self.current_blueprint(); + + let chunk = match Chunk::builder(entity_path) + .with_serialized_batch(RowId::new(), TimePoint::STATIC, component_batch) + .build() + { + Ok(chunk) => chunk, + Err(err) => { + re_log::error_once!("Failed to create Chunk for blueprint components: {}", err); + return; + } + }; + + self.command_sender() + .send_system(SystemCommand::AppendToStore( + blueprint.store_id().clone(), + vec![chunk], + )); + } + + fn save_serialized_blueprint_component( + &self, + entity_path: EntityPath, + component_batch: SerializedComponentBatch, + ) { + let blueprint = self.current_blueprint(); + let timepoint = blueprint_timepoint_for_writes(blueprint); let chunk = match Chunk::builder(entity_path) .with_serialized_batch(RowId::new(), timepoint.clone(), component_batch) @@ -88,20 +140,22 @@ impl ViewerContext<'_> { self.command_sender() .send_system(SystemCommand::AppendToStore( - self.store_context.blueprint.store_id().clone(), + blueprint.store_id().clone(), vec![chunk], )); } - pub fn save_blueprint_array( + fn save_blueprint_array( &self, entity_path: EntityPath, component_descr: ComponentDescriptor, array: ArrayRef, ) { + let blueprint = self.current_blueprint(); + let timepoint = blueprint_timepoint_for_writes(blueprint); self.append_array_to_store( - self.store_context.blueprint.store_id().clone(), - self.store_context.blueprint_timepoint_for_writes(), + blueprint.store_id().clone(), + timepoint, entity_path, component_descr, array, @@ -109,7 +163,7 @@ impl ViewerContext<'_> { } /// Append an array to the given store. - pub fn append_array_to_store( + fn append_array_to_store( &self, store_id: StoreId, timepoint: TimePoint, @@ -133,20 +187,19 @@ impl ViewerContext<'_> { } /// Queries a raw component from the default blueprint. - pub fn raw_latest_at_in_default_blueprint( + fn raw_latest_at_in_default_blueprint( &self, entity_path: &EntityPath, component_descr: &ComponentDescriptor, ) -> Option { - self.store_context - .default_blueprint? - .latest_at(self.blueprint_query, entity_path, [component_descr]) + self.default_blueprint()? + .latest_at(self.blueprint_query(), entity_path, [component_descr]) .get(component_descr)? .component_batch_raw(component_descr) } /// Resets a blueprint component to the value it had in the default blueprint. - pub fn reset_blueprint_component( + fn reset_blueprint_component( &self, entity_path: EntityPath, component_descr: ComponentDescriptor, @@ -161,15 +214,15 @@ impl ViewerContext<'_> { } /// Clears a component in the blueprint store by logging an empty array if it exists. - pub fn clear_blueprint_component( + fn clear_blueprint_component( &self, entity_path: EntityPath, component_descr: ComponentDescriptor, ) { - let blueprint = &self.store_context.blueprint; + let blueprint = self.current_blueprint(); let Some(datatype) = blueprint - .latest_at(self.blueprint_query, &entity_path, [&component_descr]) + .latest_at(self.blueprint_query(), &entity_path, [&component_descr]) .get(&component_descr) .and_then(|unit| { unit.component_batch_raw(&component_descr) @@ -180,7 +233,7 @@ impl ViewerContext<'_> { return; }; - let timepoint = self.store_context.blueprint_timepoint_for_writes(); + let timepoint = blueprint_timepoint_for_writes(blueprint); let chunk = Chunk::builder(entity_path) .with_row( RowId::new(), @@ -204,4 +257,65 @@ impl ViewerContext<'_> { } } } + + fn clear_static_blueprint_component( + &self, + entity_path: EntityPath, + component_descr: ComponentDescriptor, + ) { + let blueprint = self.current_blueprint(); + + let Some(datatype) = blueprint + .latest_at(self.blueprint_query(), &entity_path, [&component_descr]) + .get(&component_descr) + .and_then(|unit| { + unit.component_batch_raw(&component_descr) + .map(|array| array.data_type().clone()) + }) + else { + // There's no component at this path yet, so there's nothing to clear. + return; + }; + + let chunk = Chunk::builder(entity_path) + .with_row( + RowId::new(), + TimePoint::STATIC, + [( + component_descr, + re_chunk::external::arrow::array::new_empty_array(&datatype), + )], + ) + .build(); + + match chunk { + Ok(chunk) => self + .command_sender() + .send_system(SystemCommand::AppendToStore( + blueprint.store_id().clone(), + vec![chunk], + )), + Err(err) => { + re_log::error_once!("Failed to create Chunk for blueprint component: {}", err); + } + } + } +} + +impl BlueprintContext for ViewerContext<'_> { + fn command_sender(&self) -> &CommandSender { + self.command_sender() + } + + fn current_blueprint(&self) -> &EntityDb { + self.store_context.blueprint + } + + fn default_blueprint(&self) -> Option<&EntityDb> { + self.store_context.default_blueprint + } + + fn blueprint_query(&self) -> &LatestAtQuery { + self.blueprint_query + } } diff --git a/crates/viewer/re_viewer_context/src/component_ui_registry.rs b/crates/viewer/re_viewer_context/src/component_ui_registry.rs index b5188ad868b8..094a8fbdc137 100644 --- a/crates/viewer/re_viewer_context/src/component_ui_registry.rs +++ b/crates/viewer/re_viewer_context/src/component_ui_registry.rs @@ -10,7 +10,10 @@ use re_log_types::{Instance, StoreId}; use re_types::{ComponentDescriptor, ComponentType}; use re_ui::{UiExt as _, UiLayout}; -use crate::{ComponentFallbackProvider, MaybeMutRef, QueryContext, ViewerContext}; +use crate::{ + ComponentFallbackProvider, MaybeMutRef, QueryContext, ViewerContext, + blueprint_helpers::BlueprintContext as _, +}; /// Describes where an edit should be written to if any pub struct EditTarget { diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index 99e6a375f3ee..cb9a4602dec4 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -42,7 +42,7 @@ pub use re_global_context::*; pub use self::{ annotations::{AnnotationMap, Annotations, ResolvedAnnotationInfo, ResolvedAnnotationInfos}, async_runtime_handle::{AsyncRuntimeError, AsyncRuntimeHandle, WasmNotSend}, - blueprint_helpers::{blueprint_timeline, blueprint_timepoint_for_writes}, + blueprint_helpers::{BlueprintContext, blueprint_timeline, blueprint_timepoint_for_writes}, cache::{ Cache, CacheMemoryReport, CacheMemoryReportItem, Caches, ImageDecodeCache, ImageStatsCache, SharablePlayableVideoStream, TensorStatsCache, VideoAssetCache, VideoStreamCache, @@ -71,7 +71,10 @@ pub use self::{ store_hub::StoreHub, tables::{TableStore, TableStores}, tensor::{ImageStats, TensorStats}, - time_control::{Looping, PlayState, TimeControl, TimeControlResponse, TimeView}, + time_control::{ + BlueprintTimeControl, Looping, PlayState, TIME_PANEL_PATH, TimeBlueprintExt, TimeControl, + TimeControlResponse, TimeView, time_panel_blueprint_entity_path, + }, typed_entity_collections::{ IndicatedEntities, MaybeVisualizableEntities, PerVisualizer, VisualizableEntities, }, diff --git a/crates/viewer/re_viewer_context/src/time_control.rs b/crates/viewer/re_viewer_context/src/time_control.rs index ff3e2d4415e0..f1cef38cc615 100644 --- a/crates/viewer/re_viewer_context/src/time_control.rs +++ b/crates/viewer/re_viewer_context/src/time_control.rs @@ -1,16 +1,111 @@ -use std::collections::BTreeMap; +use std::{ + collections::BTreeMap, + ops::{Deref, DerefMut}, +}; use nohash_hasher::IntMap; +use re_types::blueprint::archetypes::TimePanelBlueprint; use vec1::Vec1; -use re_chunk::TimelineName; +use re_chunk::{EntityPath, TimelineName}; use re_entity_db::{TimeCounts, TimelineStats, TimesPerTimeline}; use re_log_types::{ AbsoluteTimeRange, AbsoluteTimeRangeF, Duration, TimeCell, TimeInt, TimeReal, TimeType, Timeline, }; -use crate::NeedsRepaint; +use crate::{NeedsRepaint, blueprint_helpers::BlueprintContext}; + +pub const TIME_PANEL_PATH: &str = "time_panel"; + +pub fn time_panel_blueprint_entity_path() -> EntityPath { + TIME_PANEL_PATH.into() +} + +/// Helper trait to write time panel related blueprint components. +pub trait TimeBlueprintExt { + fn set_timeline_and_time(&self, timeline: TimelineName, time: impl Into); + + fn set_time(&self, time: impl Into); + + fn get_time(&self) -> Option; + + fn set_timeline(&self, timeline: TimelineName); + + fn get_timeline(&self) -> Option; + + /// Replaces the current timeline with the automatic one. + fn clear_timeline(&self); + + fn clear_time(&self); +} + +impl TimeBlueprintExt for T { + fn set_timeline_and_time(&self, timeline: TimelineName, time: impl Into) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_timeline(), + &re_types::blueprint::components::TimelineName::from(timeline.as_str()), + ); + self.set_time(time); + } + + fn set_time(&self, time: impl Into) { + let time: TimeInt = time.into(); + self.save_static_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_time(), + &re_types::blueprint::components::TimeCell(time.as_i64().into()), + ); + } + + fn get_time(&self) -> Option { + let (_, time) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + &TimePanelBlueprint::descriptor_time(), + )?; + + Some(TimeInt::saturated_temporal_i64(time.0.0)) + } + + fn set_timeline(&self, timeline: TimelineName) { + self.save_blueprint_component( + time_panel_blueprint_entity_path(), + &TimePanelBlueprint::descriptor_timeline(), + &re_types::blueprint::components::TimelineName::from(timeline.as_str()), + ); + self.clear_time(); + } + + fn get_timeline(&self) -> Option { + let (_, timeline) = self + .current_blueprint() + .latest_at_component_quiet::( + &time_panel_blueprint_entity_path(), + self.blueprint_query(), + &TimePanelBlueprint::descriptor_timeline(), + )?; + + Some(TimelineName::new(timeline.as_str())) + } + + fn clear_timeline(&self) { + self.clear_blueprint_component( + time_panel_blueprint_entity_path(), + TimePanelBlueprint::descriptor_timeline(), + ); + } + + fn clear_time(&self) { + self.clear_static_blueprint_component( + time_panel_blueprint_entity_path(), + TimePanelBlueprint::descriptor_time(), + ); + } +} /// The time range we are currently zoomed in on. #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)] @@ -201,6 +296,7 @@ impl Default for TimeControl { } } +#[must_use] pub struct TimeControlResponse { pub needs_repaint: NeedsRepaint, @@ -236,19 +332,104 @@ impl TimeControlResponse { } impl TimeControl { + pub fn from_blueprint(blueprint_ctx: &impl BlueprintContext) -> Self { + let mut this = Self::default(); + + this.update_from_blueprint(blueprint_ctx, None); + + this + } + /// Move the time forward (if playing), and perhaps pause if we've reached the end. /// /// If `should_diff_state` is true, then the response also contains any changes in state /// between last frame and the current one. - #[must_use] + /// + /// This will read and write the current time & timeline from the + /// given blueprint context. pub fn update( &mut self, times_per_timeline: &TimesPerTimeline, stable_dt: f32, more_data_is_coming: bool, should_diff_state: bool, + blueprint_ctx: &impl BlueprintContext, + ) -> TimeControlResponse { + self.update_inner( + times_per_timeline, + stable_dt, + more_data_is_coming, + should_diff_state, + Some(blueprint_ctx), + ) + } + + /// Sets the current time. + /// + /// If `blueprint_ctx` is some, this will also update the time stored in + /// the blueprint if `time_int` has changed. + fn update_time(&mut self, blueprint_ctx: Option<&impl BlueprintContext>, time: TimeReal) { + let time_int = time.floor(); + if self.time_int() != Some(time_int) + && let Some(blueprint_ctx) = blueprint_ctx + { + blueprint_ctx.set_time(time_int); + } + + self.set_time(time); + } + + /// Read from the time panel blueprint and update the state from that. + /// + /// If `times_per_timeline` is some this will also make sure we are on + /// a valid timeline. + pub fn update_from_blueprint( + &mut self, + blueprint_ctx: &impl BlueprintContext, + times_per_timeline: Option<&TimesPerTimeline>, + ) { + if let Some(timeline) = blueprint_ctx.get_timeline() { + if matches!(self.timeline, ActiveTimeline::Auto(_)) + || timeline.as_str() != self.timeline().name().as_str() + { + self.timeline = ActiveTimeline::Pending(Timeline::new_sequence(timeline)); + } + } else { + self.timeline = ActiveTimeline::Auto(*self.timeline()); + } + + if let Some(times_per_timeline) = times_per_timeline { + self.select_a_valid_timeline(times_per_timeline); + } + + if let Some(time) = blueprint_ctx.get_time() { + if self.time_int() != Some(time) { + self.set_time(time.into()); + } + } + // If the blueprint time wasn't set, but the current state's time was, we likely just switched timelines, so restore that timeline's time. + else if let Some(state) = self.states.get(self.timeline().name()) { + blueprint_ctx.set_time(state.current.time.floor()); + } + } + + /// See [`Self::update`]. + /// + /// If `blueprint_ctx` is some, this will read and write to the + /// time panel blueprint. + fn update_inner( + &mut self, + times_per_timeline: &TimesPerTimeline, + stable_dt: f32, + more_data_is_coming: bool, + should_diff_state: bool, + blueprint_ctx: Option<&impl BlueprintContext>, ) -> TimeControlResponse { - self.select_a_valid_timeline(times_per_timeline); + if let Some(blueprint_ctx) = blueprint_ctx { + self.update_from_blueprint(blueprint_ctx, Some(times_per_timeline)); + } else { + self.select_a_valid_timeline(times_per_timeline); + } let Some(full_valid_range) = self.full_valid_range(times_per_timeline) else { return TimeControlResponse::no_repaint(); // we have no data on this timeline yet, so bail @@ -281,7 +462,7 @@ impl TimeControl { if self.looping == Looping::Off && full_valid_range.max() <= state.current.time { // We've reached the end of the data - state.current.time = full_valid_range.max().into(); + self.update_time(blueprint_ctx, full_valid_range.max().into()); if more_data_is_coming { // then let's wait for it without pausing! @@ -292,6 +473,8 @@ impl TimeControl { } } + let mut new_time = state.current.time; + let loop_range = match self.looping { Looping::Off => None, Looping::Selection => state.current.loop_selection, @@ -300,17 +483,17 @@ impl TimeControl { match self.timeline.typ() { TimeType::Sequence => { - state.current.time += TimeReal::from(state.current.fps * dt); + new_time += TimeReal::from(state.current.fps * dt); } TimeType::DurationNs | TimeType::TimestampNs => { - state.current.time += TimeReal::from(Duration::from_secs(dt)); + new_time += TimeReal::from(Duration::from_secs(dt)); } } if let Some(loop_range) = loop_range - && loop_range.max < state.current.time + && loop_range.max < new_time { - state.current.time = loop_range.min; // loop! + new_time = loop_range.min; // loop! } // Confine cursor to valid ranges. @@ -323,28 +506,21 @@ impl TimeControl { // The valid range index that the time cursor is either contained in or just behind. let next_valid_range_idx = - valid_ranges.partition_point(|range| range.max() < state.current.time); + valid_ranges.partition_point(|range| range.max() < new_time); let clamp_range = valid_ranges .get(next_valid_range_idx) .unwrap_or_else(|| valid_ranges.last()); - state.current.time = state - .current - .time - .clamp(clamp_range.min().into(), clamp_range.max().into()); + new_time = new_time.clamp(clamp_range.min().into(), clamp_range.max().into()); } + self.update_time(blueprint_ctx, new_time); + NeedsRepaint::Yes } PlayState::Following => { // Set the time to the max: - match self.states.entry(*self.timeline.name()) { - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(TimeStateEntry::new(full_valid_range.max())); - } - std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().current.time = full_valid_range.max().into(); - } - } + self.update_time(blueprint_ctx, full_valid_range.max().into()); + NeedsRepaint::No // no need for request_repaint - we already repaint when new data arrives } }; @@ -423,7 +599,12 @@ impl TimeControl { } } - pub fn set_play_state(&mut self, times_per_timeline: &TimesPerTimeline, play_state: PlayState) { + fn set_play_state_inner( + &mut self, + times_per_timeline: &TimesPerTimeline, + play_state: PlayState, + blueprint_ctx: Option<&impl BlueprintContext>, + ) { match play_state { PlayState::Paused => { self.playing = false; @@ -434,15 +615,12 @@ impl TimeControl { // Start from beginning if we are at the end: if let Some(timeline_stats) = times_per_timeline.get(self.timeline.name()) { - if let Some(state) = self.states.get_mut(self.timeline.name()) { - if max(&timeline_stats.per_time) <= state.current.time { - state.current.time = min(&timeline_stats.per_time).into(); - } - } else { - self.states.insert( - *self.timeline.name(), - TimeStateEntry::new(min(&timeline_stats.per_time)), - ); + if self + .states + .get(self.timeline.name()) + .is_none_or(|state| max(&timeline_stats.per_time) <= state.current.time) + { + self.update_time(blueprint_ctx, min(&timeline_stats.per_time).into()); } } } @@ -452,24 +630,30 @@ impl TimeControl { if let Some(timeline_stats) = times_per_timeline.get(self.timeline.name()) { // Set the time to the max: - match self.states.entry(*self.timeline.name()) { - std::collections::btree_map::Entry::Vacant(entry) => { - entry.insert(TimeStateEntry::new(max(&timeline_stats.per_time))); - } - std::collections::btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().current.time = max(&timeline_stats.per_time).into(); - } - } + self.update_time(blueprint_ctx, max(&timeline_stats.per_time).into()); } } } } + pub fn set_play_state( + &mut self, + times_per_timeline: &TimesPerTimeline, + play_state: PlayState, + blueprint_ctx: &impl BlueprintContext, + ) { + self.set_play_state_inner(times_per_timeline, play_state, Some(blueprint_ctx)); + } + pub fn pause(&mut self) { self.playing = false; } - pub fn step_time_back(&mut self, times_per_timeline: &TimesPerTimeline) { + pub fn step_time_back( + &mut self, + times_per_timeline: &TimesPerTimeline, + ctx: &impl TimeBlueprintExt, + ) { let Some(timeline_stats) = times_per_timeline.get(self.timeline().name()) else { return; }; @@ -483,11 +667,15 @@ impl TimeControl { } else { step_back_time(time, &timeline_stats.per_time).into() }; - self.set_time(new_time); + ctx.set_time(new_time.floor()); } } - pub fn step_time_fwd(&mut self, times_per_timeline: &TimesPerTimeline) { + pub fn step_time_fwd( + &mut self, + times_per_timeline: &TimesPerTimeline, + ctx: &impl TimeBlueprintExt, + ) { let Some(stats) = times_per_timeline.get(self.timeline().name()) else { return; }; @@ -501,20 +689,34 @@ impl TimeControl { } else { step_fwd_time(time, &stats.per_time).into() }; - self.set_time(new_time); + ctx.set_time(new_time.floor()); } } - pub fn restart(&mut self, times_per_timeline: &TimesPerTimeline) { - if let Some(stats) = times_per_timeline.get(self.timeline.name()) - && let Some(state) = self.states.get_mut(self.timeline.name()) - { - state.current.time = min(&stats.per_time).into(); + pub fn restart( + &mut self, + times_per_timeline: &TimesPerTimeline, + blueprint_ctx: &impl BlueprintContext, + ) { + if let Some(stats) = times_per_timeline.get(self.timeline.name()) { + self.update_time(Some(blueprint_ctx), min(&stats.per_time).into()); self.following = false; } } - pub fn toggle_play_pause(&mut self, times_per_timeline: &TimesPerTimeline) { + pub fn toggle_play_pause( + &mut self, + times_per_timeline: &TimesPerTimeline, + blueprint_ctx: &impl BlueprintContext, + ) { + self.toggle_play_pause_inner(times_per_timeline, Some(blueprint_ctx)); + } + + fn toggle_play_pause_inner( + &mut self, + times_per_timeline: &TimesPerTimeline, + blueprint_ctx: Option<&impl BlueprintContext>, + ) { #[allow(clippy::collapsible_else_if)] if self.playing { self.pause(); @@ -546,16 +748,16 @@ impl TimeControl { && let Some(state) = self.states.get_mut(self.timeline.name()) && max(&stats.per_time) <= state.current.time { - state.current.time = min(&stats.per_time).into(); + self.update_time(blueprint_ctx, min(&stats.per_time).into()); self.playing = true; self.following = false; return; } if self.following { - self.set_play_state(times_per_timeline, PlayState::Following); + self.set_play_state_inner(times_per_timeline, PlayState::Following, blueprint_ctx); } else { - self.set_play_state(times_per_timeline, PlayState::Playing); + self.set_play_state_inner(times_per_timeline, PlayState::Playing, blueprint_ctx); } } } @@ -605,8 +807,11 @@ impl TimeControl { // If it's pending never automatically refresh it. ActiveTimeline::Pending(timeline) => { // If the pending timeline is valid, it shouldn't be pending anymore. - if is_timeline_valid(timeline, times_per_timeline) { - self.set_timeline(*timeline); + if let Some(timeline) = times_per_timeline + .timelines() + .find(|t| t.name() == timeline.name()) + { + self.timeline = ActiveTimeline::UserEdited(*timeline); } false @@ -630,14 +835,6 @@ impl TimeControl { self.timeline.typ() } - pub fn set_timeline(&mut self, timeline: Timeline) { - self.timeline = ActiveTimeline::UserEdited(timeline); - } - - pub fn set_pending_timeline(&mut self, timeline: Timeline) { - self.timeline = ActiveTimeline::Pending(timeline); - } - /// Mark up a time range as valid. /// /// Everything outside can still be navigated to, but will be considered potentially lacking some data and therefore "invalid". @@ -825,28 +1022,14 @@ impl TimeControl { matches!(self.timeline, ActiveTimeline::Pending(_)) } - pub fn set_timeline_and_time(&mut self, timeline: Timeline, time: impl Into) { - self.timeline = ActiveTimeline::UserEdited(timeline); - self.set_time(time); - } - pub fn time_for_timeline(&self, timeline: TimelineName) -> Option { self.states.get(&timeline).map(|state| state.current.time) } - pub fn set_time_for_timeline(&mut self, timeline: Timeline, time: impl Into) { - let time = time.into(); - - self.states - .entry(*timeline.name()) - .or_insert_with(|| TimeStateEntry::new(time)) - .current - .time = time; - } - - pub fn set_time(&mut self, time: impl Into) { - let time = time.into(); - + /// Set the time. + /// + /// This does not affect the time stored in blueprints. + fn set_time(&mut self, time: TimeReal) { self.states .entry(*self.timeline.name()) .or_insert_with(|| TimeStateEntry::new(time)) @@ -878,6 +1061,72 @@ impl TimeControl { } } +/// Same as [`TimeControl`] but exposes some extra functions +/// to mutate inner state. +/// +/// Used for the blueprint inspector panel. +#[derive(Default, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +#[serde(default)] +pub struct BlueprintTimeControl(TimeControl); + +impl Deref for BlueprintTimeControl { + type Target = TimeControl; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BlueprintTimeControl { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl BlueprintTimeControl { + /// Move the time forward (if playing), and perhaps pause if we've reached the end. + /// + /// If `should_diff_state` is true, then the response also contains any changes in state + /// between last frame and the current one. + pub fn update( + &mut self, + times_per_timeline: &TimesPerTimeline, + stable_dt: f32, + more_data_is_coming: bool, + should_diff_state: bool, + ) -> TimeControlResponse { + self.update_inner( + times_per_timeline, + stable_dt, + more_data_is_coming, + should_diff_state, + None::<&crate::ViewerContext<'_>>, + ) + } + + pub fn set_time(&mut self, time: impl Into) { + self.0.set_time(time.into()); + } + + pub fn set_timeline(&mut self, timeline: Timeline) { + self.timeline = ActiveTimeline::UserEdited(timeline); + } + + pub fn set_play_state(&mut self, times_per_timeline: &TimesPerTimeline, play_state: PlayState) { + self.0.set_play_state_inner( + times_per_timeline, + play_state, + None::<&crate::ViewerContext<'_>>, + ); + } + + pub fn toggle_play_pause(&mut self, times_per_timeline: &TimesPerTimeline) { + self.toggle_play_pause_inner(times_per_timeline, None::<&crate::ViewerContext<'_>>); + } +} + fn min(values: &TimeCounts) -> TimeInt { *values.keys().next().unwrap_or(&TimeInt::MIN) } diff --git a/crates/viewer/re_viewer_context/src/view/view_context.rs b/crates/viewer/re_viewer_context/src/view/view_context.rs index 691e04c119fb..ccd536ba57c1 100644 --- a/crates/viewer/re_viewer_context/src/view/view_context.rs +++ b/crates/viewer/re_viewer_context/src/view/view_context.rs @@ -3,7 +3,9 @@ use re_log_types::{EntityPath, TimePoint}; use re_query::StorageEngineReadGuard; use re_types::{AsComponents, ComponentBatch, ComponentDescriptor, ViewClassIdentifier}; -use crate::{DataQueryResult, DataResult, QueryContext, ViewId}; +use crate::{ + DataQueryResult, DataResult, QueryContext, ViewId, blueprint_helpers::BlueprintContext as _, +}; use super::VisualizerCollection; diff --git a/crates/viewer/re_viewer_context/src/view/view_query.rs b/crates/viewer/re_viewer_context/src/view/view_query.rs index f704611064c2..b03280f45092 100644 --- a/crates/viewer/re_viewer_context/src/view/view_query.rs +++ b/crates/viewer/re_viewer_context/src/view/view_query.rs @@ -15,6 +15,7 @@ use re_types::{ use crate::{ DataResultTree, QueryRange, ViewHighlights, ViewId, ViewSystemIdentifier, ViewerContext, + blueprint_helpers::BlueprintContext as _, }; /// Path to a specific entity in a specific store used for overrides. diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index b607d77b648c..446b3a741dfd 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -17,7 +17,9 @@ use crate::{ SystemCommandSender as _, TimeControl, ViewClassRegistry, ViewId, query_context::DataQueryResult, }; -use crate::{GlobalContext, Item, StorageContext, StoreHub}; +use crate::{ + BlueprintContext, BlueprintTimeControl, GlobalContext, Item, StorageContext, StoreHub, +}; /// Common things needed by many parts of the viewer. pub struct ViewerContext<'a> { @@ -50,7 +52,7 @@ pub struct ViewerContext<'a> { pub rec_cfg: &'a RecordingConfig, /// UI config for the current blueprint. - pub blueprint_cfg: &'a RecordingConfig, + pub blueprint_cfg: &'a RecordingConfig, /// The blueprint query used for resolving blueprint in this frame pub blueprint_query: &'a LatestAtQuery, @@ -425,7 +427,15 @@ impl ViewerContext<'_> { /// UI config for the current recording (found in [`EntityDb`]). #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(default)] -pub struct RecordingConfig { +pub struct RecordingConfig { /// The current time of the time panel, how fast it is moving, etc. - pub time_ctrl: RwLock, + pub time_ctrl: RwLock, +} + +impl RecordingConfig { + pub fn from_blueprint(blueprint_ctx: &impl BlueprintContext) -> Self { + Self { + time_ctrl: RwLock::new(TimeControl::from_blueprint(blueprint_ctx)), + } + } } diff --git a/crates/viewer/re_viewport_blueprint/src/container.rs b/crates/viewer/re_viewport_blueprint/src/container.rs index ff5229c8cbf9..f79b9e06fcca 100644 --- a/crates/viewer/re_viewport_blueprint/src/container.rs +++ b/crates/viewer/re_viewport_blueprint/src/container.rs @@ -13,7 +13,9 @@ use re_types::blueprint::components::{ }; use re_types::components::Name; use re_types::{Archetype as _, components::Visible}; -use re_viewer_context::{ContainerId, Contents, ContentsName, ViewId, ViewerContext}; +use re_viewer_context::{ + BlueprintContext as _, ContainerId, Contents, ContentsName, ViewId, ViewerContext, +}; /// The native version of a [`re_types::blueprint::archetypes::ContainerBlueprint`]. /// diff --git a/crates/viewer/re_viewport_blueprint/src/view.rs b/crates/viewer/re_viewport_blueprint/src/view.rs index 4a08e6bd165a..170c17941bc4 100644 --- a/crates/viewer/re_viewport_blueprint/src/view.rs +++ b/crates/viewer/re_viewport_blueprint/src/view.rs @@ -14,7 +14,7 @@ use re_types::{ }; use re_types_core::Archetype as _; use re_viewer_context::{ - ContentsName, QueryRange, RecommendedView, StoreContext, SystemCommand, + BlueprintContext as _, ContentsName, QueryRange, RecommendedView, StoreContext, SystemCommand, SystemCommandSender as _, ViewClass, ViewClassRegistry, ViewContext, ViewId, ViewState, ViewStates, ViewerContext, }; diff --git a/crates/viewer/re_viewport_blueprint/src/view_properties.rs b/crates/viewer/re_viewport_blueprint/src/view_properties.rs index fe4987adf1e8..759f0310c5a4 100644 --- a/crates/viewer/re_viewport_blueprint/src/view_properties.rs +++ b/crates/viewer/re_viewport_blueprint/src/view_properties.rs @@ -5,8 +5,9 @@ use re_types::{ Archetype, ArchetypeName, ComponentBatch, ComponentDescriptor, DeserializationError, }; use re_viewer_context::{ - ComponentFallbackError, ComponentFallbackProvider, QueryContext, ViewContext, ViewId, - ViewSystemExecutionError, ViewerContext, external::re_entity_db::EntityTree, + BlueprintContext as _, ComponentFallbackError, ComponentFallbackProvider, QueryContext, + ViewContext, ViewId, ViewSystemExecutionError, ViewerContext, + external::re_entity_db::EntityTree, }; #[derive(thiserror::Error, Debug)] diff --git a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs index 2961016cbb0a..1705795c9c01 100644 --- a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs +++ b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs @@ -24,7 +24,8 @@ use re_types::{ Archetype as _, ViewClassIdentifier, blueprint::components::ViewerRecommendationHash, }; use re_viewer_context::{ - ContainerId, Contents, Item, ViewId, ViewerContext, VisitorControlFlow, blueprint_id_to_tile_id, + BlueprintContext as _, ContainerId, Contents, Item, ViewId, ViewerContext, VisitorControlFlow, + blueprint_id_to_tile_id, }; use crate::{VIEWPORT_PATH, ViewBlueprint, ViewportCommand, container::ContainerBlueprint}; diff --git a/examples/python/blueprint/blueprint.py b/examples/python/blueprint/blueprint.py index 5e6a2f861ad9..d5088a40ede9 100755 --- a/examples/python/blueprint/blueprint.py +++ b/examples/python/blueprint/blueprint.py @@ -39,20 +39,26 @@ def main() -> None: ), rrb.BlueprintPanel(state="collapsed"), rrb.SelectionPanel(state="collapsed"), - rrb.TimePanel(state="collapsed"), + rrb.TimePanel(state="collapsed", timeline="custom", sequence_cursor=15), auto_views=args.auto_views, ) rr.init("rerun_example_blueprint", spawn=True, default_blueprint=blueprint) + rr.set_time("custom", sequence=0) + img = np.zeros([128, 128, 3], dtype="uint8") for i in range(8): img[(i * 16) + 4 : (i * 16) + 12, :] = (0, 0, 200) rr.log("image", rr.Image(img)) + + rr.set_time("custom", sequence=10) rr.log( "rect/0", rr.Boxes2D(mins=[16, 16], sizes=[64, 64], labels="Rect0", colors=(255, 0, 0)), ) + + rr.set_time("custom", sequence=20) rr.log( "rect/1", rr.Boxes2D(mins=[48, 48], sizes=[64, 64], labels="Rect1", colors=(0, 255, 0)), diff --git a/rerun_cpp/src/rerun/blueprint/archetypes.hpp b/rerun_cpp/src/rerun/blueprint/archetypes.hpp index f664cd1d0270..c06b8be4a497 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes.hpp @@ -23,6 +23,7 @@ #include "blueprint/archetypes/tensor_slice_selection.hpp" #include "blueprint/archetypes/tensor_view_fit.hpp" #include "blueprint/archetypes/time_axis.hpp" +#include "blueprint/archetypes/time_panel_blueprint.hpp" #include "blueprint/archetypes/view_blueprint.hpp" #include "blueprint/archetypes/view_contents.hpp" #include "blueprint/archetypes/viewport_blueprint.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes index f97040f58e5e..7671167e6a5d 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/archetypes/.gitattributes @@ -43,6 +43,8 @@ tensor_view_fit.cpp linguist-generated=true tensor_view_fit.hpp linguist-generated=true time_axis.cpp linguist-generated=true time_axis.hpp linguist-generated=true +time_panel_blueprint.cpp linguist-generated=true +time_panel_blueprint.hpp linguist-generated=true view_blueprint.cpp linguist-generated=true view_blueprint.hpp linguist-generated=true view_contents.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/panel_blueprint.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/panel_blueprint.hpp index bacb5754ecbf..11b051e4bf00 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/panel_blueprint.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes/panel_blueprint.hpp @@ -20,7 +20,7 @@ namespace rerun::blueprint::archetypes { /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** /// struct PanelBlueprint { - /// Current state of the panels. + /// Current state of the panel. std::optional state; public: @@ -48,7 +48,7 @@ namespace rerun::blueprint::archetypes { /// Clear all the fields of a `PanelBlueprint`. static PanelBlueprint clear_fields(); - /// Current state of the panels. + /// Current state of the panel. PanelBlueprint with_state(const rerun::blueprint::components::PanelState& _state) && { state = ComponentBatch::from_loggable(_state, Descriptor_state).value_or_throw(); return std::move(*this); diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.cpp new file mode 100644 index 000000000000..4a594120c7f7 --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.cpp @@ -0,0 +1,74 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs". + +#include "time_panel_blueprint.hpp" + +#include "../../collection_adapter_builtins.hpp" + +namespace rerun::blueprint::archetypes { + TimePanelBlueprint TimePanelBlueprint::clear_fields() { + auto archetype = TimePanelBlueprint(); + archetype.state = + ComponentBatch::empty(Descriptor_state) + .value_or_throw(); + archetype.timeline = + ComponentBatch::empty(Descriptor_timeline) + .value_or_throw(); + archetype.time = + ComponentBatch::empty(Descriptor_time) + .value_or_throw(); + return archetype; + } + + Collection TimePanelBlueprint::columns(const Collection& lengths_) { + std::vector columns; + columns.reserve(3); + if (state.has_value()) { + columns.push_back(state.value().partitioned(lengths_).value_or_throw()); + } + if (timeline.has_value()) { + columns.push_back(timeline.value().partitioned(lengths_).value_or_throw()); + } + if (time.has_value()) { + columns.push_back(time.value().partitioned(lengths_).value_or_throw()); + } + return columns; + } + + Collection TimePanelBlueprint::columns() { + if (state.has_value()) { + return columns(std::vector(state.value().length(), 1)); + } + if (timeline.has_value()) { + return columns(std::vector(timeline.value().length(), 1)); + } + if (time.has_value()) { + return columns(std::vector(time.value().length(), 1)); + } + return Collection(); + } +} // namespace rerun::blueprint::archetypes + +namespace rerun { + + Result> + AsComponents::as_batches( + const blueprint::archetypes::TimePanelBlueprint& archetype + ) { + using namespace blueprint::archetypes; + std::vector cells; + cells.reserve(3); + + if (archetype.state.has_value()) { + cells.push_back(archetype.state.value()); + } + if (archetype.timeline.has_value()) { + cells.push_back(archetype.timeline.value()); + } + if (archetype.time.has_value()) { + cells.push_back(archetype.time.value()); + } + + return rerun::take_ownership(std::move(cells)); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.hpp new file mode 100644 index 000000000000..736f9a4ff72a --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/archetypes/time_panel_blueprint.hpp @@ -0,0 +1,122 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs". + +#pragma once + +#include "../../blueprint/components/panel_state.hpp" +#include "../../blueprint/components/time_cell.hpp" +#include "../../blueprint/components/timeline_name.hpp" +#include "../../collection.hpp" +#include "../../component_batch.hpp" +#include "../../component_column.hpp" +#include "../../result.hpp" + +#include +#include +#include +#include + +namespace rerun::blueprint::archetypes { + /// **Archetype**: Time panel specific state. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct TimePanelBlueprint { + /// Current state of the panel. + std::optional state; + + /// What timeline the panel is on. + std::optional timeline; + + /// What time the time cursor should be on. + std::optional time; + + public: + /// The name of the archetype as used in `ComponentDescriptor`s. + static constexpr const char ArchetypeName[] = + "rerun.blueprint.archetypes.TimePanelBlueprint"; + + /// `ComponentDescriptor` for the `state` field. + static constexpr auto Descriptor_state = ComponentDescriptor( + ArchetypeName, "TimePanelBlueprint:state", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `timeline` field. + static constexpr auto Descriptor_timeline = ComponentDescriptor( + ArchetypeName, "TimePanelBlueprint:timeline", + Loggable::ComponentType + ); + /// `ComponentDescriptor` for the `time` field. + static constexpr auto Descriptor_time = ComponentDescriptor( + ArchetypeName, "TimePanelBlueprint:time", + Loggable::ComponentType + ); + + public: + TimePanelBlueprint() = default; + TimePanelBlueprint(TimePanelBlueprint&& other) = default; + TimePanelBlueprint(const TimePanelBlueprint& other) = default; + TimePanelBlueprint& operator=(const TimePanelBlueprint& other) = default; + TimePanelBlueprint& operator=(TimePanelBlueprint&& other) = default; + + /// Update only some specific fields of a `TimePanelBlueprint`. + static TimePanelBlueprint update_fields() { + return TimePanelBlueprint(); + } + + /// Clear all the fields of a `TimePanelBlueprint`. + static TimePanelBlueprint clear_fields(); + + /// Current state of the panel. + TimePanelBlueprint with_state(const rerun::blueprint::components::PanelState& _state) && { + state = ComponentBatch::from_loggable(_state, Descriptor_state).value_or_throw(); + return std::move(*this); + } + + /// What timeline the panel is on. + TimePanelBlueprint with_timeline(const rerun::blueprint::components::TimelineName& _timeline + ) && { + timeline = + ComponentBatch::from_loggable(_timeline, Descriptor_timeline).value_or_throw(); + return std::move(*this); + } + + /// What time the time cursor should be on. + TimePanelBlueprint with_time(const rerun::blueprint::components::TimeCell& _time) && { + time = ComponentBatch::from_loggable(_time, Descriptor_time).value_or_throw(); + return std::move(*this); + } + + /// Partitions the component data into multiple sub-batches. + /// + /// Specifically, this transforms the existing `ComponentBatch` data into `ComponentColumn`s + /// instead, via `ComponentBatch::partitioned`. + /// + /// This makes it possible to use `RecordingStream::send_columns` to send columnar data directly into Rerun. + /// + /// The specified `lengths` must sum to the total length of the component batch. + Collection columns(const Collection& lengths_); + + /// Partitions the component data into unit-length sub-batches. + /// + /// This is semantically similar to calling `columns` with `std::vector(n, 1)`, + /// where `n` is automatically guessed. + Collection columns(); + }; + +} // namespace rerun::blueprint::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> as_batches( + const blueprint::archetypes::TimePanelBlueprint& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/blueprint/components.hpp b/rerun_cpp/src/rerun/blueprint/components.hpp index ef00128eaff5..c695ba7bfdaf 100644 --- a/rerun_cpp/src/rerun/blueprint/components.hpp +++ b/rerun_cpp/src/rerun/blueprint/components.hpp @@ -31,6 +31,7 @@ #include "blueprint/components/row_share.hpp" #include "blueprint/components/selected_columns.hpp" #include "blueprint/components/tensor_dimension_index_slider.hpp" +#include "blueprint/components/time_cell.hpp" #include "blueprint/components/timeline_name.hpp" #include "blueprint/components/view_class.hpp" #include "blueprint/components/view_fit.hpp" diff --git a/rerun_cpp/src/rerun/blueprint/components/.gitattributes b/rerun_cpp/src/rerun/blueprint/components/.gitattributes index e83d880b6e4d..9b4a6e5ccede 100644 --- a/rerun_cpp/src/rerun/blueprint/components/.gitattributes +++ b/rerun_cpp/src/rerun/blueprint/components/.gitattributes @@ -37,6 +37,7 @@ root_container.hpp linguist-generated=true row_share.hpp linguist-generated=true selected_columns.hpp linguist-generated=true tensor_dimension_index_slider.hpp linguist-generated=true +time_cell.hpp linguist-generated=true timeline_name.hpp linguist-generated=true view_class.hpp linguist-generated=true view_fit.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/blueprint/components/time_cell.hpp b/rerun_cpp/src/rerun/blueprint/components/time_cell.hpp new file mode 100644 index 000000000000..419ff4e1565e --- /dev/null +++ b/rerun_cpp/src/rerun/blueprint/components/time_cell.hpp @@ -0,0 +1,76 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs". + +#pragma once + +#include "../../datatypes/time_int.hpp" +#include "../../result.hpp" + +#include +#include + +namespace rerun::blueprint::components { + /// **Component**: A reference to a time. + /// + /// ⚠ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + /// + struct TimeCell { + rerun::datatypes::TimeInt time; + + public: + TimeCell() = default; + + TimeCell(rerun::datatypes::TimeInt time_) : time(time_) {} + + TimeCell& operator=(rerun::datatypes::TimeInt time_) { + time = time_; + return *this; + } + + TimeCell(int64_t value_) : time(value_) {} + + TimeCell& operator=(int64_t value_) { + time = value_; + return *this; + } + + /// Cast to the underlying TimeInt datatype + operator rerun::datatypes::TimeInt() const { + return time; + } + }; +} // namespace rerun::blueprint::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::TimeInt) == sizeof(blueprint::components::TimeCell)); + + /// \private + template <> + struct Loggable { + static constexpr std::string_view ComponentType = "rerun.blueprint.components.TimeCell"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::blueprint:: components::TimeCell` into an arrow array. + static Result> to_arrow( + const blueprint::components::TimeCell* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->time, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_py/rerun_sdk/rerun/blueprint/api.py b/rerun_py/rerun_sdk/rerun/blueprint/api.py index 1ed4217130f6..0a5268e20dac 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/api.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/api.py @@ -10,10 +10,22 @@ from .._spawn import _spawn_viewer from ..datatypes import BoolLike, EntityPathLike, Float32ArrayLike, Utf8ArrayLike, Utf8Like from ..recording_stream import RecordingStream -from .archetypes import ContainerBlueprint, PanelBlueprint, ViewBlueprint, ViewContents, ViewportBlueprint +from ..time import to_nanos, to_nanos_since_epoch +from .archetypes import ( + ContainerBlueprint, + PanelBlueprint, + TimePanelBlueprint, + ViewBlueprint, + ViewContents, + ViewportBlueprint, +) from .components import PanelState, PanelStateLike if TYPE_CHECKING: + from datetime import datetime, timedelta + + import numpy as np + from ..memory import MemoryRecording from .components.container_kind import ContainerKindLike @@ -421,7 +433,16 @@ def __init__(self, *, expanded: bool | None = None, state: PanelStateLike | None class TimePanel(Panel): """The state of the time panel.""" - def __init__(self, *, expanded: bool | None = None, state: PanelStateLike | None = None) -> None: + def __init__( + self, + *, + expanded: bool | None = None, + state: PanelStateLike | None = None, + timeline: Utf8Like | None = None, + sequence_cursor: int | None = None, + duration_cursor: int | float | timedelta | np.timedelta64 | None = None, + timestamp_cursor: int | float | datetime | np.datetime64 | None = None, + ) -> None: """ Construct a new time panel. @@ -434,9 +455,47 @@ def __init__(self, *, expanded: bool | None = None, state: PanelStateLike | None Expanded fully shows the panel, collapsed shows a simplified panel, hidden fully hides the panel. + timeline: + What timeline the timepanel should display. + + sequence_cursor: + The time cursor for a sequence timeline. + + duration_cursor: + The time cursor for a duration timeline. + + timestamp_cursor: + The time cursor for a timestamp timeline. """ super().__init__(blueprint_path="time_panel", expanded=expanded, state=state) + self.timeline = timeline + + if sum(x is not None for x in (sequence_cursor, duration_cursor, timestamp_cursor)) > 1: + raise ValueError( + "At most one of `sequence`, `duration`, and `timestamp` must be set", + ) + + if sequence_cursor is not None: + self.time = sequence_cursor + elif duration_cursor is not None: + self.time = to_nanos(duration_cursor) + elif timestamp_cursor is not None: + self.time = to_nanos_since_epoch(timestamp_cursor) + + def _log_to_stream(self, stream: RecordingStream) -> None: + """Internal method to convert to an archetype and log to the stream.""" + arch = TimePanelBlueprint( + state=self.state, + timeline=self.timeline, + ) + + stream.log(self.blueprint_path(), arch) # type: ignore[attr-defined] + + if hasattr(self, "time"): + static_arch = TimePanelBlueprint(time=self.time) + + stream.log(self.blueprint_path(), static_arch, static=True) ContainerLike = Union[Container, View] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes index e51301bb4639..d043ea47f671 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/.gitattributes @@ -23,6 +23,7 @@ tensor_scalar_mapping.py linguist-generated=true tensor_slice_selection.py linguist-generated=true tensor_view_fit.py linguist-generated=true time_axis.py linguist-generated=true +time_panel_blueprint.py linguist-generated=true view_blueprint.py linguist-generated=true view_contents.py linguist-generated=true viewport_blueprint.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py index a07d1270b6f6..dc21b08024cc 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/__init__.py @@ -23,6 +23,7 @@ from .tensor_slice_selection import TensorSliceSelection from .tensor_view_fit import TensorViewFit from .time_axis import TimeAxis +from .time_panel_blueprint import TimePanelBlueprint from .view_blueprint import ViewBlueprint from .view_contents import ViewContents from .viewport_blueprint import ViewportBlueprint @@ -52,6 +53,7 @@ "TensorSliceSelection", "TensorViewFit", "TimeAxis", + "TimePanelBlueprint", "ViewBlueprint", "ViewContents", "ViewportBlueprint", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/panel_blueprint.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/panel_blueprint.py index 5aeee70e7bf8..17cab1379680 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/panel_blueprint.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/panel_blueprint.py @@ -33,7 +33,7 @@ def __init__(self: Any, *, state: blueprint_components.PanelStateLike | None = N Parameters ---------- state: - Current state of the panels. + Current state of the panel. """ @@ -71,7 +71,7 @@ def from_fields( clear_unset: If true, all unspecified fields will be explicitly cleared. state: - Current state of the panels. + Current state of the panel. """ @@ -100,7 +100,7 @@ def cleared(cls) -> PanelBlueprint: default=None, converter=blueprint_components.PanelStateBatch._converter, # type: ignore[misc] ) - # Current state of the panels. + # Current state of the panel. # # (Docstring intentionally commented out to hide this field from the docs) diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/time_panel_blueprint.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/time_panel_blueprint.py new file mode 100644 index 000000000000..bfeb80e675c0 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/time_panel_blueprint.py @@ -0,0 +1,149 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/archetypes/panel_blueprint.fbs". + +# You can extend this class by creating a "TimePanelBlueprintExt" class in "time_panel_blueprint_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from attrs import define, field + +from ..._baseclasses import ( + Archetype, +) +from ...blueprint import components as blueprint_components +from ...error_utils import catch_and_log_exceptions + +if TYPE_CHECKING: + from ... import datatypes + +__all__ = ["TimePanelBlueprint"] + + +@define(str=False, repr=False, init=False) +class TimePanelBlueprint(Archetype): + """ + **Archetype**: Time panel specific state. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + def __init__( + self: Any, + *, + state: blueprint_components.PanelStateLike | None = None, + timeline: datatypes.Utf8Like | None = None, + time: datatypes.TimeIntLike | None = None, + ) -> None: + """ + Create a new instance of the TimePanelBlueprint archetype. + + Parameters + ---------- + state: + Current state of the panel. + timeline: + What timeline the panel is on. + time: + What time the time cursor should be on. + + """ + + # You can define your own __init__ function as a member of TimePanelBlueprintExt in time_panel_blueprint_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(state=state, timeline=timeline, time=time) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + state=None, + timeline=None, + time=None, + ) + + @classmethod + def _clear(cls) -> TimePanelBlueprint: + """Produce an empty TimePanelBlueprint, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + @classmethod + def from_fields( + cls, + *, + clear_unset: bool = False, + state: blueprint_components.PanelStateLike | None = None, + timeline: datatypes.Utf8Like | None = None, + time: datatypes.TimeIntLike | None = None, + ) -> TimePanelBlueprint: + """ + Update only some specific fields of a `TimePanelBlueprint`. + + Parameters + ---------- + clear_unset: + If true, all unspecified fields will be explicitly cleared. + state: + Current state of the panel. + timeline: + What timeline the panel is on. + time: + What time the time cursor should be on. + + """ + + inst = cls.__new__(cls) + with catch_and_log_exceptions(context=cls.__name__): + kwargs = { + "state": state, + "timeline": timeline, + "time": time, + } + + if clear_unset: + kwargs = {k: v if v is not None else [] for k, v in kwargs.items()} # type: ignore[misc] + + inst.__attrs_init__(**kwargs) + return inst + + inst.__attrs_clear__() + return inst + + @classmethod + def cleared(cls) -> TimePanelBlueprint: + """Clear all the fields of a `TimePanelBlueprint`.""" + return cls.from_fields(clear_unset=True) + + state: blueprint_components.PanelStateBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.PanelStateBatch._converter, # type: ignore[misc] + ) + # Current state of the panel. + # + # (Docstring intentionally commented out to hide this field from the docs) + + timeline: blueprint_components.TimelineNameBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.TimelineNameBatch._converter, # type: ignore[misc] + ) + # What timeline the panel is on. + # + # (Docstring intentionally commented out to hide this field from the docs) + + time: blueprint_components.TimeCellBatch | None = field( + metadata={"component": True}, + default=None, + converter=blueprint_components.TimeCellBatch._converter, # type: ignore[misc] + ) + # What time the time cursor should be on. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes index 6edc902c2182..7d44c6a5007d 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/.gitattributes @@ -31,6 +31,7 @@ root_container.py linguist-generated=true row_share.py linguist-generated=true selected_columns.py linguist-generated=true tensor_dimension_index_slider.py linguist-generated=true +time_cell.py linguist-generated=true timeline_name.py linguist-generated=true view_class.py linguist-generated=true view_fit.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py index e1f7ecf7f17f..076b2f7820f3 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/__init__.py @@ -31,6 +31,7 @@ from .row_share import RowShare, RowShareBatch from .selected_columns import SelectedColumns, SelectedColumnsBatch from .tensor_dimension_index_slider import TensorDimensionIndexSlider, TensorDimensionIndexSliderBatch +from .time_cell import TimeCell, TimeCellBatch from .timeline_name import TimelineName, TimelineNameBatch from .view_class import ViewClass, ViewClassBatch from .view_fit import ViewFit, ViewFitArrayLike, ViewFitBatch, ViewFitLike @@ -115,6 +116,8 @@ "SelectedColumnsBatch", "TensorDimensionIndexSlider", "TensorDimensionIndexSliderBatch", + "TimeCell", + "TimeCellBatch", "TimelineName", "TimelineNameBatch", "ViewClass", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/components/time_cell.py b/rerun_py/rerun_sdk/rerun/blueprint/components/time_cell.py new file mode 100644 index 000000000000..35c35dd0b099 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/components/time_cell.py @@ -0,0 +1,35 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/components/time_cell.fbs". + +# You can extend this class by creating a "TimeCellExt" class in "time_cell_ext.py". + +from __future__ import annotations + +from ... import datatypes +from ..._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["TimeCell", "TimeCellBatch"] + + +class TimeCell(datatypes.TimeInt, ComponentMixin): + """ + **Component**: A reference to a time. + + ⚠️ **This type is _unstable_ and may change significantly in a way that the data won't be backwards compatible.** + """ + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of TimeCellExt in time_cell_ext.py + + # Note: there are no fields here because TimeCell delegates to datatypes.TimeInt + + +class TimeCellBatch(datatypes.TimeIntBatch, ComponentBatchMixin): + _COMPONENT_TYPE: str = "rerun.blueprint.components.TimeCell" + + +# This is patched in late to avoid circular dependencies. +TimeCell._BATCH_TYPE = TimeCellBatch # type: ignore[assignment]