diff --git a/crates/build/re_types_builder/src/codegen/cpp/mod.rs b/crates/build/re_types_builder/src/codegen/cpp/mod.rs index e2a9d69645d6..af4dfb61bbc1 100644 --- a/crates/build/re_types_builder/src/codegen/cpp/mod.rs +++ b/crates/build/re_types_builder/src/codegen/cpp/mod.rs @@ -16,7 +16,7 @@ use crate::{ format_path, objects::ObjectClass, ArrowRegistry, Docs, ElementType, GeneratedFiles, Object, ObjectField, ObjectKind, Objects, - Reporter, Type, ATTR_CPP_NO_FIELD_CTORS, + Reporter, Type, ATTR_CPP_NO_FIELD_CTORS, ATTR_CPP_RENAME_FIELD, }; use self::array_builder::{arrow_array_builder_type, arrow_array_builder_type_object}; @@ -445,7 +445,7 @@ impl QuotedObject { .iter() .map(|obj_field| { let docstring = quote_field_docs(objects, obj_field); - let field_name = format_ident!("{}", obj_field.name); + let field_name = field_name_identifier(obj_field); let field_type = quote_archetype_field_type(&mut hpp_includes, obj_field); let field_type = if obj_field.is_nullable { hpp_includes.insert_system("optional"); @@ -476,7 +476,7 @@ impl QuotedObject { .iter() .map(|obj_field| { let field_type = quote_archetype_field_type(&mut hpp_includes, obj_field); - let field_ident = format_ident!("{}", obj_field.name); + let field_ident = field_name_identifier(obj_field); // C++ compilers give warnings for re-using the same name as the member variable. let parameter_ident = format_ident!("_{}", obj_field.name); ( @@ -498,7 +498,7 @@ impl QuotedObject { // Builder methods for all optional components. for obj_field in obj.fields.iter().filter(|field| field.is_nullable) { - let field_ident = format_ident!("{}", obj_field.name); + let field_ident = field_name_identifier(obj_field); // C++ compilers give warnings for re-using the same name as the member variable. let parameter_ident = format_ident!("_{}", obj_field.name); let method_ident = format_ident!("with_{}", obj_field.name); @@ -663,7 +663,7 @@ impl QuotedObject { objects, &mut hpp_includes, obj_field, - &format_ident!("{}", obj_field.name), + &field_name_identifier(obj_field), ); quote! { #NEWLINE_TOKEN @@ -819,7 +819,7 @@ impl QuotedObject { } }) .chain(obj.fields.iter().map(|obj_field| { - let ident = format_ident!("{}", obj_field.name); + let ident = field_name_identifier(obj_field); quote! { #ident, } @@ -896,7 +896,7 @@ impl QuotedObject { for obj_field in &obj.fields { let snake_case_name = obj_field.snake_case_name(); let field_name = format_ident!("{}", snake_case_name); - let tag_name = format_ident!("{}", obj_field.name); + let tag_name = field_name_identifier(obj_field); let method = if obj_field.typ == Type::Unit { let method_name = format_ident!("is_{}", snake_case_name); @@ -950,7 +950,7 @@ impl QuotedObject { } }) .chain(obj.fields.iter().map(|obj_field| { - let tag_ident = format_ident!("{}", obj_field.name); + let tag_ident = field_name_identifier(obj_field); let field_ident = format_ident!("{}", obj_field.snake_case_name()); if obj_field.typ.has_default_destructor(objects) { @@ -988,7 +988,7 @@ impl QuotedObject { let mut placement_new_arms = Vec::new(); let mut trivial_memcpy_cases = Vec::new(); for obj_field in &obj.fields { - let tag_ident = format_ident!("{}", obj_field.name); + let tag_ident = field_name_identifier(obj_field); let case = quote!(case detail::#tag_typename::#tag_ident:); // Inferring from trivial destructability that we don't need to call the copy constructor is a little bit wonky, @@ -1215,7 +1215,7 @@ impl QuotedObject { .enumerate() .map(|(i, obj_field)| { let docstring = quote_field_docs(objects, obj_field); - let field_name = format_ident!("{}", obj_field.name); + let field_name = field_name_identifier(obj_field); // We assign the arrow type index to the enum fields to make encoding simpler and faster: let arrow_type_index = proc_macro2::Literal::usize_unsuffixed(1 + i); // 0 is reserved for `_null_markers` @@ -1263,6 +1263,14 @@ impl QuotedObject { } } +fn field_name_identifier(obj_field: &ObjectField) -> Ident { + if let Some(name) = obj_field.try_get_attr::(ATTR_CPP_RENAME_FIELD) { + format_ident!("{}", name) + } else { + format_ident!("{}", obj_field.name) + } +} + fn single_field_constructor_methods( obj: &Object, hpp_includes: &mut Includes, @@ -1303,7 +1311,7 @@ fn add_copy_assignment_and_constructor( objects: &Objects, ) -> Vec { let mut methods = Vec::new(); - let field_ident = format_ident!("{}", target_field.name); + let field_ident = field_name_identifier(target_field); let param_ident = format_ident!("{}_", obj_field.name); // We keep parameter passing for assignment & ctors simple by _always_ passing by value. @@ -1551,7 +1559,7 @@ fn archetype_serialize(type_ident: &Ident, obj: &Object, hpp_includes: &mut Incl let num_fields = quote_integer(obj.fields.len() + 1); // Plus one for the indicator. let push_batches = obj.fields.iter().map(|field| { - let field_name = format_ident!("{}", field.name); + let field_name = field_name_identifier(field); let field_accessor = quote!(archetype.#field_name); let push_back = quote! { @@ -1829,7 +1837,7 @@ fn quote_append_field_to_builder( includes: &mut Includes, objects: &Objects, ) -> TokenStream { - let field_name = format_ident!("{}", field.name); + let field_name = field_name_identifier(field); if let Some(elem_type) = field.typ.plural_inner() { let value_builder = format_ident!("value_builder"); diff --git a/crates/build/re_types_builder/src/codegen/rust/serializer.rs b/crates/build/re_types_builder/src/codegen/rust/serializer.rs index d5f2ae9905ab..9fea9998149b 100644 --- a/crates/build/re_types_builder/src/codegen/rust/serializer.rs +++ b/crates/build/re_types_builder/src/codegen/rust/serializer.rs @@ -447,12 +447,13 @@ fn quote_arrow_field_serializer( // hence `unwrap_or_default` (unless elements_are_nullable is false) let quoted_transparent_mapping = if inner_is_arrow_transparent { let inner_obj = inner_obj.as_ref().unwrap(); - let quoted_inner_obj_type = quote_fqname_as_type_path(&inner_obj.fqname); let is_tuple_struct = is_tuple_struct_from_obj(inner_obj); let quoted_member_accessor = if is_tuple_struct { quote!(0) } else { - quote!(#quoted_inner_obj_type) + let inner_field_name = + format_ident!("{}", inner_obj.fields[0].snake_case_name()); + quote!(#inner_field_name) }; if elements_are_nullable { diff --git a/crates/build/re_types_builder/src/lib.rs b/crates/build/re_types_builder/src/lib.rs index 51d2e257df86..67beb9ebf865 100644 --- a/crates/build/re_types_builder/src/lib.rs +++ b/crates/build/re_types_builder/src/lib.rs @@ -198,6 +198,7 @@ pub const ATTR_RUST_TUPLE_STRUCT: &str = "attr.rust.tuple_struct"; pub const ATTR_RUST_GENERATE_FIELD_INFO: &str = "attr.rust.generate_field_info"; pub const ATTR_CPP_NO_FIELD_CTORS: &str = "attr.cpp.no_field_ctors"; +pub const ATTR_CPP_RENAME_FIELD: &str = "attr.cpp.rename_field"; pub const ATTR_DOCS_UNRELEASED: &str = "attr.docs.unreleased"; pub const ATTR_DOCS_CATEGORY: &str = "attr.docs.category"; diff --git a/crates/store/re_types/definitions/cpp/attributes.fbs b/crates/store/re_types/definitions/cpp/attributes.fbs index cc3fc0e29926..1ebfa0821c81 100644 --- a/crates/store/re_types/definitions/cpp/attributes.fbs +++ b/crates/store/re_types/definitions/cpp/attributes.fbs @@ -2,3 +2,7 @@ namespace cpp.attributes; /// Don't emit parameterized constructors for fields, only copy, move and default constructors. attribute "attr.cpp.no_field_ctors"; + +/// Changes the name of a field in the generated C++ code. +/// This can be used to avoid name clashes with C++ keywords or methods. +attribute "attr.cpp.rename_field"; diff --git a/crates/store/re_types/definitions/rerun/datatypes/angle.fbs b/crates/store/re_types/definitions/rerun/datatypes/angle.fbs index 67c623468211..7bf8007fd281 100644 --- a/crates/store/re_types/definitions/rerun/datatypes/angle.fbs +++ b/crates/store/re_types/definitions/rerun/datatypes/angle.fbs @@ -1,6 +1,7 @@ include "arrow/attributes.fbs"; include "rust/attributes.fbs"; include "fbs/attributes.fbs"; +include "cpp/attributes.fbs"; include "./float32.fbs"; @@ -8,15 +9,15 @@ namespace rerun.datatypes; // --- -/// Angle in either radians or degrees. -union Angle ( - "attr.rust.derive": "Copy, PartialEq" +/// Angle in radians. +struct Angle ( + "attr.arrow.transparent", + "attr.python.aliases": "float, int", + "attr.python.array_aliases": "npt.ArrayLike, Sequence[float], Sequence[int]", + "attr.rust.derive": "Copy, Default, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable", + "attr.rust.repr": "transparent", + "attr.cpp.no_field_ctors" ) { /// Angle in radians. One turn is equal to 2π (or τ) radians. - /// \py Only one of `degrees` or `radians` should be set. - Radians: rerun.datatypes.Float32 (transparent), - - /// Angle in degrees. One turn is equal to 360 degrees. - /// \py Only one of `degrees` or `radians` should be set. - Degrees: rerun.datatypes.Float32 (transparent), + radians: float (order: 100, "attr.cpp.rename_field": "angle_radians"); // Rename field to avoid nameclash with `radians` function. } diff --git a/crates/store/re_types/src/archetypes/boxes3d.rs b/crates/store/re_types/src/archetypes/boxes3d.rs index b93d27c2b893..85aa7c59f1ed 100644 --- a/crates/store/re_types/src/archetypes/boxes3d.rs +++ b/crates/store/re_types/src/archetypes/boxes3d.rs @@ -36,7 +36,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// .with_rotations([ /// rerun::Rotation3D::IDENTITY, /// rerun::Quaternion::from_xyzw([0.0, 0.0, 0.382683, 0.923880]).into(), // 45 degrees around Z -/// rerun::RotationAxisAngle::new((0.0, 1.0, 0.0), rerun::Angle::Degrees(30.0)).into(), +/// rerun::RotationAxisAngle::new((0.0, 1.0, 0.0), rerun::Angle::from_degrees(30.0)).into(), /// ]) /// .with_radii([0.025]) /// .with_colors([ diff --git a/crates/store/re_types/src/archetypes/transform3d.rs b/crates/store/re_types/src/archetypes/transform3d.rs index c5efc6629fa9..c7bad74dca53 100644 --- a/crates/store/re_types/src/archetypes/transform3d.rs +++ b/crates/store/re_types/src/archetypes/transform3d.rs @@ -51,7 +51,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// rec.log( /// "base/rotated_scaled", /// &rerun::Transform3D::from_rotation_scale( -/// rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::Radians(TAU / 8.0)), +/// rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::from_radians(TAU / 8.0)), /// rerun::Scale3D::from(2.0), /// ), /// )?; @@ -136,7 +136,7 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; /// [r_planet.sin() * d_planet, r_planet.cos() * d_planet, 0.0], /// rerun::RotationAxisAngle { /// axis: [1.0, 0.0, 0.0].into(), -/// angle: rerun::Angle::Degrees(20.0), +/// angle: rerun::Angle::from_degrees(20.0), /// }, /// ), /// )?; diff --git a/crates/store/re_types/src/datatypes/angle.rs b/crates/store/re_types/src/datatypes/angle.rs index d0c7768eba22..22925b8d2fd1 100644 --- a/crates/store/re_types/src/datatypes/angle.rs +++ b/crates/store/re_types/src/datatypes/angle.rs @@ -18,29 +18,37 @@ use ::re_types_core::SerializationResult; use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; use ::re_types_core::{DeserializationError, DeserializationResult}; -/// **Datatype**: Angle in either radians or degrees. -#[derive(Clone, Debug, Copy, PartialEq)] -pub enum Angle { +/// **Datatype**: Angle in radians. +#[derive(Clone, Debug, Copy, Default, PartialEq, PartialOrd, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(transparent)] +pub struct Angle { /// Angle in radians. One turn is equal to 2π (or τ) radians. - Radians(f32), - - /// Angle in degrees. One turn is equal to 360 degrees. - Degrees(f32), + pub radians: f32, } impl ::re_types_core::SizeBytes for Angle { #[inline] fn heap_size_bytes(&self) -> u64 { - #![allow(clippy::match_same_arms)] - match self { - Self::Radians(v) => v.heap_size_bytes(), - Self::Degrees(v) => v.heap_size_bytes(), - } + self.radians.heap_size_bytes() } #[inline] fn is_pod() -> bool { - ::is_pod() && ::is_pod() + ::is_pod() + } +} + +impl From for Angle { + #[inline] + fn from(radians: f32) -> Self { + Self { radians } + } +} + +impl From for f32 { + #[inline] + fn from(value: Angle) -> Self { + value.radians } } @@ -58,15 +66,7 @@ impl ::re_types_core::Loggable for Angle { fn arrow_datatype() -> arrow2::datatypes::DataType { #![allow(clippy::wildcard_imports)] use arrow2::datatypes::*; - DataType::Union( - std::sync::Arc::new(vec![ - Field::new("_null_markers", DataType::Null, true), - Field::new("Radians", DataType::Float32, false), - Field::new("Degrees", DataType::Float32, false), - ]), - Some(std::sync::Arc::new(vec![0i32, 1i32, 2i32])), - UnionMode::Dense, - ) + DataType::Float32 } fn to_arrow_opt<'a>( @@ -79,82 +79,24 @@ impl ::re_types_core::Loggable for Angle { use ::re_types_core::{Loggable as _, ResultExt as _}; use arrow2::{array::*, datatypes::*}; Ok({ - // Dense Arrow union - let data: Vec<_> = data + let (somes, radians): (Vec<_>, Vec<_>) = data .into_iter() .map(|datum| { let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); - datum - }) - .collect(); - let types = data - .iter() - .map(|a| match a.as_deref() { - None => 0, - Some(Self::Radians(_)) => 1i8, - Some(Self::Degrees(_)) => 2i8, + let datum = datum.map(|datum| datum.into_owned().radians); + (datum.is_some(), datum) }) - .collect(); - let fields = vec![ - NullArray::new(DataType::Null, data.iter().filter(|v| v.is_none()).count()).boxed(), - { - let radians: Vec<_> = data - .iter() - .filter_map(|datum| match datum.as_deref() { - Some(Self::Radians(v)) => Some(v.clone()), - _ => None, - }) - .collect(); - let radians_bitmap: Option = None; - PrimitiveArray::new( - DataType::Float32, - radians.into_iter().collect(), - radians_bitmap, - ) - .boxed() - }, - { - let degrees: Vec<_> = data - .iter() - .filter_map(|datum| match datum.as_deref() { - Some(Self::Degrees(v)) => Some(v.clone()), - _ => None, - }) - .collect(); - let degrees_bitmap: Option = None; - PrimitiveArray::new( - DataType::Float32, - degrees.into_iter().collect(), - degrees_bitmap, - ) - .boxed() - }, - ]; - let offsets = Some({ - let mut radians_offset = 0; - let mut degrees_offset = 0; - let mut nulls_offset = 0; - data.iter() - .map(|v| match v.as_deref() { - None => { - let offset = nulls_offset; - nulls_offset += 1; - offset - } - Some(Self::Radians(_)) => { - let offset = radians_offset; - radians_offset += 1; - offset - } - Some(Self::Degrees(_)) => { - let offset = degrees_offset; - degrees_offset += 1; - offset - } - }) - .collect() - }); - UnionArray::new(Self::arrow_datatype(), types, fields, offsets).boxed() + .unzip(); + let radians_bitmap: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + PrimitiveArray::new( + Self::arrow_datatype(), + radians.into_iter().map(|v| v.unwrap_or_default()).collect(), + radians_bitmap, + ) + .boxed() }) } @@ -167,123 +109,55 @@ impl ::re_types_core::Loggable for Angle { #![allow(clippy::wildcard_imports)] use ::re_types_core::{Loggable as _, ResultExt as _}; use arrow2::{array::*, buffer::*, datatypes::*}; + Ok(arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.Angle#radians")? + .into_iter() + .map(|opt| opt.copied()) + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .map(|res| res.map(|radians| Some(Self { radians }))) + .collect::>>>() + .with_context("rerun.datatypes.Angle#radians") + .with_context("rerun.datatypes.Angle")?) + } + + #[inline] + fn from_arrow(arrow_data: &dyn arrow2::array::Array) -> DeserializationResult> + where + Self: Sized, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow2::{array::*, buffer::*, datatypes::*}; + if let Some(validity) = arrow_data.validity() { + if validity.unset_bits() != 0 { + return Err(DeserializationError::missing_data()); + } + } Ok({ - let arrow_data = arrow_data + let slice = arrow_data .as_any() - .downcast_ref::() + .downcast_ref::() .ok_or_else(|| { - let expected = Self::arrow_datatype(); + let expected = DataType::Float32; let actual = arrow_data.data_type().clone(); DeserializationError::datatype_mismatch(expected, actual) }) - .with_context("rerun.datatypes.Angle")?; - if arrow_data.is_empty() { - Vec::new() - } else { - let (arrow_data_types, arrow_data_arrays) = - (arrow_data.types(), arrow_data.fields()); - let arrow_data_offsets = arrow_data - .offsets() - .ok_or_else(|| { - let expected = Self::arrow_datatype(); - let actual = arrow_data.data_type().clone(); - DeserializationError::datatype_mismatch(expected, actual) - }) - .with_context("rerun.datatypes.Angle")?; - if arrow_data_types.len() != arrow_data_offsets.len() { - return Err(DeserializationError::offset_slice_oob( - (0, arrow_data_types.len()), - arrow_data_offsets.len(), - )) - .with_context("rerun.datatypes.Angle"); - } - let radians = { - if 1usize >= arrow_data_arrays.len() { - return Ok(Vec::new()); - } - let arrow_data = &*arrow_data_arrays[1usize]; - arrow_data - .as_any() - .downcast_ref::() - .ok_or_else(|| { - let expected = DataType::Float32; - let actual = arrow_data.data_type().clone(); - DeserializationError::datatype_mismatch(expected, actual) - }) - .with_context("rerun.datatypes.Angle#Radians")? - .into_iter() - .map(|opt| opt.copied()) - .collect::>() - }; - let degrees = { - if 2usize >= arrow_data_arrays.len() { - return Ok(Vec::new()); - } - let arrow_data = &*arrow_data_arrays[2usize]; - arrow_data - .as_any() - .downcast_ref::() - .ok_or_else(|| { - let expected = DataType::Float32; - let actual = arrow_data.data_type().clone(); - DeserializationError::datatype_mismatch(expected, actual) - }) - .with_context("rerun.datatypes.Angle#Degrees")? - .into_iter() - .map(|opt| opt.copied()) - .collect::>() - }; - arrow_data_types + .with_context("rerun.datatypes.Angle#radians")? + .values() + .as_slice(); + { + slice .iter() - .enumerate() - .map(|(i, typ)| { - let offset = arrow_data_offsets[i]; - if *typ == 0 { - Ok(None) - } else { - Ok(Some(match typ { - 1i8 => Self::Radians({ - if offset as usize >= radians.len() { - return Err(DeserializationError::offset_oob( - offset as _, - radians.len(), - )) - .with_context("rerun.datatypes.Angle#Radians"); - } - - #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] - unsafe { radians.get_unchecked(offset as usize) } - .clone() - .ok_or_else(DeserializationError::missing_data) - .with_context("rerun.datatypes.Angle#Radians")? - }), - 2i8 => Self::Degrees({ - if offset as usize >= degrees.len() { - return Err(DeserializationError::offset_oob( - offset as _, - degrees.len(), - )) - .with_context("rerun.datatypes.Angle#Degrees"); - } - - #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] - unsafe { degrees.get_unchecked(offset as usize) } - .clone() - .ok_or_else(DeserializationError::missing_data) - .with_context("rerun.datatypes.Angle#Degrees")? - }), - _ => { - return Err(DeserializationError::missing_union_arm( - Self::arrow_datatype(), - "", - *typ as _, - )); - } - })) - } - }) - .collect::>>() - .with_context("rerun.datatypes.Angle")? + .copied() + .map(|radians| Self { radians }) + .collect::>() } }) } diff --git a/crates/store/re_types/src/datatypes/angle_ext.rs b/crates/store/re_types/src/datatypes/angle_ext.rs index fba9a12eea70..b6a98f9f8f4f 100644 --- a/crates/store/re_types/src/datatypes/angle_ext.rs +++ b/crates/store/re_types/src/datatypes/angle_ext.rs @@ -2,36 +2,60 @@ use super::Angle; use std::fmt::Formatter; impl Angle { - /// Angle in radians independent of the underlying representation. + /// Angle in radians. #[inline] pub fn radians(&self) -> f32 { - match self { - Self::Radians(v) => *v, - Self::Degrees(v) => v.to_radians(), - } + self.radians } - /// Angle in degrees independent of the underlying representation. + /// Angle in degrees (converts from radians). #[inline] pub fn degrees(&self) -> f32 { - match self { - Self::Radians(v) => v.to_degrees(), - Self::Degrees(v) => *v, + self.radians.to_degrees() + } + + /// Create a new angle from degrees. + /// + /// Converts the value to radians. + /// + /// Deprecated method to mimic previous enum variant. + #[allow(non_snake_case)] + #[deprecated(since = "0.18.0", note = "Use `Angle::from_degrees` instead.")] + #[inline] + pub fn Degrees(degrees: f32) -> Self { + Self::from_degrees(degrees) + } + + /// Create a new angle from radians. + /// + /// Deprecated method to mimic previous enum variant. + #[allow(non_snake_case)] + #[deprecated(since = "0.18.0", note = "Use `Angle::from_radians` instead.")] + #[inline] + pub fn Radians(radians: f32) -> Self { + Self { radians } + } + + /// Create a new angle from degrees. + /// + /// Converts the value to radians. + #[inline] + pub fn from_degrees(degrees: f32) -> Self { + Self { + radians: degrees.to_radians(), } } + + /// Create a new angle from radians. + #[inline] + pub fn from_radians(radians: f32) -> Self { + Self { radians } + } } impl std::fmt::Display for Angle { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Radians(v) => { - write!(f, "{} rad", re_format::format_f32(*v)) - } - Self::Degrees(v) => { - // TODO(andreas): Convert to arc minutes/seconds for very small angles. - // That code should be in re_format! - write!(f, "{} °", re_format::format_f32(*v)) - } - } + let prec = f.precision().unwrap_or(crate::DEFAULT_DISPLAY_DECIMALS); + write!(f, "{:.prec$} rad", re_format::format_f32(self.radians)) } } diff --git a/crates/store/re_types/src/datatypes/rotation_axis_angle.rs b/crates/store/re_types/src/datatypes/rotation_axis_angle.rs index d6bec3dc805b..6e2ef8502e8e 100644 --- a/crates/store/re_types/src/datatypes/rotation_axis_angle.rs +++ b/crates/store/re_types/src/datatypes/rotation_axis_angle.rs @@ -148,10 +148,15 @@ impl ::re_types_core::Loggable for RotationAxisAngle { let any_nones = somes.iter().any(|some| !*some); any_nones.then(|| somes.into()) }; - { - _ = angle_bitmap; - crate::datatypes::Angle::to_arrow_opt(angle)? - } + PrimitiveArray::new( + DataType::Float32, + angle + .into_iter() + .map(|datum| datum.map(|datum| datum.radians).unwrap_or_default()) + .collect(), + angle_bitmap, + ) + .boxed() }, ], bitmap, @@ -278,9 +283,20 @@ impl ::re_types_core::Loggable for RotationAxisAngle { .with_context("rerun.datatypes.RotationAxisAngle"); } let arrow_data = &**arrays_by_name["angle"]; - crate::datatypes::Angle::from_arrow_opt(arrow_data) + arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = DataType::Float32; + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) .with_context("rerun.datatypes.RotationAxisAngle#angle")? .into_iter() + .map(|opt| opt.copied()) + .map(|res_or_opt| { + res_or_opt.map(|radians| crate::datatypes::Angle { radians }) + }) }; arrow2::bitmap::utils::ZipValidity::new_with_validity( ::itertools::izip!(axis, angle), diff --git a/crates/store/re_types/tests/asset3d.rs b/crates/store/re_types/tests/asset3d.rs index b01b524dbc79..113b6c6e34f0 100644 --- a/crates/store/re_types/tests/asset3d.rs +++ b/crates/store/re_types/tests/asset3d.rs @@ -22,7 +22,7 @@ fn roundtrip() { translation: Some(Vec3D([1.0, 2.0, 3.0])), rotation: Some(Rotation3D::AxisAngle(RotationAxisAngle { axis: Vec3D([0.2, 0.2, 0.8]), - angle: Angle::Radians(0.5 * TAU), + angle: Angle::from_radians(0.5 * TAU), })), scale: Some(Scale3D::Uniform(42.0)), from_parent: true, @@ -34,7 +34,7 @@ fn roundtrip() { let arch = Asset3D::from_file_contents(BYTES.to_vec(), Some(MediaType::gltf())).with_transform( re_types::datatypes::Transform3D::from_translation_rotation_scale( [1.0, 2.0, 3.0], - RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::Radians(0.5 * TAU)), + RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::from_radians(0.5 * TAU)), 42.0, ) .from_parent(), diff --git a/crates/store/re_types/tests/box3d.rs b/crates/store/re_types/tests/box3d.rs index b9bd61b53f6d..bfc3e33c33fd 100644 --- a/crates/store/re_types/tests/box3d.rs +++ b/crates/store/re_types/tests/box3d.rs @@ -17,7 +17,7 @@ fn roundtrip() { components::Rotation3D::from(datatypes::Quaternion::from_xyzw([1.0, 2.0, 3.0, 4.0])), components::Rotation3D::from(datatypes::RotationAxisAngle::new( [1.0, 2.0, 3.0], - datatypes::Angle::Radians(4.0), + datatypes::Angle::from_radians(4.0), )), ]), colors: Some(vec![ @@ -44,7 +44,7 @@ fn roundtrip() { components::Rotation3D::from(datatypes::Quaternion::from_xyzw([1.0, 2.0, 3.0, 4.0])), components::Rotation3D::from(datatypes::RotationAxisAngle::new( [1.0, 2.0, 3.0], - datatypes::Angle::Radians(4.0), + datatypes::Angle::from_radians(4.0), )), ]) .with_colors([0xAA0000CC, 0x00BB00DD]) diff --git a/crates/store/re_types/tests/mint_conversions.rs b/crates/store/re_types/tests/mint_conversions.rs index 35fce8927f60..8f19581137f5 100644 --- a/crates/store/re_types/tests/mint_conversions.rs +++ b/crates/store/re_types/tests/mint_conversions.rs @@ -152,7 +152,7 @@ fn rotation3d() { { let datatype: datatypes::Rotation3D = datatypes::RotationAxisAngle { axis: [1.0, 0.0, 0.0].into(), - angle: datatypes::Angle::Degrees(90.0), + angle: datatypes::Angle::from_degrees(90.0), } .into(); let mint: mint::Quaternion = datatype.into(); @@ -175,7 +175,7 @@ fn rotation3d() { { let component: components::Rotation3D = datatypes::RotationAxisAngle { axis: [1.0, 0.0, 0.0].into(), - angle: datatypes::Angle::Degrees(90.0), + angle: datatypes::Angle::from_degrees(90.0), } .into(); let mint: mint::Quaternion = component.into(); diff --git a/crates/store/re_types/tests/transform3d.rs b/crates/store/re_types/tests/transform3d.rs index a8b5cfc77ee7..a0e78a9739b6 100644 --- a/crates/store/re_types/tests/transform3d.rs +++ b/crates/store/re_types/tests/transform3d.rs @@ -46,7 +46,7 @@ fn roundtrip() { translation: None, rotation: Some(Rotation3D::AxisAngle(RotationAxisAngle { axis: Vec3D([0.2, 0.2, 0.8]), - angle: Angle::Radians(0.5 * TAU), + angle: Angle::from_radians(0.5 * TAU), })), scale: None, from_parent: false, @@ -63,7 +63,7 @@ fn roundtrip() { translation: None, rotation: Some(Rotation3D::AxisAngle(RotationAxisAngle { axis: Vec3D([0.2, 0.2, 0.8]), - angle: Angle::Radians(0.5 * TAU), + angle: Angle::from_radians(0.5 * TAU), })), scale: None, from_parent: true, @@ -111,11 +111,11 @@ fn roundtrip() { Transform3D::from_translation_scale([1.0, 2.0, 3.0], Scale3D::uniform(42.0)).from_parent(), // Transform3D::from_translation_rotation( [1.0, 2.0, 3.0], - RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::Radians(0.5 * TAU)), + RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::from_radians(0.5 * TAU)), ), // Transform3D::from_translation_rotation_scale( [1.0, 2.0, 3.0], - RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::Radians(0.5 * TAU)), + RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::from_radians(0.5 * TAU)), 42.0, ) .from_parent(), diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 720774c6214b..9085406b2014 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -602,7 +602,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ArchetypeName::new("rerun.archetypes.Transform3D"), ArchetypeReflection { display_name: "Transform 3D", - docstring_md: "A transform between two 3D spaces, i.e. a pose.\n\nAll components are applied in the inverse order they are listed here.\nE.g. if both a 4x4 matrix with a translation and a translation vector are present,\nthe translation is applied first, followed by the matrix.\n\nEach transform component can be listed multiple times, but transform tree propagation is only possible\nif there's only one instance for each transform component.\nTODO(#6831): write more about the exact interaction with the to be written `OutOfTreeTransform` component.\n\n## Examples\n\n### Variety of 3D transforms\n```ignore\nuse std::f32::consts::TAU;\n\nfn main() -> Result<(), Box> {\n let rec = rerun::RecordingStreamBuilder::new(\"rerun_example_transform3d\").spawn()?;\n\n let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]).with_origins([(0.0, 0.0, 0.0)]);\n\n rec.log(\"base\", &arrow)?;\n\n rec.log(\n \"base/translated\",\n &rerun::Transform3D::from_translation([1.0, 0.0, 0.0]),\n )?;\n\n rec.log(\"base/translated\", &arrow)?;\n\n rec.log(\n \"base/rotated_scaled\",\n &rerun::Transform3D::from_rotation_scale(\n rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::Radians(TAU / 8.0)),\n rerun::Scale3D::from(2.0),\n ),\n )?;\n\n rec.log(\"base/rotated_scaled\", &arrow)?;\n\n Ok(())\n}\n```\n
\n\n \n \n \n \n \n\n
\n\n### Transform hierarchy\n```ignore\nfn main() -> Result<(), Box> {\n let rec = rerun::RecordingStreamBuilder::new(\"rerun_example_transform3d_hierarchy\").spawn()?;\n\n // TODO(#5521): log two space views as in the python example\n\n rec.set_time_seconds(\"sim_time\", 0.0);\n\n // Planetary motion is typically in the XY plane.\n rec.log_static(\"/\", &rerun::ViewCoordinates::RIGHT_HAND_Z_UP)?;\n\n // Setup points, all are in the center of their own space:\n rec.log(\n \"sun\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([1.0])\n .with_colors([rerun::Color::from_rgb(255, 200, 10)]),\n )?;\n rec.log(\n \"sun/planet\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([0.4])\n .with_colors([rerun::Color::from_rgb(40, 80, 200)]),\n )?;\n rec.log(\n \"sun/planet/moon\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([0.15])\n .with_colors([rerun::Color::from_rgb(180, 180, 180)]),\n )?;\n\n // Draw fixed paths where the planet & moon move.\n let d_planet = 6.0;\n let d_moon = 3.0;\n let angles = (0..=100).map(|i| i as f32 * 0.01 * std::f32::consts::TAU);\n let circle: Vec<_> = angles.map(|angle| [angle.sin(), angle.cos()]).collect();\n rec.log(\n \"sun/planet_path\",\n &rerun::LineStrips3D::new([rerun::LineStrip3D::from_iter(\n circle\n .iter()\n .map(|p| [p[0] * d_planet, p[1] * d_planet, 0.0]),\n )]),\n )?;\n rec.log(\n \"sun/planet/moon_path\",\n &rerun::LineStrips3D::new([rerun::LineStrip3D::from_iter(\n circle.iter().map(|p| [p[0] * d_moon, p[1] * d_moon, 0.0]),\n )]),\n )?;\n\n // Movement via transforms.\n for i in 0..(6 * 120) {\n let time = i as f32 / 120.0;\n rec.set_time_seconds(\"sim_time\", time);\n let r_moon = time * 5.0;\n let r_planet = time * 2.0;\n\n rec.log(\n \"sun/planet\",\n &rerun::Transform3D::from_translation_rotation(\n [r_planet.sin() * d_planet, r_planet.cos() * d_planet, 0.0],\n rerun::RotationAxisAngle {\n axis: [1.0, 0.0, 0.0].into(),\n angle: rerun::Angle::Degrees(20.0),\n },\n ),\n )?;\n rec.log(\n \"sun/planet/moon\",\n &rerun::Transform3D::from_translation([\n r_moon.cos() * d_moon,\n r_moon.sin() * d_moon,\n 0.0,\n ])\n .from_parent(),\n )?;\n }\n\n Ok(())\n}\n```\n
\n\n \n \n \n \n \n\n
", + docstring_md: "A transform between two 3D spaces, i.e. a pose.\n\nAll components are applied in the inverse order they are listed here.\nE.g. if both a 4x4 matrix with a translation and a translation vector are present,\nthe translation is applied first, followed by the matrix.\n\nEach transform component can be listed multiple times, but transform tree propagation is only possible\nif there's only one instance for each transform component.\nTODO(#6831): write more about the exact interaction with the to be written `OutOfTreeTransform` component.\n\n## Examples\n\n### Variety of 3D transforms\n```ignore\nuse std::f32::consts::TAU;\n\nfn main() -> Result<(), Box> {\n let rec = rerun::RecordingStreamBuilder::new(\"rerun_example_transform3d\").spawn()?;\n\n let arrow = rerun::Arrows3D::from_vectors([(0.0, 1.0, 0.0)]).with_origins([(0.0, 0.0, 0.0)]);\n\n rec.log(\"base\", &arrow)?;\n\n rec.log(\n \"base/translated\",\n &rerun::Transform3D::from_translation([1.0, 0.0, 0.0]),\n )?;\n\n rec.log(\"base/translated\", &arrow)?;\n\n rec.log(\n \"base/rotated_scaled\",\n &rerun::Transform3D::from_rotation_scale(\n rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::from_radians(TAU / 8.0)),\n rerun::Scale3D::from(2.0),\n ),\n )?;\n\n rec.log(\"base/rotated_scaled\", &arrow)?;\n\n Ok(())\n}\n```\n
\n\n \n \n \n \n \n\n
\n\n### Transform hierarchy\n```ignore\nfn main() -> Result<(), Box> {\n let rec = rerun::RecordingStreamBuilder::new(\"rerun_example_transform3d_hierarchy\").spawn()?;\n\n // TODO(#5521): log two space views as in the python example\n\n rec.set_time_seconds(\"sim_time\", 0.0);\n\n // Planetary motion is typically in the XY plane.\n rec.log_static(\"/\", &rerun::ViewCoordinates::RIGHT_HAND_Z_UP)?;\n\n // Setup points, all are in the center of their own space:\n rec.log(\n \"sun\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([1.0])\n .with_colors([rerun::Color::from_rgb(255, 200, 10)]),\n )?;\n rec.log(\n \"sun/planet\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([0.4])\n .with_colors([rerun::Color::from_rgb(40, 80, 200)]),\n )?;\n rec.log(\n \"sun/planet/moon\",\n &rerun::Points3D::new([[0.0, 0.0, 0.0]])\n .with_radii([0.15])\n .with_colors([rerun::Color::from_rgb(180, 180, 180)]),\n )?;\n\n // Draw fixed paths where the planet & moon move.\n let d_planet = 6.0;\n let d_moon = 3.0;\n let angles = (0..=100).map(|i| i as f32 * 0.01 * std::f32::consts::TAU);\n let circle: Vec<_> = angles.map(|angle| [angle.sin(), angle.cos()]).collect();\n rec.log(\n \"sun/planet_path\",\n &rerun::LineStrips3D::new([rerun::LineStrip3D::from_iter(\n circle\n .iter()\n .map(|p| [p[0] * d_planet, p[1] * d_planet, 0.0]),\n )]),\n )?;\n rec.log(\n \"sun/planet/moon_path\",\n &rerun::LineStrips3D::new([rerun::LineStrip3D::from_iter(\n circle.iter().map(|p| [p[0] * d_moon, p[1] * d_moon, 0.0]),\n )]),\n )?;\n\n // Movement via transforms.\n for i in 0..(6 * 120) {\n let time = i as f32 / 120.0;\n rec.set_time_seconds(\"sim_time\", time);\n let r_moon = time * 5.0;\n let r_planet = time * 2.0;\n\n rec.log(\n \"sun/planet\",\n &rerun::Transform3D::from_translation_rotation(\n [r_planet.sin() * d_planet, r_planet.cos() * d_planet, 0.0],\n rerun::RotationAxisAngle {\n axis: [1.0, 0.0, 0.0].into(),\n angle: rerun::Angle::from_degrees(20.0),\n },\n ),\n )?;\n rec.log(\n \"sun/planet/moon\",\n &rerun::Transform3D::from_translation([\n r_moon.cos() * d_moon,\n r_moon.sin() * d_moon,\n 0.0,\n ])\n .from_parent(),\n )?;\n }\n\n Ok(())\n}\n```\n
\n\n \n \n \n \n \n\n
", fields: vec![ ArchetypeFieldReflection { component_name : "rerun.components.Transform3D".into(), display_name : "Transform", diff --git a/docs/content/reference/migration/migration-0-18.md b/docs/content/reference/migration/migration-0-18.md index 49423e0dcc92..e8969eb48aaa 100644 --- a/docs/content/reference/migration/migration-0-18.md +++ b/docs/content/reference/migration/migration-0-18.md @@ -44,11 +44,14 @@ Instead, there are now several components for translation/scale/rotation/matrice For this purpose `TranslationRotationScale3D` and `TranslationAndMat3x3` datatypes & components have been removed and split up into new components: * [`Translation3D`](https://rerun.io/docs/reference/types/components/translation3d#speculative-link) * [`TransformMat3x3`](https://rerun.io/docs/reference/types/components/transform_mat3x3#speculative-link) -* TODO(andreas): More! +* [`Scale3D`](https://rerun.io/docs/reference/types/components/scale3d#speculative-link) All components are applied to the final transform in the opposite order they're listed in. E.g. if both a 4x4 matrix and a translation is set, the entity is first translated and then transformed with the matrix. If translation, rotation & scale are applied, then (just as in prior versions), from the point of view of the parent space the object is first scaled, then rotated and then translated. +Other changes in data representation: +* Scaling no longer distinguishes uniform and 3D scaling in its data representation, it is now always expressed as 3 floats with the same value. Helper functions are provided to build uniform scales. +* Angles (as used in `RotationAxisAngle`) are now always stored in radians, conversion functions for degrees are provided. Scaling no longer distinguishes uniform and 3D scaling in its data representation. Uniform scaling is now always expressed as 3 floats with the same value. TODO(andreas): Write about OutOfTreeTransform changes and how `Transform3D` has now arrays of components. diff --git a/docs/content/reference/types/datatypes.md b/docs/content/reference/types/datatypes.md index 70dcefb9c06b..4920c78a37bb 100644 --- a/docs/content/reference/types/datatypes.md +++ b/docs/content/reference/types/datatypes.md @@ -7,7 +7,7 @@ order: 3 Data types are the lowest layer of the data model hierarchy. They are re-usable types used by the components. -* [`Angle`](datatypes/angle.md): Angle in either radians or degrees. +* [`Angle`](datatypes/angle.md): Angle in radians. * [`AnnotationInfo`](datatypes/annotation_info.md): Annotation info annotating a class id or key-point id. * [`Blob`](datatypes/blob.md): A binary blob of data. * [`Bool`](datatypes/bool.md): A single boolean. diff --git a/docs/content/reference/types/datatypes/angle.md b/docs/content/reference/types/datatypes/angle.md index b6421e0ab69d..14ea32f9ff57 100644 --- a/docs/content/reference/types/datatypes/angle.md +++ b/docs/content/reference/types/datatypes/angle.md @@ -3,17 +3,16 @@ title: "Angle" --- -Angle in either radians or degrees. +Angle in radians. -## Variants +## Fields -* Radians: `f32` -* Degrees: `f32` +* radians: `f32` ## API reference links * 🌊 [C++ API docs for `Angle`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1datatypes_1_1Angle.html) * 🐍 [Python API docs for `Angle`](https://ref.rerun.io/docs/python/stable/common/datatypes#rerun.datatypes.Angle) - * 🦀 [Rust API docs for `Angle`](https://docs.rs/rerun/latest/rerun/datatypes/enum.Angle.html) + * 🦀 [Rust API docs for `Angle`](https://docs.rs/rerun/latest/rerun/datatypes/struct.Angle.html) ## Used by diff --git a/docs/snippets/all/archetypes/box3d_batch.rs b/docs/snippets/all/archetypes/box3d_batch.rs index 4357bad8dad0..4bd135abcf1a 100644 --- a/docs/snippets/all/archetypes/box3d_batch.rs +++ b/docs/snippets/all/archetypes/box3d_batch.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { .with_rotations([ rerun::Rotation3D::IDENTITY, rerun::Quaternion::from_xyzw([0.0, 0.0, 0.382683, 0.923880]).into(), // 45 degrees around Z - rerun::RotationAxisAngle::new((0.0, 1.0, 0.0), rerun::Angle::Degrees(30.0)).into(), + rerun::RotationAxisAngle::new((0.0, 1.0, 0.0), rerun::Angle::from_degrees(30.0)).into(), ]) .with_radii([0.025]) .with_colors([ diff --git a/docs/snippets/all/archetypes/transform3d_axes.rs b/docs/snippets/all/archetypes/transform3d_axes.rs index c5f886c43043..8694f483931c 100644 --- a/docs/snippets/all/archetypes/transform3d_axes.rs +++ b/docs/snippets/all/archetypes/transform3d_axes.rs @@ -18,7 +18,7 @@ fn main() -> Result<(), Box> { "base/rotated", &rerun::Transform3D::from_rotation(rerun::RotationAxisAngle::new( [1.0, 1.0, 1.0], - rerun::Angle::Degrees(deg as f32), + rerun::Angle::from_degrees(deg as f32), )), )?; rec.log( diff --git a/docs/snippets/all/archetypes/transform3d_hierarchy.rs b/docs/snippets/all/archetypes/transform3d_hierarchy.rs index f5c3e5a9658a..8f8ced25d787 100644 --- a/docs/snippets/all/archetypes/transform3d_hierarchy.rs +++ b/docs/snippets/all/archetypes/transform3d_hierarchy.rs @@ -63,7 +63,7 @@ fn main() -> Result<(), Box> { [r_planet.sin() * d_planet, r_planet.cos() * d_planet, 0.0], rerun::RotationAxisAngle { axis: [1.0, 0.0, 0.0].into(), - angle: rerun::Angle::Degrees(20.0), + angle: rerun::Angle::from_degrees(20.0), }, ), )?; diff --git a/docs/snippets/all/archetypes/transform3d_simple.rs b/docs/snippets/all/archetypes/transform3d_simple.rs index 8f707ddedc6a..a96c68613480 100644 --- a/docs/snippets/all/archetypes/transform3d_simple.rs +++ b/docs/snippets/all/archetypes/transform3d_simple.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { rec.log( "base/rotated_scaled", &rerun::Transform3D::from_rotation_scale( - rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::Radians(TAU / 8.0)), + rerun::RotationAxisAngle::new([0.0, 0.0, 1.0], rerun::Angle::from_radians(TAU / 8.0)), rerun::Scale3D::from(2.0), ), )?; diff --git a/docs/snippets/snippets.toml b/docs/snippets/snippets.toml index 584932851708..7b0362647aa9 100644 --- a/docs/snippets/snippets.toml +++ b/docs/snippets/snippets.toml @@ -135,6 +135,11 @@ quick_start = [ # These examples don't have exactly the same implementation. "py", "rust", ] +"archetypes/transform3d_axes" = [ # TODO(#3235): Degree to radian conversion is slightly different. + "cpp", + "py", + "rust", +] "archetypes/transform3d_hierarchy" = [ # Uses a lot of trigonometry which is surprisingly easy to get the same on Rust & C++, but not on Python/Numpy "py", ] diff --git a/examples/rust/dna/src/main.rs b/examples/rust/dna/src/main.rs index 7f9a838c6cf0..8ffc94009cb6 100644 --- a/examples/rust/dna/src/main.rs +++ b/examples/rust/dna/src/main.rs @@ -80,7 +80,7 @@ fn main() -> Result<(), Box> { "dna/structure", &rerun::archetypes::Transform3D::from_rotation(rerun::RotationAxisAngle::new( glam::Vec3::Z, - rerun::Angle::Radians(time / 4.0 * TAU), + rerun::Angle::from_radians(time / 4.0 * TAU), )), )?; } diff --git a/rerun_cpp/src/rerun/datatypes/angle.cpp b/rerun_cpp/src/rerun/datatypes/angle.cpp index 434118252a0d..93c52c69ab06 100644 --- a/rerun_cpp/src/rerun/datatypes/angle.cpp +++ b/rerun_cpp/src/rerun/datatypes/angle.cpp @@ -10,11 +10,7 @@ namespace rerun::datatypes {} namespace rerun { const std::shared_ptr& Loggable::arrow_datatype() { - static const auto datatype = arrow::dense_union({ - arrow::field("_null_markers", arrow::null(), true, nullptr), - arrow::field("Radians", arrow::float32(), false), - arrow::field("Degrees", arrow::float32(), false), - }); + static const auto datatype = arrow::float32(); return datatype; } @@ -28,7 +24,7 @@ namespace rerun { ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) if (instances && num_instances > 0) { RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( - static_cast(builder.get()), + static_cast(builder.get()), instances, num_instances )); @@ -39,7 +35,7 @@ namespace rerun { } rerun::Error Loggable::fill_arrow_array_builder( - arrow::DenseUnionBuilder* builder, const datatypes::Angle* elements, size_t num_elements + arrow::FloatBuilder* builder, const datatypes::Angle* elements, size_t num_elements ) { if (builder == nullptr) { return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); @@ -51,36 +47,10 @@ namespace rerun { ); } - ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); - for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { - const auto& union_instance = elements[elem_idx]; - ARROW_RETURN_NOT_OK(builder->Append(static_cast(union_instance.get_union_tag())) - ); - - auto variant_index = static_cast(union_instance.get_union_tag()); - auto variant_builder_untyped = builder->child_builder(variant_index).get(); - - using TagType = datatypes::detail::AngleTag; - switch (union_instance.get_union_tag()) { - case TagType::None: { - ARROW_RETURN_NOT_OK(variant_builder_untyped->AppendNull()); - } break; - case TagType::Radians: { - auto variant_builder = - static_cast(variant_builder_untyped); - ARROW_RETURN_NOT_OK( - variant_builder->Append(union_instance.get_union_data().radians) - ); - } break; - case TagType::Degrees: { - auto variant_builder = - static_cast(variant_builder_untyped); - ARROW_RETURN_NOT_OK( - variant_builder->Append(union_instance.get_union_data().degrees) - ); - } break; - } - } + static_assert(sizeof(*elements) == sizeof(elements->angle_radians)); + ARROW_RETURN_NOT_OK( + builder->AppendValues(&elements->angle_radians, static_cast(num_elements)) + ); return Error::ok(); } diff --git a/rerun_cpp/src/rerun/datatypes/angle.hpp b/rerun_cpp/src/rerun/datatypes/angle.hpp index cb22614e8d82..6ead18d43592 100644 --- a/rerun_cpp/src/rerun/datatypes/angle.hpp +++ b/rerun_cpp/src/rerun/datatypes/angle.hpp @@ -6,131 +6,47 @@ #include "../result.hpp" #include -#include #include -#include -#include namespace arrow { + /// \private + template + class NumericBuilder; + class Array; class DataType; - class DenseUnionBuilder; + class FloatType; + using FloatBuilder = NumericBuilder; } // namespace arrow namespace rerun::datatypes { - namespace detail { - /// \private - enum class AngleTag : uint8_t { - /// Having a special empty state makes it possible to implement move-semantics. We need to be able to leave the object in a state which we can run the destructor on. - None = 0, - Radians, - Degrees, - }; - - /// \private - union AngleData { - /// Angle in radians. One turn is equal to 2π (or τ) radians. - float radians; - - /// Angle in degrees. One turn is equal to 360 degrees. - float degrees; - - AngleData() { - std::memset(reinterpret_cast(this), 0, sizeof(AngleData)); - } - - ~AngleData() {} - - void swap(AngleData& other) noexcept { - // This bitwise swap would fail for self-referential types, but we don't have any of those. - char temp[sizeof(AngleData)]; - void* otherbytes = reinterpret_cast(&other); - void* thisbytes = reinterpret_cast(this); - std::memcpy(temp, thisbytes, sizeof(AngleData)); - std::memcpy(thisbytes, otherbytes, sizeof(AngleData)); - std::memcpy(otherbytes, temp, sizeof(AngleData)); - } - }; - } // namespace detail - - /// **Datatype**: Angle in either radians or degrees. + /// **Datatype**: Angle in radians. struct Angle { - Angle() : _tag(detail::AngleTag::None) {} - - /// Copy constructor - Angle(const Angle& other) : _tag(other._tag) { - const void* otherbytes = reinterpret_cast(&other._data); - void* thisbytes = reinterpret_cast(&this->_data); - std::memcpy(thisbytes, otherbytes, sizeof(detail::AngleData)); - } - - Angle& operator=(const Angle& other) noexcept { - Angle tmp(other); - this->swap(tmp); - return *this; - } - - Angle(Angle&& other) noexcept : Angle() { - this->swap(other); - } - - Angle& operator=(Angle&& other) noexcept { - this->swap(other); - return *this; - } - - void swap(Angle& other) noexcept { - std::swap(this->_tag, other._tag); - this->_data.swap(other._data); - } - /// Angle in radians. One turn is equal to 2π (or τ) radians. - static Angle radians(float radians) { - Angle self; - self._tag = detail::AngleTag::Radians; - new (&self._data.radians) float(std::move(radians)); - return self; - } - - /// Angle in degrees. One turn is equal to 360 degrees. - static Angle degrees(float degrees) { - Angle self; - self._tag = detail::AngleTag::Degrees; - new (&self._data.degrees) float(std::move(degrees)); - return self; - } + float angle_radians; - /// Return a pointer to radians if the union is in that state, otherwise `nullptr`. - const float* get_radians() const { - if (_tag == detail::AngleTag::Radians) { - return &_data.radians; - } else { - return nullptr; - } - } - - /// Return a pointer to degrees if the union is in that state, otherwise `nullptr`. - const float* get_degrees() const { - if (_tag == detail::AngleTag::Degrees) { - return &_data.degrees; - } else { - return nullptr; - } - } + public: + // Extensions to generated type defined in 'angle_ext.cpp' - /// \private - const detail::AngleData& get_union_data() const { - return _data; + /// New angle in radians. + static Angle radians(float radians_) { + Angle angle; + angle.angle_radians = radians_; + return angle; } - /// \private - detail::AngleTag get_union_tag() const { - return _tag; + /// New angle in degrees. + /// + /// Converts to radians to store the angle. + static Angle degrees(float degrees_) { + Angle angle; + // Can't use math constants here: `M_PI` doesn't work on all platforms out of the box and std::numbers::pi is C++20. + angle.angle_radians = degrees_ * (3.14159265358979323846264338327950288f / 180.f); + return angle; } - private: - detail::AngleTag _tag; - detail::AngleData _data; + public: + Angle() = default; }; } // namespace rerun::datatypes @@ -153,7 +69,7 @@ namespace rerun { /// Fills an arrow array builder with an array of this type. static rerun::Error fill_arrow_array_builder( - arrow::DenseUnionBuilder* builder, const datatypes::Angle* elements, size_t num_elements + arrow::FloatBuilder* builder, const datatypes::Angle* elements, size_t num_elements ); }; } // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/angle_ext.cpp b/rerun_cpp/src/rerun/datatypes/angle_ext.cpp new file mode 100644 index 000000000000..fe65f735111a --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/angle_ext.cpp @@ -0,0 +1,29 @@ +#include "angle.hpp" + +namespace rerun::datatypes { +#if 0 + + // + + /// New angle in radians. + static Angle radians(float radians_) { + Angle angle; + angle.angle_radians = radians_; + return angle; + } + + /// New angle in degrees. + /// + /// Converts to radians to store the angle. + static Angle degrees(float degrees_) { + Angle angle; + // Can't use math constants here: `M_PI` doesn't work on all platforms out of the box and std::numbers::pi is C++20. + angle.angle_radians = degrees_ * (3.14159265358979323846264338327950288f / 180.f); + return angle; + } + + // + +#endif + +} // namespace rerun::datatypes diff --git a/rerun_cpp/src/rerun/datatypes/rotation_axis_angle.cpp b/rerun_cpp/src/rerun/datatypes/rotation_axis_angle.cpp index 3b59bdd34e38..8f047dd15c0f 100644 --- a/rerun_cpp/src/rerun/datatypes/rotation_axis_angle.cpp +++ b/rerun_cpp/src/rerun/datatypes/rotation_axis_angle.cpp @@ -68,7 +68,7 @@ namespace rerun { } } { - auto field_builder = static_cast(builder->field_builder(1)); + auto field_builder = static_cast(builder->field_builder(1)); ARROW_RETURN_NOT_OK(field_builder->Reserve(static_cast(num_elements))); for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( diff --git a/rerun_py/rerun_sdk/rerun/datatypes/angle.py b/rerun_py/rerun_sdk/rerun/datatypes/angle.py index 8977c8abc176..d933330fec17 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/angle.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/angle.py @@ -5,8 +5,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal, Sequence, Union +from typing import TYPE_CHECKING, Any, Sequence, Union +import numpy as np +import numpy.typing as npt import pyarrow as pa from attrs import define, field @@ -21,65 +23,39 @@ @define(init=False) class Angle(AngleExt): - """**Datatype**: Angle in either radians or degrees.""" + """**Datatype**: Angle in radians.""" # __init__ can be found in angle_ext.py - inner: float = field(converter=float) - """ - Must be one of: + radians: float = field(converter=float) + # Angle in radians. One turn is equal to 2π (or τ) radians. + # + # (Docstring intentionally commented out to hide this field from the docs) - * Radians (float): - Angle in radians. One turn is equal to 2π (or τ) radians. - Only one of `degrees` or `radians` should be set. + def __array__(self, dtype: npt.DTypeLike = None) -> npt.NDArray[Any]: + # You can define your own __array__ function as a member of AngleExt in angle_ext.py + return np.asarray(self.radians, dtype=dtype) - * Degrees (float): - Angle in degrees. One turn is equal to 360 degrees. - Only one of `degrees` or `radians` should be set. - """ + def __float__(self) -> float: + return float(self.radians) - kind: Literal["radians", "degrees"] = field(default="radians") - """ - Possible values: - - * "radians": - Angle in radians. One turn is equal to 2π (or τ) radians. - Only one of `degrees` or `radians` should be set. - - * "degrees": - Angle in degrees. One turn is equal to 360 degrees. - Only one of `degrees` or `radians` should be set. - """ + def __hash__(self) -> int: + return hash(self.radians) if TYPE_CHECKING: - AngleLike = Union[ - Angle, - float, - ] - AngleArrayLike = Union[ - Angle, - float, - Sequence[AngleLike], - ] + AngleLike = Union[Angle, float, int] else: AngleLike = Any - AngleArrayLike = Any + +AngleArrayLike = Union[Angle, Sequence[AngleLike], npt.ArrayLike, Sequence[float], Sequence[int]] class AngleType(BaseExtensionType): _TYPE_NAME: str = "rerun.datatypes.Angle" def __init__(self) -> None: - pa.ExtensionType.__init__( - self, - pa.dense_union([ - pa.field("_null_markers", pa.null(), nullable=True, metadata={}), - pa.field("Radians", pa.float32(), nullable=False, metadata={}), - pa.field("Degrees", pa.float32(), nullable=False, metadata={}), - ]), - self._TYPE_NAME, - ) + pa.ExtensionType.__init__(self, pa.float32(), self._TYPE_NAME) class AngleBatch(BaseBatch[AngleArrayLike]): @@ -87,52 +63,5 @@ class AngleBatch(BaseBatch[AngleArrayLike]): @staticmethod def _native_to_pa_array(data: AngleArrayLike, data_type: pa.DataType) -> pa.Array: - from typing import cast - - # TODO(#2623): There should be a separate overridable `coerce_to_array` method that can be overridden. - # If we can call iter, it may be that one of the variants implements __iter__. - if not hasattr(data, "__iter__") or isinstance(data, (Angle, float)): # type: ignore[arg-type] - data = [data] # type: ignore[list-item] - data = cast(Sequence[AngleLike], data) # type: ignore[redundant-cast] - - types: list[int] = [] - value_offsets: list[int] = [] - - num_nulls = 0 - variant_radians: list[float] = [] - variant_degrees: list[float] = [] - - for value in data: - if value is None: - value_offsets.append(num_nulls) - num_nulls += 1 - types.append(0) - else: - if not isinstance(value, Angle): - value = Angle(value) - if value.kind == "radians": - value_offsets.append(len(variant_radians)) - variant_radians.append(value.inner) # type: ignore[arg-type] - types.append(1) - elif value.kind == "degrees": - value_offsets.append(len(variant_degrees)) - variant_degrees.append(value.inner) # type: ignore[arg-type] - types.append(2) - - buffers = [ - None, - pa.array(types, type=pa.int8()).buffers()[1], - pa.array(value_offsets, type=pa.int32()).buffers()[1], - ] - children = [ - pa.nulls(num_nulls), - pa.array(variant_radians, type=pa.float32()), - pa.array(variant_degrees, type=pa.float32()), - ] - - return pa.UnionArray.from_buffers( - type=data_type, - length=len(data), - buffers=buffers, - children=children, - ) + array = np.asarray(data, dtype=np.float32).flatten() + return pa.array(array, type=data_type) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/angle_ext.py b/rerun_py/rerun_sdk/rerun/datatypes/angle_ext.py index 1c2796b421de..a0b64826a0fd 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/angle_ext.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/angle_ext.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -19,12 +20,13 @@ def __init__(self: Any, rad: float | None = None, deg: float | None = None) -> N Angle in radians, specify either `rad` or `deg`. deg: Angle in degrees, specify either `rad` or `deg`. + Converts the angle to radians internally. """ if rad is not None: - self.__attrs_init__(inner=rad, kind="radians") # pyright: ignore[reportGeneralTypeIssues] + self.radians = rad elif deg is not None: - self.__attrs_init__(inner=deg, kind="degrees") # pyright: ignore[reportGeneralTypeIssues] + self.radians = math.radians(deg) else: raise ValueError("Either `rad` or `deg` must be provided.") diff --git a/rerun_py/rerun_sdk/rerun/datatypes/rotation3d.py b/rerun_py/rerun_sdk/rerun/datatypes/rotation3d.py index 58c67deb7577..e291c4f13839 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/rotation3d.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/rotation3d.py @@ -76,16 +76,7 @@ def __init__(self) -> None: nullable=False, metadata={}, ), - pa.field( - "angle", - pa.dense_union([ - pa.field("_null_markers", pa.null(), nullable=True, metadata={}), - pa.field("Radians", pa.float32(), nullable=False, metadata={}), - pa.field("Degrees", pa.float32(), nullable=False, metadata={}), - ]), - nullable=False, - metadata={}, - ), + pa.field("angle", pa.float32(), nullable=False, metadata={}), ]), nullable=False, metadata={}, diff --git a/rerun_py/rerun_sdk/rerun/datatypes/rotation_axis_angle.py b/rerun_py/rerun_sdk/rerun/datatypes/rotation_axis_angle.py index 1a779fbcb0de..bd97365eeb94 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/rotation_axis_angle.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/rotation_axis_angle.py @@ -76,16 +76,7 @@ def __init__(self) -> None: nullable=False, metadata={}, ), - pa.field( - "angle", - pa.dense_union([ - pa.field("_null_markers", pa.null(), nullable=True, metadata={}), - pa.field("Radians", pa.float32(), nullable=False, metadata={}), - pa.field("Degrees", pa.float32(), nullable=False, metadata={}), - ]), - nullable=False, - metadata={}, - ), + pa.field("angle", pa.float32(), nullable=False, metadata={}), ]), self._TYPE_NAME, ) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/transform3d.py b/rerun_py/rerun_sdk/rerun/datatypes/transform3d.py index afa47ddb2758..a9ec65a80276 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/transform3d.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/transform3d.py @@ -86,16 +86,7 @@ def __init__(self) -> None: nullable=False, metadata={}, ), - pa.field( - "angle", - pa.dense_union([ - pa.field("_null_markers", pa.null(), nullable=True, metadata={}), - pa.field("Radians", pa.float32(), nullable=False, metadata={}), - pa.field("Degrees", pa.float32(), nullable=False, metadata={}), - ]), - nullable=False, - metadata={}, - ), + pa.field("angle", pa.float32(), nullable=False, metadata={}), ]), nullable=False, metadata={}, diff --git a/rerun_py/rerun_sdk/rerun/datatypes/translation_rotation_scale3d.py b/rerun_py/rerun_sdk/rerun/datatypes/translation_rotation_scale3d.py index d3361cdce9fe..6cad3d4baeb6 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/translation_rotation_scale3d.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/translation_rotation_scale3d.py @@ -125,16 +125,7 @@ def __init__(self) -> None: nullable=False, metadata={}, ), - pa.field( - "angle", - pa.dense_union([ - pa.field("_null_markers", pa.null(), nullable=True, metadata={}), - pa.field("Radians", pa.float32(), nullable=False, metadata={}), - pa.field("Degrees", pa.float32(), nullable=False, metadata={}), - ]), - nullable=False, - metadata={}, - ), + pa.field("angle", pa.float32(), nullable=False, metadata={}), ]), nullable=False, metadata={}, diff --git a/rerun_py/tests/unit/test_transform3d.py b/rerun_py/tests/unit/test_transform3d.py index 4860af67a37c..29a96d38e872 100644 --- a/rerun_py/tests/unit/test_transform3d.py +++ b/rerun_py/tests/unit/test_transform3d.py @@ -1,6 +1,7 @@ from __future__ import annotations import itertools +import math from fractions import Fraction from typing import Optional, cast @@ -67,8 +68,6 @@ def assert_correct_rotation3d(rot: Rotation3D | None) -> None: assert np.all(rot.inner.axis.xyz == np.array([1.0, 2.0, 3.0])) assert rot.inner.axis.xyz.dtype == np.float32 assert rot.inner.angle == Angle(4.0) - assert isinstance(rot.inner.angle.inner, float) - assert rot.inner.angle.kind == "radians" else: assert False, f"Unexpected inner type: {type(rot.inner)}" @@ -82,9 +81,7 @@ def test_angle() -> None: ] for a in five_rad: - assert a.inner == 5.0 - assert isinstance(a.inner, float) - assert a.kind == "radians" + assert a.radians == 5.0 five_deg = [ Angle(deg=5), @@ -92,9 +89,7 @@ def test_angle() -> None: ] for a in five_deg: - assert a.inner == 5.0 - assert isinstance(a.inner, float) - assert a.kind == "degrees" + assert a.radians == math.radians(5.0) def test_transform3d() -> None: diff --git a/tests/rust/roundtrips/boxes3d/src/main.rs b/tests/rust/roundtrips/boxes3d/src/main.rs index 3d27750c316c..b6664b0518fa 100644 --- a/tests/rust/roundtrips/boxes3d/src/main.rs +++ b/tests/rust/roundtrips/boxes3d/src/main.rs @@ -22,7 +22,10 @@ fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { .with_centers([[0., 0., 0.], [-1., 1., -2.]]) .with_rotations([ Rotation3D::from(Quaternion::from_xyzw([0., 1., 2., 3.])), - Rotation3D::from(RotationAxisAngle::new([0., 1., 2.], Angle::Degrees(45.))), + Rotation3D::from(RotationAxisAngle::new( + [0., 1., 2.], + Angle::from_degrees(45.), + )), ]) .with_colors([0xAA0000CC, 0x00BB00DD]) .with_labels(["hello", "friend"]) diff --git a/tests/rust/roundtrips/transform3d/src/main.rs b/tests/rust/roundtrips/transform3d/src/main.rs index d3a385bd1d60..fb1955412b6e 100644 --- a/tests/rust/roundtrips/transform3d/src/main.rs +++ b/tests/rust/roundtrips/transform3d/src/main.rs @@ -36,7 +36,7 @@ fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { "transform/rigid", &Transform3D::from_translation_rotation( [1.0, 2.0, 3.0], - RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::Radians(0.5 * TAU)), + RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::from_radians(0.5 * TAU)), ), )?; @@ -44,7 +44,7 @@ fn run(rec: &RecordingStream, _args: &Args) -> anyhow::Result<()> { "transform/affine", &Transform3D::from_translation_rotation_scale( [1.0, 2.0, 3.0], - RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::Radians(0.5 * TAU)), + RotationAxisAngle::new([0.2, 0.2, 0.8], Angle::from_radians(0.5 * TAU)), 42.0, ) .from_parent(), diff --git a/tests/rust/test_api/src/main.rs b/tests/rust/test_api/src/main.rs index bb0b1fd1e534..dd4dd25d8288 100644 --- a/tests/rust/test_api/src/main.rs +++ b/tests/rust/test_api/src/main.rs @@ -429,7 +429,7 @@ fn test_transforms_3d(rec: &RecordingStream) -> anyhow::Result<()> { (time * rotation_speed_planet).cos() * sun_to_planet_distance, 0.0, ], - RotationAxisAngle::new(glam::Vec3::X, Angle::Degrees(20.0)), + RotationAxisAngle::new(glam::Vec3::X, Angle::from_degrees(20.0)), ), )?; diff --git a/tests/rust/test_data_density_graph/src/main.rs b/tests/rust/test_data_density_graph/src/main.rs index e4d04021d417..ff0faaf45bae 100644 --- a/tests/rust/test_data_density_graph/src/main.rs +++ b/tests/rust/test_data_density_graph/src/main.rs @@ -116,7 +116,7 @@ fn log( let components = (0..num_rows_per_chunk).map(|i| { let angle_deg = i as f32 % 360.0; rerun::Transform3D::from_rotation(rerun::Rotation3D::AxisAngle( - ((0.0, 0.0, 1.0), rerun::Angle::Degrees(angle_deg)).into(), + ((0.0, 0.0, 1.0), rerun::Angle::from_degrees(angle_deg)).into(), )) });