diff --git a/Cargo.lock b/Cargo.lock index 6948ce26bb7a..fc62ec1507dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9719,6 +9719,7 @@ dependencies = [ "mimalloc", "nohash-hasher", "parking_lot", + "re_arrow_util", "re_chunk_store", "re_entity_db", "re_log", diff --git a/crates/store/re_chunk/src/helpers.rs b/crates/store/re_chunk/src/helpers.rs index 237992af8da6..97989b8e460f 100644 --- a/crates/store/re_chunk/src/helpers.rs +++ b/crates/store/re_chunk/src/helpers.rs @@ -368,7 +368,6 @@ impl UnitChunkShared { /// Returns the deserialized data for the specified component, assuming a mono-batch. /// /// Returns an error if the data cannot be deserialized, or if the underlying batch is not of unit length. - /// In debug builds, panics if the descriptor doesn't have the same type as the component type. #[inline] pub fn component_mono( &self, diff --git a/crates/store/re_tf/Cargo.toml b/crates/store/re_tf/Cargo.toml index ebfb6eaa0bda..bcfd1a9a28c5 100644 --- a/crates/store/re_tf/Cargo.toml +++ b/crates/store/re_tf/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [dependencies] +re_arrow_util.workspace = true re_chunk_store.workspace = true re_entity_db.workspace = true # It would be nice not to depend on this, but we need this in order to do queries right now. re_log_types.workspace = true diff --git a/crates/store/re_tf/src/transform_queries.rs b/crates/store/re_tf/src/transform_queries.rs index 125a89a8f2b3..b6820d8990a4 100644 --- a/crates/store/re_tf/src/transform_queries.rs +++ b/crates/store/re_tf/src/transform_queries.rs @@ -1,18 +1,20 @@ //! Utilities for querying out transform types. use glam::DAffine3; -use itertools::Either; +use itertools::{Either, Itertools as _}; use crate::convert; use crate::{ResolvedPinholeProjection, transform_resolution_cache::ParentFromChildTransform}; -use re_chunk_store::LatestAtQuery; +use re_arrow_util::ArrowArrayDowncastRef as _; +use re_chunk_store::{Chunk, LatestAtQuery, UnitChunkShared}; use re_entity_db::EntityDb; -use re_log_types::EntityPath; -use re_types::archetypes::InstancePoses3D; +use re_log_types::{EntityPath, TimeInt}; use re_types::{ ComponentIdentifier, TransformFrameIdHash, + archetypes::InstancePoses3D, archetypes::{self}, components, + external::arrow::{self, array::Array as _}, }; #[derive(Debug, thiserror::Error)] @@ -27,16 +29,127 @@ pub enum TransformError { MissingTransform { entity_path: EntityPath }, } -/// Queries all components that are part of pose transforms, returning the transform from child to parent. +/// Returns true if any of the given components is non-null on the given row. +fn has_row_any_component( + chunk: &Chunk, + row_index: usize, + components: &[ComponentIdentifier], +) -> bool { + components.iter().any(|component| { + chunk + .components() + .get_array(*component) + .is_some_and(|array| !array.is_null(row_index)) + }) +} + +/// Finds a unit chunk/row that has the latest changes for the given set of components and has +/// the given `frame_id` at the `frame_id_component` +/// +/// We have to find the last row-id for the given `child_frame_id` and time. +/// Since everything has the same row-id, everything has to be on the same chunk. +fn atomic_latest_at_query_for_frame( + entity_db: &EntityDb, + query: &LatestAtQuery, + entity_path: &EntityPath, + frame_id_component: ComponentIdentifier, + requested_frame_id: TransformFrameIdHash, + component_set: &[ComponentIdentifier], +) -> Option { + let include_static = true; + let chunks = entity_db + .storage_engine() + .store() + .latest_at_relevant_chunks_for_all_components(query, entity_path, include_static); + + let entity_path_derived_frame_id = TransformFrameIdHash::from_entity_path(entity_path); + + let mut unit_chunk: Option = None; + + let query_time = query.at().as_i64(); + + for chunk in chunks { + let mut row_indices_with_queried_time_from_new_to_old = if let Some(time_column) = + chunk.timelines().get(&query.timeline()) + && query_time != TimeInt::STATIC.as_i64() + { + // TODO: optimize for already sorted. + Either::Right( + time_column + .times_raw() + .iter() + .enumerate() + .filter_map(|(index, time)| (*time <= query_time).then_some(index)) + .sorted() + .rev(), + ) + } else { + Either::Left((0..chunk.num_rows()).rev()) + }; + + // Finds the last row index with time <= the query time and a matching frame id. + let higest_row_index_with_expected_frame_id = + if let Some(frame_ids) = chunk.components().get_array(frame_id_component) { + row_indices_with_queried_time_from_new_to_old.find(|index| { + let frame_id_row_untyped = frame_ids.value(*index); + let Some(frame_id_row) = + frame_id_row_untyped.downcast_array_ref::() + else { + // TODO: report error + return false; + }; + // Right now everything is singular on a single row, so check only the first element of this string array. + let frame_id = if frame_id_row.is_empty() || frame_id_row.is_null(0) { + // *something* on this row has to be non-empty & non-null! + if !has_row_any_component(&chunk, *index, component_set) { + return false; + } + entity_path_derived_frame_id + } else { + TransformFrameIdHash::from_str(frame_id_row.value(0)) + }; + + frame_id == requested_frame_id + }) + } else if entity_path_derived_frame_id == requested_frame_id { + // Pick the last where any relevant component is non-null & non-empty. + row_indices_with_queried_time_from_new_to_old + .find(|index| has_row_any_component(&chunk, *index, component_set)) + } else { + // There's no child_frame id and we're also not looking for the entity-path derived frame, + // so this chunk doesn't have any information about the the transform we're looking for. + continue; + }; + + if let Some(row_index) = higest_row_index_with_expected_frame_id { + let new_unit_chunk = chunk.row_sliced(row_index, 1).into_unit() + .expect("Chunk was just sliced to single row, therefore it must be convertible to a unit chunk"); + + if let Some(previous_chunk) = &unit_chunk { + // This should be rare: there's another chunk that also fits the exact same child id and the exact same time. + // Have to use row id as the tie breaker. + if previous_chunk.row_id() < new_unit_chunk.row_id() { + continue; + } + } + + unit_chunk = chunk.row_sliced(row_index, 1).into_unit(); + } + } + + unit_chunk +} + +/// Queries & processes all components that are part of a transform, returning the transform from child to parent. /// /// If any of the components yields an invalid transform, returns `None`. // TODO(#3849): There's no way to discover invalid transforms right now (they can be intentional but often aren't). -// TODO(grtlr): Consider returning a `SmallVec1`. pub fn query_and_resolve_tree_transform_at_entity( entity_path: &EntityPath, + child_frame_id: TransformFrameIdHash, entity_db: &EntityDb, query: &LatestAtQuery, -) -> Result, TransformError> { +) -> Result { // TODO(RR-2799): Output more than one target at once, doing the usual clamping - means probably we can merge a lot of code here with instance poses! // Topology @@ -52,28 +165,48 @@ pub fn query_and_resolve_tree_transform_at_entity( let identifier_scales = archetypes::Transform3D::descriptor_scale().component; let identifier_mat3x3 = archetypes::Transform3D::descriptor_mat3x3().component; - let results = entity_db.latest_at( + let all_components_of_transaction = [ + identifier_parent_frame, + identifier_child_frame, + identifier_relation, + // Geometry + identifier_translations, + identifier_rotation_axis_angles, + identifier_quaternions, + identifier_scales, + identifier_mat3x3, + ]; + + // We're querying for transactional/atomic transform state: + // If any of the topology or geometry components change, we reset the entire transform. + // + // This means we don't have to do latest-at for individual components. + // Instead, we're looking for the last change and then get everything with that row id. + // + // We bipass the query cache here: + // * we're already doing special caching anyways + // * we don't want to merge over row-ids *at all* since our query handling here is a little bit different. The query cache is geared towards "regular Rerun semantics" + // * we already handled `Clear`/`ClearRecursive` upon pre-population of our cache entries (we know when a clear occurs on this entity!) + let unit_chunk: Option = atomic_latest_at_query_for_frame( + entity_db, query, entity_path, - [ - identifier_parent_frame, - identifier_child_frame, - identifier_relation, - identifier_translations, - identifier_rotation_axis_angles, - identifier_quaternions, - identifier_scales, - identifier_mat3x3, - ], + identifier_child_frame, + child_frame_id, + &all_components_of_transaction, ); - if results.components.is_empty() { + let Some(unit_chunk) = unit_chunk else { + // TODO: Is this REALLY an error? return Err(TransformError::MissingTransform { entity_path: entity_path.clone(), }); - } + }; + + // TODO(andreas): silently ignores deserialization error right now. - let parent = results - .component_mono_quiet::(identifier_parent_frame) + let parent = unit_chunk + .component_mono::(identifier_parent_frame) + .and_then(|v| v.ok()) .map_or_else( || { TransformFrameIdHash::from_entity_path( @@ -83,30 +216,18 @@ pub fn query_and_resolve_tree_transform_at_entity( |frame_id| TransformFrameIdHash::new(&frame_id), ); - let child = results - .component_mono_quiet::(identifier_child_frame) - .map_or_else( - || TransformFrameIdHash::from_entity_path(entity_path), - |frame_id| TransformFrameIdHash::new(&frame_id), - ); - let mut transform = DAffine3::IDENTITY; - // It's an error if there's more than one component. Warn in that case. - let mono_log_level = re_log::Level::Warn; - // The order of the components here is important. - if let Some(translation) = results.component_mono_with_log_level::( - identifier_translations, - mono_log_level, - ) { + if let Some(translation) = unit_chunk + .component_mono::(identifier_translations) + .and_then(|v| v.ok()) + { transform = convert::translation_3d_to_daffine3(translation); } - if let Some(axis_angle) = results - .component_mono_with_log_level::( - identifier_rotation_axis_angles, - mono_log_level, - ) + if let Some(axis_angle) = unit_chunk + .component_mono::(identifier_rotation_axis_angles) + .and_then(|v| v.ok()) { let axis_angle = convert::rotation_axis_angle_to_daffine3(axis_angle).map_err(|_err| { TransformError::InvalidTransform { @@ -116,10 +237,10 @@ pub fn query_and_resolve_tree_transform_at_entity( })?; transform *= axis_angle; } - if let Some(quaternion) = results.component_mono_with_log_level::( - identifier_quaternions, - mono_log_level, - ) { + if let Some(quaternion) = unit_chunk + .component_mono::(identifier_quaternions) + .and_then(|v| v.ok()) + { let quaternion = convert::rotation_quat_to_daffine3(quaternion).map_err(|_err| { TransformError::InvalidTransform { entity_path: entity_path.clone(), @@ -128,8 +249,9 @@ pub fn query_and_resolve_tree_transform_at_entity( })?; transform *= quaternion; } - if let Some(scale) = results - .component_mono_with_log_level::(identifier_scales, mono_log_level) + if let Some(scale) = unit_chunk + .component_mono::(identifier_scales) + .and_then(|v| v.ok()) { if scale.x() == 0.0 && scale.y() == 0.0 && scale.z() == 0.0 { return Err(TransformError::InvalidTransform { @@ -139,10 +261,10 @@ pub fn query_and_resolve_tree_transform_at_entity( } transform *= convert::scale_3d_to_daffine3(scale); } - if let Some(mat3x3) = results.component_mono_with_log_level::( - identifier_mat3x3, - mono_log_level, - ) { + if let Some(mat3x3) = unit_chunk + .component_mono::(identifier_mat3x3) + .and_then(|v| v.ok()) + { let affine_transform = convert::transform_mat3x3_to_daffine3(mat3x3); if affine_transform.matrix3.determinant() == 0.0 { return Err(TransformError::InvalidTransform { @@ -153,10 +275,10 @@ pub fn query_and_resolve_tree_transform_at_entity( transform *= affine_transform; } - if results.component_mono_with_log_level::( - identifier_relation, - mono_log_level, - ) == Some(components::TransformRelation::ChildFromParent) + if unit_chunk + .component_mono::(identifier_relation) + .and_then(|v| v.ok()) + == Some(components::TransformRelation::ChildFromParent) { let determinant = transform.matrix3.determinant(); if determinant != 0.0 && determinant.is_finite() { @@ -171,10 +293,7 @@ pub fn query_and_resolve_tree_transform_at_entity( } } - Ok(vec![( - child, - ParentFromChildTransform { transform, parent }, - )]) + Ok(ParentFromChildTransform { transform, parent }) } /// Queries all components that are part of pose transforms, returning the transform from child to parent. @@ -187,7 +306,7 @@ pub fn query_and_resolve_instance_poses_at_entity( entity_db: &EntityDb, query: &LatestAtQuery, ) -> Vec { - let identifier_translations = archetypes::InstancePoses3D::descriptor_translations().component; + let identifier_translations = InstancePoses3D::descriptor_translations().component; let identifier_rotation_axis_angles = InstancePoses3D::descriptor_rotation_axis_angles().component; let identifier_quaternions = InstancePoses3D::descriptor_quaternions().component; diff --git a/crates/store/re_tf/src/transform_resolution_cache.rs b/crates/store/re_tf/src/transform_resolution_cache.rs index 0880c98ae5e7..71b5b09d7e5b 100644 --- a/crates/store/re_tf/src/transform_resolution_cache.rs +++ b/crates/store/re_tf/src/transform_resolution_cache.rs @@ -600,28 +600,17 @@ impl TransformsForChildFrame { CachedTransformValue::Resident(transform) => Some(transform.clone()), CachedTransformValue::Cleared => None, CachedTransformValue::Invalidated => { - let transforms = query_and_resolve_tree_transform_at_entity( + let transform = query_and_resolve_tree_transform_at_entity( &frame_transform.entity_path, + self.child_frame, entity_db, // Do NOT use the original query time since that may give us information about a different child frame! &LatestAtQuery::new(query.timeline(), *time_of_last_update_to_this_frame), ); // First, we update the cache value. - frame_transform.value = match &transforms { - Ok(transform) => { - if let Some(found) = transform.iter().find_map(|(child, transform)| { - (child == &self.child_frame).then_some(transform) - }) { - CachedTransformValue::Resident(found.clone()) - } else { - assert!( - !cfg!(debug_assertions), - "[DEBUG ASSERT] not finding a child here means our book keeping failed" - ); - CachedTransformValue::Cleared - } - } + frame_transform.value = match &transform { + Ok(transform) => CachedTransformValue::Resident(transform.clone()), Err(err) => { re_log::error_once!("Failed to query transformations: {err}"); CachedTransformValue::Cleared diff --git a/crates/store/re_types/definitions/rerun/archetypes/instance_poses3d.fbs b/crates/store/re_types/definitions/rerun/archetypes/instance_poses3d.fbs index a242137d388f..ffec82d33851 100644 --- a/crates/store/re_types/definitions/rerun/archetypes/instance_poses3d.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes/instance_poses3d.fbs @@ -6,6 +6,11 @@ namespace rerun.archetypes; /// If both [archetypes.InstancePoses3D] and [archetypes.Transform3D] are present, /// first the tree propagating [archetypes.Transform3D] is applied, then [archetypes.InstancePoses3D]. /// +/// Whenever you log this archetype, the state of the resulting overall pose is fully reset to the new archetype. +/// This means that if you first log a pose with only a translation, and then log one with only a rotation, +/// it will be resolved to a pose with only a rotation. +/// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) +/// /// From the point of view of the entity's coordinate system, /// all components are applied in the inverse order they are listed here. /// E.g. if both a translation and a max3x3 transform are present, @@ -39,6 +44,6 @@ table InstancePoses3D ( /// 3x3 transformation matrices. mat3x3: [rerun.components.PoseTransformMat3x3] ("attr.rerun.component_optional", nullable, order: 1500); - // TODO(andreas): Support TransformRelation? + // TODO(RR-2627): Make frame id configurable. // TODO(andreas): Support axis_length? } diff --git a/crates/store/re_types/definitions/rerun/archetypes/transform3d.fbs b/crates/store/re_types/definitions/rerun/archetypes/transform3d.fbs index e5d58ab3e41f..eb9fd76b1208 100644 --- a/crates/store/re_types/definitions/rerun/archetypes/transform3d.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes/transform3d.fbs @@ -7,9 +7,10 @@ namespace rerun.archetypes; /// E.g. if both a translation and a max3x3 transform are present, /// the 3x3 matrix is applied first, followed by the translation. /// -/// Whenever you log this archetype, it will write all components, even if you do not explicitly set them. +/// Whenever you log this archetype, the state of the resulting transform relationship is fully reset to the new archetype. /// This means that if you first log a transform with only a translation, and then log one with only a rotation, /// it will be resolved to a transform with only a rotation. +/// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) /// /// For transforms that affect only a single entity and do not propagate along the entity tree refer to [archetypes.InstancePoses3D]. /// diff --git a/crates/store/re_types/src/archetypes/instance_poses3d.rs b/crates/store/re_types/src/archetypes/instance_poses3d.rs index 61dc7c251e6b..ceed67ba8c7a 100644 --- a/crates/store/re_types/src/archetypes/instance_poses3d.rs +++ b/crates/store/re_types/src/archetypes/instance_poses3d.rs @@ -26,6 +26,11 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// If both [`archetypes::InstancePoses3D`][crate::archetypes::InstancePoses3D] and [`archetypes::Transform3D`][crate::archetypes::Transform3D] are present, /// first the tree propagating [`archetypes::Transform3D`][crate::archetypes::Transform3D] is applied, then [`archetypes::InstancePoses3D`][crate::archetypes::InstancePoses3D]. /// +/// Whenever you log this archetype, the state of the resulting overall pose is fully reset to the new archetype. +/// This means that if you first log a pose with only a translation, and then log one with only a rotation, +/// it will be resolved to a pose with only a rotation. +/// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) +/// /// From the point of view of the entity's coordinate system, /// all components are applied in the inverse order they are listed here. /// E.g. if both a translation and a max3x3 transform are present, diff --git a/crates/store/re_types/src/archetypes/transform3d.rs b/crates/store/re_types/src/archetypes/transform3d.rs index 8644238fdf7b..87a14b868727 100644 --- a/crates/store/re_types/src/archetypes/transform3d.rs +++ b/crates/store/re_types/src/archetypes/transform3d.rs @@ -28,9 +28,10 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// E.g. if both a translation and a max3x3 transform are present, /// the 3x3 matrix is applied first, followed by the translation. /// -/// Whenever you log this archetype, it will write all components, even if you do not explicitly set them. +/// Whenever you log this archetype, the state of the resulting transform relationship is fully reset to the new archetype. /// This means that if you first log a transform with only a translation, and then log one with only a rotation, /// it will be resolved to a transform with only a rotation. +/// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) /// /// For transforms that affect only a single entity and do not propagate along the entity tree refer to [`archetypes::InstancePoses3D`][crate::archetypes::InstancePoses3D]. /// diff --git a/docs/content/reference/migration/migration-0-28.md b/docs/content/reference/migration/migration-0-28.md new file mode 100644 index 000000000000..92c848cb7fa0 --- /dev/null +++ b/docs/content/reference/migration/migration-0-28.md @@ -0,0 +1,47 @@ +--- +title: Migrating from 0.27 to 0.28 +order: 982 +--- + + + +## Changes to `Transform3D`/`InstancePose3D` are now treated transactionally by the Viewer + +If you previously updated only certain components of `Transform3D`/`InstancePose3D` and relied on previously logged +values remaining present, +you must now re-log those previous values every time you update the `Transform3D`/`InstancePose3D`. + +If you always logged the same transform components on every log/send call or used the standard constructor of +`Transform3D`, no changes are required! + +snippet: migration/transactional_transforms + +### Details & motivation + +We changed the way `Transform3D` and `InstancePose3D` are queried under the hood! + +Usually, when querying any collection of components with latest-at semantics, we look for the latest update of each +individual component. +This is useful, for example, when you log a mesh and only change its texture over time: +a latest-at query at any point in time gets all the same vertex information, but the texture that is active at any given +point in time may changes. + +However, for `Transform3D`, this behavior can be very surprising, +as the typical expectation is that logging a `Transform3D` with only a rotation will not inherit previously logged +translations to the same path. +Previously, to work around this, all SDKs implemented the constructor of `Transform3D` such that it set all components +to empty arrays, thereby clearing everything that was logged before. +This caused significant memory (and networking) bloat, as well as needlessly convoluted displays in the viewer. +With the arrival of explicit ROS-style transform frames, per-component latest-at semantics can cause even more +surprising side effects. + +Therefore, we decided to change the semantics of `Transform3D` such that any change to any of its components fully +resets the transform state. + +For example, if you change its rotation and scale fields but do not write to translation, we will not look further back +in time to find the previous value of translation. +Instead, we assume that translation is not set at all (i.e., zero), deriving the new overall transform state only from +rotation and scale. +Naturally, if any update to a transform always changes the same components, this does not cause any changes other than +the simplification of not having to clear out all other components that may ever be set, thus reducing memory bloat both +on send and query! diff --git a/docs/content/reference/types/archetypes/instance_poses3d.md b/docs/content/reference/types/archetypes/instance_poses3d.md index a7bca48f8e08..4a4e4b7458c9 100644 --- a/docs/content/reference/types/archetypes/instance_poses3d.md +++ b/docs/content/reference/types/archetypes/instance_poses3d.md @@ -8,6 +8,11 @@ One or more transforms between the current entity and its parent. Unlike [`arche If both [`archetypes.InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d) and [`archetypes.Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d) are present, first the tree propagating [`archetypes.Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d) is applied, then [`archetypes.InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d). +Whenever you log this archetype, the state of the resulting overall pose is fully reset to the new archetype. +This means that if you first log a pose with only a translation, and then log one with only a rotation, +it will be resolved to a pose with only a rotation. +(This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) + From the point of view of the entity's coordinate system, all components are applied in the inverse order they are listed here. E.g. if both a translation and a max3x3 transform are present, diff --git a/docs/content/reference/types/archetypes/transform3d.md b/docs/content/reference/types/archetypes/transform3d.md index bd87d9ab6cb5..22a8fc7ccafb 100644 --- a/docs/content/reference/types/archetypes/transform3d.md +++ b/docs/content/reference/types/archetypes/transform3d.md @@ -10,9 +10,10 @@ all components are applied in the inverse order they are listed here. E.g. if both a translation and a max3x3 transform are present, the 3x3 matrix is applied first, followed by the translation. -Whenever you log this archetype, it will write all components, even if you do not explicitly set them. +Whenever you log this archetype, the state of the resulting transform relationship is fully reset to the new archetype. This means that if you first log a transform with only a translation, and then log one with only a rotation, it will be resolved to a transform with only a rotation. +(This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) For transforms that affect only a single entity and do not propagate along the entity tree refer to [`archetypes.InstancePoses3D`](https://rerun.io/docs/reference/types/archetypes/instance_poses3d). diff --git a/docs/snippets/INDEX.md b/docs/snippets/INDEX.md index 44e2b32b5247..3415474c3d13 100644 --- a/docs/snippets/INDEX.md +++ b/docs/snippets/INDEX.md @@ -203,6 +203,7 @@ _All snippets, organized by the [`Archetype`](https://rerun.io/docs/reference/ty | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠transform3d_column_updates` | Update a transform over time, in a single operation | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_column_updates.cpp) | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠transform3d_axes` | Log different transforms with visualized coordinates axes | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/transform3d_axes.cpp) | | **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `archetypes⁠/⁠instance_poses3d_combined` | Log a simple 3D box with a regular & instance pose transform | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/instance_poses3d_combined.cpp) | +| **[`Transform3D`](https://rerun.io/docs/reference/types/archetypes/transform3d)** | `migration⁠/⁠transactional_transforms` | | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/migration/transactional_transforms.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/migration/transactional_transforms.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/migration/transactional_transforms.cpp) | | **[`VideoFrameReference`](https://rerun.io/docs/reference/types/archetypes/video_frame_reference)** | `archetypes⁠/⁠video_auto_frames` | Log a video asset using automatically determined frame references | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_auto_frames.cpp) | | **[`VideoFrameReference`](https://rerun.io/docs/reference/types/archetypes/video_frame_reference)** | `archetypes⁠/⁠video_manual_frames` | Manual use of individual video frame references | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.py) | [🦀](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.rs) | [🌊](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_manual_frames.cpp) | | **[`VideoStream`](https://rerun.io/docs/reference/types/archetypes/video_stream)** | `archetypes⁠/⁠video_stream_synthetic` | Video encode images using av and stream them to Rerun | [🐍](https://github.com/rerun-io/rerun/blob/main/docs/snippets/all/archetypes/video_stream_synthetic.py) | | | diff --git a/docs/snippets/all/migration/transactional_transforms.cpp b/docs/snippets/all/migration/transactional_transforms.cpp new file mode 100644 index 000000000000..4af84638a7d5 --- /dev/null +++ b/docs/snippets/all/migration/transactional_transforms.cpp @@ -0,0 +1,7 @@ +// Log a translation transform. +rec.log("simple", rerun::Transform3D::from_translation({1.0f, 2.0f, 3.0f})); + +// Note that we explicitly only set the scale here: +// Previously, this would have meant that we keep the translation. +// However, in 0.27 the Viewer will no longer apply the previous translation regardless. +rec.log("simple", rerun::Transform3D::update_fields().with_scale(2.0f)); diff --git a/docs/snippets/all/migration/transactional_transforms.py b/docs/snippets/all/migration/transactional_transforms.py new file mode 100644 index 000000000000..7b263d391f98 --- /dev/null +++ b/docs/snippets/all/migration/transactional_transforms.py @@ -0,0 +1,8 @@ +import rerun as rr + +rr.log("simple", rr.Transform3D(translation=[1.0, 2.0, 3.0])) + +# Note that we explicitly only set the scale here: +# Previously, this would have meant that we keep the translation. +# However, in 0.27 the Viewer will no longer show apply the previous translation regardless. +rr.log("simple", rr.Transform3D.from_fields(scale=2)) diff --git a/docs/snippets/all/migration/transactional_transforms.rs b/docs/snippets/all/migration/transactional_transforms.rs new file mode 100644 index 000000000000..b37013640f6f --- /dev/null +++ b/docs/snippets/all/migration/transactional_transforms.rs @@ -0,0 +1,13 @@ +// Log a translation transform. +rec.log( + "simple", + &rerun::Transform3D::from_translation([1.0, 2.0, 3.0]), +)?; + +// Note that we explicitly only set the scale here: +// Previously, this would have meant that we keep the translation. +// However, in 0.27 the Viewer will no longer apply the previous translation regardless. +rec.log( + "simple", + &rerun::Transform3D::update_fields().with_scale(2.0), +)?; diff --git a/docs/snippets/snippets.toml b/docs/snippets/snippets.toml index ca377613e9e3..b2d91300dc65 100644 --- a/docs/snippets/snippets.toml +++ b/docs/snippets/snippets.toml @@ -226,6 +226,11 @@ backwards_check = [ "rust", "py", ] +"migration/transactional_transforms" = [ # Not a complete example -- just a couple of log lines + "cpp", + "rust", + "py", +] "reference/dataframe_query" = [ # No output "cpp", "rust", diff --git a/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp b/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp index 767bf03a1146..9517bd5f4bb5 100644 --- a/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/instance_poses3d.hpp @@ -24,6 +24,11 @@ namespace rerun::archetypes { /// If both `archetypes::InstancePoses3D` and `archetypes::Transform3D` are present, /// first the tree propagating `archetypes::Transform3D` is applied, then `archetypes::InstancePoses3D`. /// + /// Whenever you log this archetype, the state of the resulting overall pose is fully reset to the new archetype. + /// This means that if you first log a pose with only a translation, and then log one with only a rotation, + /// it will be resolved to a pose with only a rotation. + /// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) + /// /// From the point of view of the entity's coordinate system, /// all components are applied in the inverse order they are listed here. /// E.g. if both a translation and a max3x3 transform are present, diff --git a/rerun_cpp/src/rerun/archetypes/transform3d.hpp b/rerun_cpp/src/rerun/archetypes/transform3d.hpp index 171d40c75833..425b9045b652 100644 --- a/rerun_cpp/src/rerun/archetypes/transform3d.hpp +++ b/rerun_cpp/src/rerun/archetypes/transform3d.hpp @@ -32,9 +32,10 @@ namespace rerun::archetypes { /// E.g. if both a translation and a max3x3 transform are present, /// the 3x3 matrix is applied first, followed by the translation. /// - /// Whenever you log this archetype, it will write all components, even if you do not explicitly set them. + /// Whenever you log this archetype, the state of the resulting transform relationship is fully reset to the new archetype. /// This means that if you first log a transform with only a translation, and then log one with only a rotation, /// it will be resolved to a transform with only a rotation. + /// (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) /// /// For transforms that affect only a single entity and do not propagate along the entity tree refer to `archetypes::InstancePoses3D`. /// diff --git a/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py b/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py index ffa3608960a3..a29930ed1349 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/instance_poses3d.py @@ -29,6 +29,11 @@ class InstancePoses3D(Archetype): If both [`archetypes.InstancePoses3D`][rerun.archetypes.InstancePoses3D] and [`archetypes.Transform3D`][rerun.archetypes.Transform3D] are present, first the tree propagating [`archetypes.Transform3D`][rerun.archetypes.Transform3D] is applied, then [`archetypes.InstancePoses3D`][rerun.archetypes.InstancePoses3D]. + Whenever you log this archetype, the state of the resulting overall pose is fully reset to the new archetype. + This means that if you first log a pose with only a translation, and then log one with only a rotation, + it will be resolved to a pose with only a rotation. + (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) + From the point of view of the entity's coordinate system, all components are applied in the inverse order they are listed here. E.g. if both a translation and a max3x3 transform are present, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py b/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py index e093d8a09eac..f25691eafa59 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/transform3d.py @@ -30,9 +30,10 @@ class Transform3D(Transform3DExt, Archetype): E.g. if both a translation and a max3x3 transform are present, the 3x3 matrix is applied first, followed by the translation. - Whenever you log this archetype, it will write all components, even if you do not explicitly set them. + Whenever you log this archetype, the state of the resulting transform relationship is fully reset to the new archetype. This means that if you first log a transform with only a translation, and then log one with only a rotation, it will be resolved to a transform with only a rotation. + (This is unlike how we usually apply latest-at semantics on an archetype where we take the latest state of any component independently) For transforms that affect only a single entity and do not propagate along the entity tree refer to [`archetypes.InstancePoses3D`][rerun.archetypes.InstancePoses3D].