diff --git a/Cargo.lock b/Cargo.lock index 927ed6876abc..6251356e79dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4996,6 +4996,15 @@ dependencies = [ "rerun", ] +[[package]] +name = "roundtrip_points3d" +version = "0.8.0-alpha.6" +dependencies = [ + "anyhow", + "clap", + "rerun", +] + [[package]] name = "roundtrip_transform3d" version = "0.8.0-alpha.6" diff --git a/crates/re_types/definitions/rerun/archetypes.fbs b/crates/re_types/definitions/rerun/archetypes.fbs index 94a52a37403d..2224cf09208b 100644 --- a/crates/re_types/definitions/rerun/archetypes.fbs +++ b/crates/re_types/definitions/rerun/archetypes.fbs @@ -1,4 +1,5 @@ include "./archetypes/points2d.fbs"; +include "./archetypes/points3d.fbs"; include "./archetypes/transform3d.fbs"; include "./testing/archetypes/fuzzy.fbs"; diff --git a/crates/re_types/definitions/rerun/archetypes/points3d.fbs b/crates/re_types/definitions/rerun/archetypes/points3d.fbs new file mode 100644 index 000000000000..2375a56c9d9d --- /dev/null +++ b/crates/re_types/definitions/rerun/archetypes/points3d.fbs @@ -0,0 +1,72 @@ +include "fbs/attributes.fbs"; + +include "rerun/datatypes.fbs"; +include "rerun/components.fbs"; + +namespace rerun.archetypes; + +// --- + +/// A 3D point cloud with positions and optional colors, radii, labels, etc. +/// +/// \py Example +/// \py ------- +/// \py +/// \py ```python +/// \py \include:../../../../../docs/code-examples/point3d_simple_v2.py +/// \py ``` +/// +/// \rs ## Example +/// \rs +/// \rs ```ignore +/// \rs \include:../../../../../docs/code-examples/point3d_simple_v2.rs +/// \rs ``` +table Points3D ( + "attr.rust.derive": "PartialEq", + order: 100 +) { + // --- Required --- + + /// All the actual 3D points that make up the point cloud. + points: [rerun.components.Point3D] ("attr.rerun.component_required", order: 1000); + + // --- Recommended --- + + /// Optional radii for the points, effectively turning them into circles. + radii: [rerun.components.Radius] ("attr.rerun.component_recommended", nullable, order: 2000); + + /// Optional colors for the points. + /// + /// \python The colors are interpreted as RGB or RGBA in sRGB gamma-space, + /// \python As either 0-1 floats or 0-255 integers, with separate alpha. + colors: [rerun.components.Color] ("attr.rerun.component_recommended", nullable, order: 2100); + + // --- Optional --- + + /// Optional text labels for the points. + labels: [rerun.components.Label] ("attr.rerun.component_optional", nullable, order: 3000); + + /// An optional floating point value that specifies the 3D drawing order. + /// Objects with higher values are drawn on top of those with lower values. + /// + /// The default for 3D points is 30.0. + draw_order: rerun.components.DrawOrder ("attr.rerun.component_optional", nullable, order: 3100); + + /// Optional class Ids for the points. + /// + /// The class ID provides colors and labels if not specified explicitly. + class_ids: [rerun.components.ClassId] ("attr.rerun.component_optional", nullable, order: 3200); + + /// Optional keypoint IDs for the points, identifying them within a class. + /// + /// If keypoint IDs are passed in but no class IDs were specified, the class ID will + /// default to 0. + /// This is useful to identify points within a single classification (which is identified + /// with `class_id`). + /// E.g. the classification might be 'Person' and the keypoints refer to joints on a + /// detected skeleton. + keypoint_ids: [rerun.components.KeypointId] ("attr.rerun.component_optional", nullable, order: 3300); + + /// Unique identifiers for each individual point in the batch. + instance_keys: [rerun.components.InstanceKey] ("attr.rerun.component_optional", nullable, order: 3400); +} diff --git a/crates/re_types/definitions/rerun/components.fbs b/crates/re_types/definitions/rerun/components.fbs index 66cc65379aa6..a106056475d4 100644 --- a/crates/re_types/definitions/rerun/components.fbs +++ b/crates/re_types/definitions/rerun/components.fbs @@ -5,5 +5,6 @@ include "./components/instance_key.fbs"; include "./components/keypoint_id.fbs"; include "./components/label.fbs"; include "./components/point2d.fbs"; +include "./components/point3d.fbs"; include "./components/radius.fbs"; include "./components/transform3d.fbs"; diff --git a/crates/re_types/definitions/rerun/components/point3d.fbs b/crates/re_types/definitions/rerun/components/point3d.fbs new file mode 100644 index 000000000000..1785c85aefc9 --- /dev/null +++ b/crates/re_types/definitions/rerun/components/point3d.fbs @@ -0,0 +1,21 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "rust/attributes.fbs"; + +include "rerun/datatypes.fbs"; +include "rerun/attributes.fbs"; + +namespace rerun.components; + +// --- + +/// A point in 3D space. +struct Point3D ( + "attr.python.aliases": "npt.NDArray[np.float32], Sequence[float], Tuple[float, float, float]", + "attr.python.array_aliases": "npt.NDArray[np.float32], Sequence[float]", + "attr.rerun.legacy_fqname": "rerun.point3d", + "attr.rust.derive": "Default, Copy, PartialEq, PartialOrd", + order: 100 +) { + xy: rerun.datatypes.Point3D (order: 100); +} diff --git a/crates/re_types/definitions/rerun/datatypes.fbs b/crates/re_types/definitions/rerun/datatypes.fbs index 61ae6c2e0c87..f39cf35273e2 100644 --- a/crates/re_types/definitions/rerun/datatypes.fbs +++ b/crates/re_types/definitions/rerun/datatypes.fbs @@ -2,6 +2,7 @@ include "./datatypes/angle.fbs"; include "./datatypes/mat3x3.fbs"; include "./datatypes/mat4x4.fbs"; include "./datatypes/point2d.fbs"; +include "./datatypes/point3d.fbs"; include "./datatypes/quaternion.fbs"; include "./datatypes/rotation3d.fbs"; include "./datatypes/rotation_axis_angle.fbs"; diff --git a/crates/re_types/definitions/rerun/datatypes/point3d.fbs b/crates/re_types/definitions/rerun/datatypes/point3d.fbs new file mode 100644 index 000000000000..9c94e6b29342 --- /dev/null +++ b/crates/re_types/definitions/rerun/datatypes/point3d.fbs @@ -0,0 +1,20 @@ +include "arrow/attributes.fbs"; +include "python/attributes.fbs"; +include "fbs/attributes.fbs"; +include "rust/attributes.fbs"; + +namespace rerun.datatypes; + +// --- + +/// A point in 3D space. +struct Point3D ( + "attr.python.aliases": "Sequence[float]", + "attr.python.array_aliases": "npt.NDArray[Any], Sequence[npt.NDArray[Any]], Sequence[Tuple[float, float]], Sequence[float]", + "attr.rust.derive": "Default, Copy, PartialEq, PartialOrd", + order: 100 +) { + x: float (order: 100); + y: float (order: 200); + z: float (order: 300); +} diff --git a/crates/re_types/source_hash.txt b/crates/re_types/source_hash.txt index 06e590017def..62691e2e3d22 100644 --- a/crates/re_types/source_hash.txt +++ b/crates/re_types/source_hash.txt @@ -1,4 +1,4 @@ # This is a sha256 hash for all direct and indirect dependencies of this crate's build script. # It can be safely removed at anytime to force the build script to run again. # Check out build.rs to see how it's computed. -94f7feacba1a7d75fee69d10294c77e2f4539106e4906f3d58a5db776e11c4be +173356986894caad5a2b01f9b985726839309968b8466a9f1e19401efe546b87 diff --git a/crates/re_types/src/archetypes/mod.rs b/crates/re_types/src/archetypes/mod.rs index b22ffe6bac10..255acf4f3435 100644 --- a/crates/re_types/src/archetypes/mod.rs +++ b/crates/re_types/src/archetypes/mod.rs @@ -2,8 +2,10 @@ mod fuzzy; mod points2d; +mod points3d; mod transform3d; pub use self::fuzzy::AffixFuzzer1; pub use self::points2d::Points2D; +pub use self::points3d::Points3D; pub use self::transform3d::Transform3D; diff --git a/crates/re_types/src/archetypes/points3d.rs b/crates/re_types/src/archetypes/points3d.rs new file mode 100644 index 000000000000..5afbc582c39c --- /dev/null +++ b/crates/re_types/src/archetypes/points3d.rs @@ -0,0 +1,597 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +#![allow(trivial_numeric_casts)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::iter_on_single_items)] +#![allow(clippy::map_flatten)] +#![allow(clippy::match_wildcard_for_single_variants)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_cast)] + +/// A 3D point cloud with positions and optional colors, radii, labels, etc. +/// +/// ## Example +/// +/// ```ignore +/// //! Log some very simple points. +/// use rerun::{experimental::archetypes::Points3D, MsgSender, RecordingStreamBuilder}; +/// +/// fn main() -> Result<(), Box> { +/// let (rec_stream, storage) = RecordingStreamBuilder::new("points").memory()?; +/// +/// MsgSender::from_archetype("points", &Points3D::new([(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]))? +/// .send(&rec_stream)?; +/// +/// rerun::native_viewer::show(storage.take())?; +/// Ok(()) +/// } +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Points3D { + /// All the actual 3D points that make up the point cloud. + pub points: Vec, + + /// Optional radii for the points, effectively turning them into circles. + pub radii: Option>, + + /// Optional colors for the points. + pub colors: Option>, + + /// Optional text labels for the points. + pub labels: Option>, + + /// An optional floating point value that specifies the 3D drawing order. + /// Objects with higher values are drawn on top of those with lower values. + /// + /// The default for 3D points is 30.0. + pub draw_order: Option, + + /// Optional class Ids for the points. + /// + /// The class ID provides colors and labels if not specified explicitly. + pub class_ids: Option>, + + /// Optional keypoint IDs for the points, identifying them within a class. + /// + /// If keypoint IDs are passed in but no class IDs were specified, the class ID will + /// default to 0. + /// This is useful to identify points within a single classification (which is identified + /// with `class_id`). + /// E.g. the classification might be 'Person' and the keypoints refer to joints on a + /// detected skeleton. + pub keypoint_ids: Option>, + + /// Unique identifiers for each individual point in the batch. + pub instance_keys: Option>, +} + +static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.point3d".into()]); +static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 2usize]> = + once_cell::sync::Lazy::new(|| ["rerun.radius".into(), "rerun.colorrgba".into()]); +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 5usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.label".into(), + "rerun.draw_order".into(), + "rerun.class_id".into(), + "rerun.keypoint_id".into(), + "rerun.instance_key".into(), + ] + }); +static ALL_COMPONENTS: once_cell::sync::Lazy<[crate::ComponentName; 8usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.point3d".into(), + "rerun.radius".into(), + "rerun.colorrgba".into(), + "rerun.label".into(), + "rerun.draw_order".into(), + "rerun.class_id".into(), + "rerun.keypoint_id".into(), + "rerun.instance_key".into(), + ] + }); + +impl Points3D { + pub const NUM_COMPONENTS: usize = 8usize; +} + +impl crate::Archetype for Points3D { + #[inline] + fn name() -> crate::ArchetypeName { + crate::ArchetypeName::Borrowed("rerun.archetypes.Points3D") + } + + #[inline] + fn required_components() -> &'static [crate::ComponentName] { + REQUIRED_COMPONENTS.as_slice() + } + + #[inline] + fn recommended_components() -> &'static [crate::ComponentName] { + RECOMMENDED_COMPONENTS.as_slice() + } + + #[inline] + fn optional_components() -> &'static [crate::ComponentName] { + OPTIONAL_COMPONENTS.as_slice() + } + + #[inline] + fn all_components() -> &'static [crate::ComponentName] { + ALL_COMPONENTS.as_slice() + } + + #[inline] + fn try_to_arrow( + &self, + ) -> crate::SerializationResult< + Vec<(::arrow2::datatypes::Field, Box)>, + > { + use crate::Loggable as _; + Ok([ + { + Some({ + let array = + ::try_to_arrow(self.points.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.Point3D".into(), + Box::new(array.data_type().clone()), + Some("rerun.point3d".into()), + ); + ( + ::arrow2::datatypes::Field::new("points", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#points".into(), + source: Box::new(err), + })? + }, + { + self.radii + .as_ref() + .map(|many| { + let array = ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.Radius".into(), + Box::new(array.data_type().clone()), + Some("rerun.radius".into()), + ); + ( + ::arrow2::datatypes::Field::new("radii", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#radii".into(), + source: Box::new(err), + })? + }, + { + self.colors + .as_ref() + .map(|many| { + let array = ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.Color".into(), + Box::new(array.data_type().clone()), + Some("rerun.colorrgba".into()), + ); + ( + ::arrow2::datatypes::Field::new("colors", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#colors".into(), + source: Box::new(err), + })? + }, + { + self.labels + .as_ref() + .map(|many| { + let array = ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.Label".into(), + Box::new(array.data_type().clone()), + Some("rerun.label".into()), + ); + ( + ::arrow2::datatypes::Field::new("labels", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#labels".into(), + source: Box::new(err), + })? + }, + { + self.draw_order + .as_ref() + .map(|single| { + let array = ::try_to_arrow([single], None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.DrawOrder".into(), + Box::new(array.data_type().clone()), + Some("rerun.draw_order".into()), + ); + ( + ::arrow2::datatypes::Field::new("draw_order", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#draw_order".into(), + source: Box::new(err), + })? + }, + { + self.class_ids + .as_ref() + .map(|many| { + let array = ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.ClassId".into(), + Box::new(array.data_type().clone()), + Some("rerun.class_id".into()), + ); + ( + ::arrow2::datatypes::Field::new("class_ids", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#class_ids".into(), + source: Box::new(err), + })? + }, + { + self.keypoint_ids + .as_ref() + .map(|many| { + let array = + ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.KeypointId".into(), + Box::new(array.data_type().clone()), + Some("rerun.keypoint_id".into()), + ); + ( + ::arrow2::datatypes::Field::new("keypoint_ids", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#keypoint_ids".into(), + source: Box::new(err), + })? + }, + { + self.instance_keys + .as_ref() + .map(|many| { + let array = + ::try_to_arrow(many.iter(), None); + array.map(|array| { + let datatype = ::arrow2::datatypes::DataType::Extension( + "rerun.components.InstanceKey".into(), + Box::new(array.data_type().clone()), + Some("rerun.instance_key".into()), + ); + ( + ::arrow2::datatypes::Field::new("instance_keys", datatype, false), + array, + ) + }) + }) + .transpose() + .map_err(|err| crate::SerializationError::Context { + location: "rerun.archetypes.Points3D#instance_keys".into(), + source: Box::new(err), + })? + }, + ] + .into_iter() + .flatten() + .collect()) + } + + #[inline] + fn try_from_arrow( + data: impl IntoIterator)>, + ) -> crate::DeserializationResult { + use crate::Loggable as _; + let arrays_by_name: ::std::collections::HashMap<_, _> = data + .into_iter() + .map(|(field, array)| (field.name, array)) + .collect(); + let points = { + let array = arrays_by_name + .get("points") + .ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#points".into(), + source: Box::new(err), + })?; + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#points".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#points".into(), + source: Box::new(err), + })? + }; + let radii = if let Some(array) = arrays_by_name.get("radii") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#radii".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#radii".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let colors = if let Some(array) = arrays_by_name.get("colors") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#colors".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#colors".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let labels = if let Some(array) = arrays_by_name.get("labels") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#labels".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#labels".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let draw_order = if let Some(array) = arrays_by_name.get("draw_order") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#draw_order".into(), + source: Box::new(err), + })? + .into_iter() + .next() + .flatten() + .ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#draw_order".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let class_ids = if let Some(array) = arrays_by_name.get("class_ids") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#class_ids".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#class_ids".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let keypoint_ids = if let Some(array) = arrays_by_name.get("keypoint_ids") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#keypoint_ids".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#keypoint_ids".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + let instance_keys = if let Some(array) = arrays_by_name.get("instance_keys") { + Some( + ::try_from_arrow_opt(&**array) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#instance_keys".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.archetypes.Points3D#instance_keys".into(), + source: Box::new(err), + })?, + ) + } else { + None + }; + Ok(Self { + points, + radii, + colors, + labels, + draw_order, + class_ids, + keypoint_ids, + instance_keys, + }) + } +} + +impl Points3D { + pub fn new(points: impl IntoIterator>) -> Self { + Self { + points: points.into_iter().map(Into::into).collect(), + radii: None, + colors: None, + labels: None, + draw_order: None, + class_ids: None, + keypoint_ids: None, + instance_keys: None, + } + } + + pub fn with_radii( + mut self, + radii: impl IntoIterator>, + ) -> Self { + self.radii = Some(radii.into_iter().map(Into::into).collect()); + self + } + + pub fn with_colors( + mut self, + colors: impl IntoIterator>, + ) -> Self { + self.colors = Some(colors.into_iter().map(Into::into).collect()); + self + } + + pub fn with_labels( + mut self, + labels: impl IntoIterator>, + ) -> Self { + self.labels = Some(labels.into_iter().map(Into::into).collect()); + self + } + + pub fn with_draw_order(mut self, draw_order: impl Into) -> Self { + self.draw_order = Some(draw_order.into()); + self + } + + pub fn with_class_ids( + mut self, + class_ids: impl IntoIterator>, + ) -> Self { + self.class_ids = Some(class_ids.into_iter().map(Into::into).collect()); + self + } + + pub fn with_keypoint_ids( + mut self, + keypoint_ids: impl IntoIterator>, + ) -> Self { + self.keypoint_ids = Some(keypoint_ids.into_iter().map(Into::into).collect()); + self + } + + pub fn with_instance_keys( + mut self, + instance_keys: impl IntoIterator>, + ) -> Self { + self.instance_keys = Some(instance_keys.into_iter().map(Into::into).collect()); + self + } +} diff --git a/crates/re_types/src/components/mod.rs b/crates/re_types/src/components/mod.rs index 9a39229240d3..6669a0aead4b 100644 --- a/crates/re_types/src/components/mod.rs +++ b/crates/re_types/src/components/mod.rs @@ -15,6 +15,8 @@ mod label; mod label_ext; mod point2d; mod point2d_ext; +mod point3d; +mod point3d_ext; mod radius; mod radius_ext; mod transform3d; @@ -33,5 +35,6 @@ pub use self::instance_key::InstanceKey; pub use self::keypoint_id::KeypointId; pub use self::label::Label; pub use self::point2d::Point2D; +pub use self::point3d::Point3D; pub use self::radius::Radius; pub use self::transform3d::Transform3D; diff --git a/crates/re_types/src/components/point3d.rs b/crates/re_types/src/components/point3d.rs new file mode 100644 index 000000000000..23f585590808 --- /dev/null +++ b/crates/re_types/src/components/point3d.rs @@ -0,0 +1,138 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +#![allow(trivial_numeric_casts)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::iter_on_single_items)] +#![allow(clippy::map_flatten)] +#![allow(clippy::match_wildcard_for_single_variants)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_cast)] + +/// A point in 3D space. +#[derive(Clone, Debug, Default, Copy, PartialEq, PartialOrd)] +pub struct Point3D(pub crate::datatypes::Point3D); + +impl From for Point3D { + fn from(v: crate::datatypes::Point3D) -> Self { + Self(v) + } +} + +impl<'a> From for ::std::borrow::Cow<'a, Point3D> { + #[inline] + fn from(value: Point3D) -> Self { + std::borrow::Cow::Owned(value) + } +} + +impl<'a> From<&'a Point3D> for ::std::borrow::Cow<'a, Point3D> { + #[inline] + fn from(value: &'a Point3D) -> Self { + std::borrow::Cow::Borrowed(value) + } +} + +impl crate::Loggable for Point3D { + type Name = crate::ComponentName; + #[inline] + fn name() -> Self::Name { + "rerun.point3d".into() + } + + #[allow(unused_imports, clippy::wildcard_imports)] + #[inline] + fn to_arrow_datatype() -> arrow2::datatypes::DataType { + use ::arrow2::datatypes::*; + DataType::Struct(vec![ + Field { + name: "x".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + Field { + name: "y".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + Field { + name: "z".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + ]) + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_to_arrow_opt<'a>( + data: impl IntoIterator>>>, + extension_wrapper: Option<&str>, + ) -> crate::SerializationResult> + where + Self: Clone + 'a, + { + use crate::Loggable as _; + use ::arrow2::{array::*, datatypes::*}; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| { + let Self(data0) = datum.into_owned(); + data0 + }); + (datum.is_some(), datum) + }) + .unzip(); + let data0_bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + _ = data0_bitmap; + _ = extension_wrapper; + crate::datatypes::Point3D::try_to_arrow_opt( + data0, + Some("rerun.components.Point3D"), + )? + } + }) + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_from_arrow_opt( + data: &dyn ::arrow2::array::Array, + ) -> crate::DeserializationResult>> + where + Self: Sized, + { + use crate::Loggable as _; + use ::arrow2::{array::*, datatypes::*}; + Ok(crate::datatypes::Point3D::try_from_arrow_opt(data) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.components.Point3D#xy".into(), + source: Box::new(err), + })? + .into_iter() + .map(|v| { + v.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + }) + .map(|res| res.map(|v| Some(Self(v)))) + .collect::>>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.components.Point3D#xy".into(), + source: Box::new(err), + })?) + } +} + +impl crate::Component for Point3D {} diff --git a/crates/re_types/src/components/point3d_ext.rs b/crates/re_types/src/components/point3d_ext.rs new file mode 100644 index 000000000000..9ed367a43a10 --- /dev/null +++ b/crates/re_types/src/components/point3d_ext.rs @@ -0,0 +1,58 @@ +use super::Point3D; + +// --- + +impl Point3D { + pub const ZERO: Self = Self::new(0.0, 0.0, 0.0); + pub const ONE: Self = Self::new(1.0, 1.0, 1.0); + + #[inline] + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self(crate::datatypes::Point3D::new(x, y, z)) + } + + #[inline] + pub fn x(&self) -> f32 { + self.0.x + } + + #[inline] + pub fn y(&self) -> f32 { + self.0.y + } + + #[inline] + pub fn z(&self) -> f32 { + self.0.z + } +} + +impl From<(f32, f32, f32)> for Point3D { + #[inline] + fn from((x, y, z): (f32, f32, f32)) -> Self { + Self::new(x, y, z) + } +} + +impl From<[f32; 3]> for Point3D { + #[inline] + fn from([x, y, z]: [f32; 3]) -> Self { + Self::new(x, y, z) + } +} + +#[cfg(feature = "glam")] +impl From for Point3D { + #[inline] + fn from(pt: glam::Vec3) -> Self { + Self::new(pt.x, pt.y, pt.z) + } +} + +#[cfg(feature = "glam")] +impl From for glam::Vec3 { + #[inline] + fn from(pt: Point3D) -> Self { + Self::new(pt.x(), pt.y(), pt.z()) + } +} diff --git a/crates/re_types/src/datatypes/mod.rs b/crates/re_types/src/datatypes/mod.rs index 2229fae46c03..157cf6f81faf 100644 --- a/crates/re_types/src/datatypes/mod.rs +++ b/crates/re_types/src/datatypes/mod.rs @@ -8,6 +8,8 @@ mod mat3x3_ext; mod mat4x4; mod point2d; mod point2d_ext; +mod point3d; +mod point3d_ext; mod quaternion; mod quaternion_ext; mod rotation3d; @@ -35,6 +37,7 @@ pub use self::fuzzy::{ pub use self::mat3x3::Mat3x3; pub use self::mat4x4::Mat4x4; pub use self::point2d::Point2D; +pub use self::point3d::Point3D; pub use self::quaternion::Quaternion; pub use self::rotation3d::Rotation3D; pub use self::rotation_axis_angle::RotationAxisAngle; diff --git a/crates/re_types/src/datatypes/point3d.rs b/crates/re_types/src/datatypes/point3d.rs new file mode 100644 index 000000000000..5b25b457ec6d --- /dev/null +++ b/crates/re_types/src/datatypes/point3d.rs @@ -0,0 +1,294 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +#![allow(trivial_numeric_casts)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::iter_on_single_items)] +#![allow(clippy::map_flatten)] +#![allow(clippy::match_wildcard_for_single_variants)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::unnecessary_cast)] + +/// A point in 3D space. +#[derive(Clone, Debug, Default, Copy, PartialEq, PartialOrd)] +pub struct Point3D { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl<'a> From for ::std::borrow::Cow<'a, Point3D> { + #[inline] + fn from(value: Point3D) -> Self { + std::borrow::Cow::Owned(value) + } +} + +impl<'a> From<&'a Point3D> for ::std::borrow::Cow<'a, Point3D> { + #[inline] + fn from(value: &'a Point3D) -> Self { + std::borrow::Cow::Borrowed(value) + } +} + +impl crate::Loggable for Point3D { + type Name = crate::DatatypeName; + #[inline] + fn name() -> Self::Name { + "rerun.datatypes.Point3D".into() + } + + #[allow(unused_imports, clippy::wildcard_imports)] + #[inline] + fn to_arrow_datatype() -> arrow2::datatypes::DataType { + use ::arrow2::datatypes::*; + DataType::Struct(vec![ + Field { + name: "x".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + Field { + name: "y".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + Field { + name: "z".to_owned(), + data_type: DataType::Float32, + is_nullable: false, + metadata: [].into(), + }, + ]) + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_to_arrow_opt<'a>( + data: impl IntoIterator>>>, + extension_wrapper: Option<&str>, + ) -> crate::SerializationResult> + where + Self: Clone + 'a, + { + use crate::Loggable as _; + use ::arrow2::{array::*, datatypes::*}; + Ok({ + let (somes, data): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + (datum.is_some(), datum) + }) + .unzip(); + let bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + StructArray::new( + (if let Some(ext) = extension_wrapper { + DataType::Extension( + ext.to_owned(), + Box::new(::to_arrow_datatype()), + None, + ) + } else { + ::to_arrow_datatype() + }) + .to_logical_type() + .clone(), + vec![ + { + let (somes, x): (Vec<_>, Vec<_>) = data + .iter() + .map(|datum| { + let datum = datum.as_ref().map(|datum| { + let Self { x, .. } = &**datum; + x.clone() + }); + (datum.is_some(), datum) + }) + .unzip(); + let x_bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + PrimitiveArray::new( + { + _ = extension_wrapper; + DataType::Float32.to_logical_type().clone() + }, + x.into_iter().map(|v| v.unwrap_or_default()).collect(), + x_bitmap, + ) + .boxed() + }, + { + let (somes, y): (Vec<_>, Vec<_>) = data + .iter() + .map(|datum| { + let datum = datum.as_ref().map(|datum| { + let Self { y, .. } = &**datum; + y.clone() + }); + (datum.is_some(), datum) + }) + .unzip(); + let y_bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + PrimitiveArray::new( + { + _ = extension_wrapper; + DataType::Float32.to_logical_type().clone() + }, + y.into_iter().map(|v| v.unwrap_or_default()).collect(), + y_bitmap, + ) + .boxed() + }, + { + let (somes, z): (Vec<_>, Vec<_>) = data + .iter() + .map(|datum| { + let datum = datum.as_ref().map(|datum| { + let Self { z, .. } = &**datum; + z.clone() + }); + (datum.is_some(), datum) + }) + .unzip(); + let z_bitmap: Option<::arrow2::bitmap::Bitmap> = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + PrimitiveArray::new( + { + _ = extension_wrapper; + DataType::Float32.to_logical_type().clone() + }, + z.into_iter().map(|v| v.unwrap_or_default()).collect(), + z_bitmap, + ) + .boxed() + }, + ], + bitmap, + ) + .boxed() + }) + } + + #[allow(unused_imports, clippy::wildcard_imports)] + fn try_from_arrow_opt( + data: &dyn ::arrow2::array::Array, + ) -> crate::DeserializationResult>> + where + Self: Sized, + { + use crate::Loggable as _; + use ::arrow2::{array::*, datatypes::*}; + Ok({ + let data = data + .as_any() + .downcast_ref::<::arrow2::array::StructArray>() + .ok_or_else(|| crate::DeserializationError::DatatypeMismatch { + expected: data.data_type().clone(), + got: data.data_type().clone(), + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.datatypes.Point3D".into(), + source: Box::new(err), + })?; + if data.is_empty() { + Vec::new() + } else { + let (data_fields, data_arrays, data_bitmap) = + (data.fields(), data.values(), data.validity()); + let is_valid = |i| data_bitmap.map_or(true, |bitmap| bitmap.get_bit(i)); + let arrays_by_name: ::std::collections::HashMap<_, _> = data_fields + .iter() + .map(|field| field.name.as_str()) + .zip(data_arrays) + .collect(); + let x = { + let data = &**arrays_by_name["x"]; + + data.as_any() + .downcast_ref::() + .unwrap() + .into_iter() + .map(|v| v.copied()) + }; + let y = { + let data = &**arrays_by_name["y"]; + + data.as_any() + .downcast_ref::() + .unwrap() + .into_iter() + .map(|v| v.copied()) + }; + let z = { + let data = &**arrays_by_name["z"]; + + data.as_any() + .downcast_ref::() + .unwrap() + .into_iter() + .map(|v| v.copied()) + }; + ::itertools::izip!(x, y, z) + .enumerate() + .map(|(i, (x, y, z))| { + is_valid(i) + .then(|| { + Ok(Self { + x: x.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| { + crate::DeserializationError::Context { + location: "rerun.datatypes.Point3D#x".into(), + source: Box::new(err), + } + })?, + y: y.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| { + crate::DeserializationError::Context { + location: "rerun.datatypes.Point3D#y".into(), + source: Box::new(err), + } + })?, + z: z.ok_or_else(|| crate::DeserializationError::MissingData { + backtrace: ::backtrace::Backtrace::new_unresolved(), + }) + .map_err(|err| { + crate::DeserializationError::Context { + location: "rerun.datatypes.Point3D#z".into(), + source: Box::new(err), + } + })?, + }) + }) + .transpose() + }) + .collect::>>() + .map_err(|err| crate::DeserializationError::Context { + location: "rerun.datatypes.Point3D".into(), + source: Box::new(err), + })? + } + }) + } +} + +impl crate::Datatype for Point3D {} diff --git a/crates/re_types/src/datatypes/point3d_ext.rs b/crates/re_types/src/datatypes/point3d_ext.rs new file mode 100644 index 000000000000..898208dba152 --- /dev/null +++ b/crates/re_types/src/datatypes/point3d_ext.rs @@ -0,0 +1,43 @@ +use super::Point3D; + +// --- + +impl Point3D { + pub const ZERO: Self = Self::new(0.0, 0.0, 0.0); + pub const ONE: Self = Self::new(1.0, 1.0, 1.0); + + #[inline] + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } +} + +impl From<(f32, f32, f32)> for Point3D { + #[inline] + fn from((x, y, z): (f32, f32, f32)) -> Self { + Self { x, y, z } + } +} + +impl From<[f32; 3]> for Point3D { + #[inline] + fn from([x, y, z]: [f32; 3]) -> Self { + Self { x, y, z } + } +} + +#[cfg(feature = "glam")] +impl From for Point3D { + #[inline] + fn from(pt: glam::Vec3) -> Self { + Self::new(pt.x, pt.y, pt.z) + } +} + +#[cfg(feature = "glam")] +impl From for glam::Vec3 { + #[inline] + fn from(pt: Point3D) -> Self { + Self::new(pt.x, pt.y, pt.z) + } +} diff --git a/crates/re_types/tests/points3d.rs b/crates/re_types/tests/points3d.rs new file mode 100644 index 000000000000..6c771940d0b8 --- /dev/null +++ b/crates/re_types/tests/points3d.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use re_types::{archetypes::Points3D, components, Archetype as _}; + +#[test] +fn roundtrip() { + let expected = Points3D { + points: vec![ + components::Point3D::new(1.0, 2.0, 3.0), // + components::Point3D::new(4.0, 5.0, 6.0), + ], + radii: Some(vec![ + components::Radius(42.0), // + components::Radius(43.0), + ]), + colors: Some(vec![ + components::Color::from_unmultiplied_rgba(0xAA, 0x00, 0x00, 0xCC), // + components::Color::from_unmultiplied_rgba(0x00, 0xBB, 0x00, 0xDD), + ]), + labels: Some(vec![ + components::Label("hello".to_owned()), // + components::Label("friend".to_owned()), // + ]), + draw_order: Some(components::DrawOrder(300.0)), + class_ids: Some(vec![ + components::ClassId(126), // + components::ClassId(127), // + ]), + keypoint_ids: Some(vec![ + components::KeypointId(2), // + components::KeypointId(3), // + ]), + instance_keys: Some(vec![ + components::InstanceKey(u64::MAX - 1), // + components::InstanceKey(u64::MAX), + ]), + }; + + let arch = Points3D::new([(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]) + .with_radii([42.0, 43.0]) + .with_colors([0xAA0000CC, 0x00BB00DD]) + .with_labels(["hello", "friend"]) + .with_draw_order(300.0) + .with_class_ids([126, 127]) + .with_keypoint_ids([2, 3]) + .with_instance_keys([u64::MAX - 1, u64::MAX]); + similar_asserts::assert_eq!(expected, arch); + + let expected_extensions: HashMap<_, _> = [ + ("points", vec!["rerun.components.Point3D"]), + ("radii", vec!["rerun.components.Radius"]), + ("colors", vec!["rerun.components.Color"]), + ("labels", vec!["rerun.components.Label"]), + ("draw_order", vec!["rerun.components.DrawOrder"]), + ("class_ids", vec!["rerun.components.ClassId"]), + ("keypoint_ids", vec!["rerun.components.KeypointId"]), + ("instance_keys", vec!["rerun.components.InstanceKey"]), + ] + .into(); + + eprintln!("arch = {arch:#?}"); + let serialized = arch.to_arrow(); + for (field, array) in &serialized { + // NOTE: Keep those around please, very useful when debugging. + // eprintln!("field = {field:#?}"); + // eprintln!("array = {array:#?}"); + eprintln!("{} = {array:#?}", field.name); + + // TODO(cmc): Re-enable extensions and these assertions once `arrow2-convert` + // has been fully replaced. + if false { + util::assert_extensions( + &**array, + expected_extensions[field.name.as_str()].as_slice(), + ); + } + } + + let deserialized = Points3D::from_arrow(serialized); + similar_asserts::assert_eq!(expected, deserialized); +} + +mod util; diff --git a/docs/code-examples/Cargo.toml b/docs/code-examples/Cargo.toml index af8d65092643..ac144b2447d2 100644 --- a/docs/code-examples/Cargo.toml +++ b/docs/code-examples/Cargo.toml @@ -83,10 +83,18 @@ path = "point2d_random_v2.rs" name = "point3d_random" path = "point3d_random.rs" +[[bin]] +name = "point3d_random_v2" +path = "point3d_random_v2.rs" + [[bin]] name = "point3d_simple" path = "point3d_simple.rs" +[[bin]] +name = "point3d_simple_v2" +path = "point3d_simple_v2.rs" + [[bin]] name = "rect2d_simple" path = "rect2d_simple.rs" diff --git a/docs/code-examples/point3d_random_v2.py b/docs/code-examples/point3d_random_v2.py new file mode 100644 index 000000000000..2d51bc6a5209 --- /dev/null +++ b/docs/code-examples/point3d_random_v2.py @@ -0,0 +1,13 @@ +"""Log some random points with color and radii.""" +import rerun as rr +import rerun.experimental as rr_exp +from numpy.random import default_rng + +rr.init("points", spawn=True) +rng = default_rng(12345) + +positions = rng.uniform(-5, 5, size=[10, 3]) +colors = rng.uniform(0, 255, size=[10, 3]) +radii = rng.uniform(0, 1, size=[10]) + +rr_exp.log_any("random", rr_exp.Points3D(positions, colors=colors, radii=radii)) diff --git a/docs/code-examples/point3d_random_v2.rs b/docs/code-examples/point3d_random_v2.rs new file mode 100644 index 000000000000..3838ab610c3f --- /dev/null +++ b/docs/code-examples/point3d_random_v2.rs @@ -0,0 +1,25 @@ +//! Log some random points with color and radii.""" +use rand::distributions::Uniform; +use rand::Rng; +use rerun::{ + experimental::{archetypes::Points3D, components::Color}, + MsgSender, RecordingStreamBuilder, +}; + +fn main() -> Result<(), Box> { + let (rec_stream, storage) = RecordingStreamBuilder::new("points").memory()?; + + let mut rng = rand::thread_rng(); + let dist = Uniform::new(-5., 5.); + + MsgSender::from_archetype( + "random", + &Points3D::new((0..10).map(|_| (rng.sample(dist), rng.sample(dist), rng.sample(dist)))) + .with_colors((0..10).map(|_| Color::from_rgb(rng.gen(), rng.gen(), rng.gen()))) + .with_radii((0..10).map(|_| rng.gen::())), + )? + .send(&rec_stream)?; + + rerun::native_viewer::show(storage.take())?; + Ok(()) +} diff --git a/docs/code-examples/point3d_simple.rs b/docs/code-examples/point3d_simple.rs index 49140426fdfb..8cbf6ec4263b 100644 --- a/docs/code-examples/point3d_simple.rs +++ b/docs/code-examples/point3d_simple.rs @@ -1,26 +1,18 @@ //! Log some very simple points. -use rerun::{ - components::{Point2D, Rect2D, Vec4D}, - MsgSender, RecordingStreamBuilder, -}; +use rerun::{components::Point3D, MsgSender, RecordingStreamBuilder}; fn main() -> Result<(), Box> { let (rec_stream, storage) = RecordingStreamBuilder::new("points").memory()?; - let points = [[0.0, 0.0], [1.0, 1.0]] + let points = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]] .into_iter() - .map(Point2D::from) + .map(Point3D::from) .collect::>(); MsgSender::new("points") .with_component(&points)? .send(&rec_stream)?; - // Log an extra rect to set the view bounds - MsgSender::new("bounds") - .with_component(&[Rect2D::XCYCWH(Vec4D([0.0, 0.0, 4.0, 3.0]))])? - .send(&rec_stream)?; - rerun::native_viewer::show(storage.take())?; Ok(()) } diff --git a/docs/code-examples/point3d_simple_v2.py b/docs/code-examples/point3d_simple_v2.py new file mode 100644 index 000000000000..fecd124abde2 --- /dev/null +++ b/docs/code-examples/point3d_simple_v2.py @@ -0,0 +1,7 @@ +"""Log some very simple points.""" +import rerun as rr +import rerun.experimental as rr_exp + +rr.init("points", spawn=True) + +rr_exp.log_any("simple", rr_exp.Points3D([[0, 0, 0], [1, 1, 1]])) diff --git a/docs/code-examples/point3d_simple_v2.rs b/docs/code-examples/point3d_simple_v2.rs new file mode 100644 index 000000000000..c249a6a0b193 --- /dev/null +++ b/docs/code-examples/point3d_simple_v2.rs @@ -0,0 +1,12 @@ +//! Log some very simple points. +use rerun::{experimental::archetypes::Points3D, MsgSender, RecordingStreamBuilder}; + +fn main() -> Result<(), Box> { + let (rec_stream, storage) = RecordingStreamBuilder::new("points").memory()?; + + MsgSender::from_archetype("points", &Points3D::new([(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]))? + .send(&rec_stream)?; + + rerun::native_viewer::show(storage.take())?; + Ok(()) +} diff --git a/rerun_cpp/src/archetypes.hpp b/rerun_cpp/src/archetypes.hpp index e69cd7d5414f..e1f509fd7d78 100644 --- a/rerun_cpp/src/archetypes.hpp +++ b/rerun_cpp/src/archetypes.hpp @@ -4,4 +4,5 @@ #include "archetypes/affix_fuzzer1.hpp" #include "archetypes/points2d.hpp" +#include "archetypes/points3d.hpp" #include "archetypes/transform3d.hpp" diff --git a/rerun_cpp/src/archetypes/points3d.cpp b/rerun_cpp/src/archetypes/points3d.cpp new file mode 100644 index 000000000000..c1830b732e53 --- /dev/null +++ b/rerun_cpp/src/archetypes/points3d.cpp @@ -0,0 +1,8 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/archetypes/points3d.fbs" + +#include "points3d.hpp" + +namespace rr { + namespace archetypes {} +} // namespace rr diff --git a/rerun_cpp/src/archetypes/points3d.hpp b/rerun_cpp/src/archetypes/points3d.hpp new file mode 100644 index 000000000000..841b6a0d709b --- /dev/null +++ b/rerun_cpp/src/archetypes/points3d.hpp @@ -0,0 +1,59 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/archetypes/points3d.fbs" + +#pragma once + +#include "../components/class_id.hpp" +#include "../components/color.hpp" +#include "../components/draw_order.hpp" +#include "../components/instance_key.hpp" +#include "../components/keypoint_id.hpp" +#include "../components/label.hpp" +#include "../components/point3d.hpp" +#include "../components/radius.hpp" + +#include +#include +#include + +namespace rr { + namespace archetypes { + /// A 3D point cloud with positions and optional colors, radii, labels, etc. + struct Points3D { + /// All the actual 3D points that make up the point cloud. + std::vector points; + + /// Optional radii for the points, effectively turning them into circles. + std::optional> radii; + + /// Optional colors for the points. + std::optional> colors; + + /// Optional text labels for the points. + std::optional> labels; + + /// An optional floating point value that specifies the 3D drawing order. + /// Objects with higher values are drawn on top of those with lower values. + /// + /// The default for 3D points is 30.0. + std::optional draw_order; + + /// Optional class Ids for the points. + /// + /// The class ID provides colors and labels if not specified explicitly. + std::optional> class_ids; + + /// Optional keypoint IDs for the points, identifying them within a class. + /// + /// If keypoint IDs are passed in but no class IDs were specified, the class ID will + /// default to 0. + /// This is useful to identify points within a single classification (which is + /// identified with `class_id`). E.g. the classification might be 'Person' and the + /// keypoints refer to joints on a detected skeleton. + std::optional> keypoint_ids; + + /// Unique identifiers for each individual point in the batch. + std::optional> instance_keys; + }; + } // namespace archetypes +} // namespace rr diff --git a/rerun_cpp/src/components.hpp b/rerun_cpp/src/components.hpp index d72a4e6bbe63..5ce763a6c17a 100644 --- a/rerun_cpp/src/components.hpp +++ b/rerun_cpp/src/components.hpp @@ -28,5 +28,6 @@ #include "components/keypoint_id.hpp" #include "components/label.hpp" #include "components/point2d.hpp" +#include "components/point3d.hpp" #include "components/radius.hpp" #include "components/transform3d.hpp" diff --git a/rerun_cpp/src/components/point3d.cpp b/rerun_cpp/src/components/point3d.cpp new file mode 100644 index 000000000000..ff4b363341bb --- /dev/null +++ b/rerun_cpp/src/components/point3d.cpp @@ -0,0 +1,16 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/components/point3d.fbs" + +#include "point3d.hpp" + +#include "../datatypes/point3d.hpp" + +#include + +namespace rr { + namespace components { + std::shared_ptr Point3D::to_arrow_datatype() { + return rr::datatypes::Point3D::to_arrow_datatype(); + } + } // namespace components +} // namespace rr diff --git a/rerun_cpp/src/components/point3d.hpp b/rerun_cpp/src/components/point3d.hpp new file mode 100644 index 000000000000..f3f28c0f0891 --- /dev/null +++ b/rerun_cpp/src/components/point3d.hpp @@ -0,0 +1,29 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/components/point3d.fbs" + +#pragma once + +#include "../datatypes/point3d.hpp" + +#include +#include +#include + +namespace arrow { + class DataType; +} + +namespace rr { + namespace components { + /// A point in 3D space. + struct Point3D { + rr::datatypes::Point3D xy; + + public: + Point3D(rr::datatypes::Point3D xy) : xy(std::move(xy)) {} + + /// Returns the arrow data type this type corresponds to. + static std::shared_ptr to_arrow_datatype(); + }; + } // namespace components +} // namespace rr diff --git a/rerun_cpp/src/datatypes.hpp b/rerun_cpp/src/datatypes.hpp index 65bfaa796e8e..d83dbcdbef83 100644 --- a/rerun_cpp/src/datatypes.hpp +++ b/rerun_cpp/src/datatypes.hpp @@ -12,6 +12,7 @@ #include "datatypes/mat3x3.hpp" #include "datatypes/mat4x4.hpp" #include "datatypes/point2d.hpp" +#include "datatypes/point3d.hpp" #include "datatypes/quaternion.hpp" #include "datatypes/rotation3d.hpp" #include "datatypes/rotation_axis_angle.hpp" diff --git a/rerun_cpp/src/datatypes/point3d.cpp b/rerun_cpp/src/datatypes/point3d.cpp new file mode 100644 index 000000000000..f6bae5a0411b --- /dev/null +++ b/rerun_cpp/src/datatypes/point3d.cpp @@ -0,0 +1,18 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/datatypes/point3d.fbs" + +#include "point3d.hpp" + +#include + +namespace rr { + namespace datatypes { + std::shared_ptr Point3D::to_arrow_datatype() { + return arrow::struct_({ + arrow::field("x", arrow::float32(), false, nullptr), + arrow::field("y", arrow::float32(), false, nullptr), + arrow::field("z", arrow::float32(), false, nullptr), + }); + } + } // namespace datatypes +} // namespace rr diff --git a/rerun_cpp/src/datatypes/point3d.hpp b/rerun_cpp/src/datatypes/point3d.hpp new file mode 100644 index 000000000000..78b6e9b04e0b --- /dev/null +++ b/rerun_cpp/src/datatypes/point3d.hpp @@ -0,0 +1,28 @@ +// NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. +// Based on "crates/re_types/definitions/rerun/datatypes/point3d.fbs" + +#pragma once + +#include +#include + +namespace arrow { + class DataType; +} + +namespace rr { + namespace datatypes { + /// A point in 3D space. + struct Point3D { + float x; + + float y; + + float z; + + public: + /// Returns the arrow data type this type corresponds to. + static std::shared_ptr to_arrow_datatype(); + }; + } // namespace datatypes +} // namespace rr diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/__init__.py index d33d6f1f9829..2b77ae243ed7 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/__init__.py @@ -2,6 +2,6 @@ from __future__ import annotations -__all__ = ["AffixFuzzer1", "Points2D", "Transform3D"] +__all__ = ["AffixFuzzer1", "Points2D", "Points3D", "Transform3D"] -from .archetypes import AffixFuzzer1, Points2D, Transform3D +from .archetypes import AffixFuzzer1, Points2D, Points3D, Transform3D diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/__init__.py index 70720d6c332e..a70cccfadd1e 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/__init__.py @@ -4,6 +4,7 @@ from .fuzzy import AffixFuzzer1 from .points2d import Points2D +from .points3d import Points3D from .transform3d import Transform3D -__all__ = ["AffixFuzzer1", "Points2D", "Transform3D"] +__all__ = ["AffixFuzzer1", "Points2D", "Points3D", "Transform3D"] diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/points3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/points3d.py new file mode 100644 index 000000000000..e7bb53df9eb8 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_rerun2/archetypes/points3d.py @@ -0,0 +1,119 @@ +# NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +from __future__ import annotations + +from attrs import define, field + +from .. import components +from .._baseclasses import ( + Archetype, +) + +__all__ = ["Points3D"] + + +@define(str=False, repr=False) +class Points3D(Archetype): + """ + A 3D point cloud with positions and optional colors, radii, labels, etc. + + Example + ------- + ```python + import rerun as rr + import rerun.experimental as rr_exp + + rr.init("points", spawn=True) + + rr_exp.log_any("simple", rr_exp.Points3D([[0, 0, 0], [1, 1, 1]])) + ``` + """ + + points: components.Point3DArray = field( + metadata={"component": "primary"}, + converter=components.Point3DArray.from_similar, # type: ignore[misc] + ) + """ + All the actual 3D points that make up the point cloud. + """ + + radii: components.RadiusArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.RadiusArray.from_similar, # type: ignore[misc] + ) + """ + Optional radii for the points, effectively turning them into circles. + """ + + colors: components.ColorArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.ColorArray.from_similar, # type: ignore[misc] + ) + """ + Optional colors for the points. + + The colors are interpreted as RGB or RGBA in sRGB gamma-space, + As either 0-1 floats or 0-255 integers, with separate alpha. + """ + + labels: components.LabelArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.LabelArray.from_similar, # type: ignore[misc] + ) + """ + Optional text labels for the points. + """ + + draw_order: components.DrawOrderArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.DrawOrderArray.from_similar, # type: ignore[misc] + ) + """ + An optional floating point value that specifies the 3D drawing order. + Objects with higher values are drawn on top of those with lower values. + + The default for 3D points is 30.0. + """ + + class_ids: components.ClassIdArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.ClassIdArray.from_similar, # type: ignore[misc] + ) + """ + Optional class Ids for the points. + + The class ID provides colors and labels if not specified explicitly. + """ + + keypoint_ids: components.KeypointIdArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.KeypointIdArray.from_similar, # type: ignore[misc] + ) + """ + Optional keypoint IDs for the points, identifying them within a class. + + If keypoint IDs are passed in but no class IDs were specified, the class ID will + default to 0. + This is useful to identify points within a single classification (which is identified + with `class_id`). + E.g. the classification might be 'Person' and the keypoints refer to joints on a + detected skeleton. + """ + + instance_keys: components.InstanceKeyArray | None = field( + metadata={"component": "secondary"}, + default=None, + converter=components.InstanceKeyArray.from_similar, # type: ignore[misc] + ) + """ + Unique identifiers for each individual point in the batch. + """ + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py index bf9870054250..62c5f65d3945 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/components/__init__.py @@ -79,6 +79,7 @@ from .keypoint_id import KeypointId, KeypointIdArray, KeypointIdArrayLike, KeypointIdLike, KeypointIdType from .label import Label, LabelArray, LabelArrayLike, LabelLike, LabelType from .point2d import Point2DArray, Point2DType +from .point3d import Point3DArray, Point3DType from .radius import Radius, RadiusArray, RadiusArrayLike, RadiusLike, RadiusType from .transform3d import Transform3DArray, Transform3DType @@ -183,6 +184,8 @@ "LabelType", "Point2DArray", "Point2DType", + "Point3DArray", + "Point3DType", "Radius", "RadiusArray", "RadiusArrayLike", diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/components/point3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/components/point3d.py new file mode 100644 index 000000000000..4b63e3cced9f --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_rerun2/components/point3d.py @@ -0,0 +1,28 @@ +# NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + BaseDelegatingExtensionArray, + BaseDelegatingExtensionType, +) + +__all__ = ["Point3DArray", "Point3DType"] + + +class Point3DType(BaseDelegatingExtensionType): + _TYPE_NAME = "rerun.point3d" + _DELEGATED_EXTENSION_TYPE = datatypes.Point3DType + + +class Point3DArray(BaseDelegatingExtensionArray[datatypes.Point3DArrayLike]): + _EXTENSION_NAME = "rerun.point3d" + _EXTENSION_TYPE = Point3DType + _DELEGATED_ARRAY_TYPE = datatypes.Point3DArray + + +Point3DType._ARRAY_TYPE = Point3DArray + +# TODO(cmc): bring back registration to pyarrow once legacy types are gone +# pa.register_extension_type(Point3DType()) diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/__init__.py index 10f998ed4340..10a7a8f056b0 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/__init__.py @@ -38,6 +38,7 @@ from .mat3x3 import Mat3x3, Mat3x3Array, Mat3x3ArrayLike, Mat3x3Like, Mat3x3Type from .mat4x4 import Mat4x4, Mat4x4Array, Mat4x4ArrayLike, Mat4x4Like, Mat4x4Type from .point2d import Point2D, Point2DArray, Point2DArrayLike, Point2DLike, Point2DType +from .point3d import Point3D, Point3DArray, Point3DArrayLike, Point3DLike, Point3DType from .quaternion import Quaternion, QuaternionArray, QuaternionArrayLike, QuaternionLike, QuaternionType from .rotation3d import Rotation3D, Rotation3DArray, Rotation3DArrayLike, Rotation3DLike, Rotation3DType from .rotation_axis_angle import ( @@ -118,6 +119,11 @@ "Point2DArrayLike", "Point2DLike", "Point2DType", + "Point3D", + "Point3DArray", + "Point3DArrayLike", + "Point3DLike", + "Point3DType", "Quaternion", "QuaternionArray", "QuaternionArrayLike", diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/__init__.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/__init__.py index b54c0ddbd6a0..46613af8e01d 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/__init__.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/__init__.py @@ -3,6 +3,7 @@ from .angle import angle_init from .matnxn import mat3x3_coeffs_converter, mat4x4_coeffs_converter from .point2d import point2d_as_array, point2d_native_to_pa_array +from .point3d import point3d_as_array, point3d_native_to_pa_array from .quaternion import quaternion_init from .rotation3d import rotation3d_inner_converter from .rotation_axis_angle import rotationaxisangle_angle_converter diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/point3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/point3d.py new file mode 100644 index 000000000000..df606ed5b205 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/_overrides/point3d.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence + +import numpy as np +import pyarrow as pa + +if TYPE_CHECKING: + import numpy.typing as npt + + from .. import Point3D, Point3DArrayLike + + +NUMPY_VERSION = tuple(map(int, np.version.version.split(".")[:2])) + + +def point3d_as_array(data: Point3D, dtype: npt.DTypeLike = None) -> npt.NDArray[Any]: + return np.array([data.x, data.y, data.z], dtype=dtype) + + +def point3d_native_to_pa_array(data: Point3DArrayLike, data_type: pa.DataType) -> pa.Array: + from .. import Point3D + + # TODO(ab): get rid of this once we drop support for Python 3.8. Make sure to pin numpy>=1.25. + if NUMPY_VERSION < (1, 25): + # Older numpy doesn't seem to support `data` in the form of [Point3D(1, 2), Point3D(3, 4)] + # this happens for python 3.8 (1.25 supports 3.9+) + if isinstance(data, Sequence): + data = [point3d_as_array(p) if isinstance(p, Point3D) else p for p in data] # type: ignore[assignment] + + points = np.asarray(data, dtype=np.float32).reshape((-1, 3)) + return pa.StructArray.from_arrays( + arrays=[pa.array(c, type=pa.float32()) for c in points.T], + fields=list(data_type), + ) diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/fuzzy.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/fuzzy.py index 5bea0163e66c..47a1ea91f26d 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/fuzzy.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/fuzzy.py @@ -208,7 +208,7 @@ def _native_to_pa_array(data: AffixFuzzer2ArrayLike, data_type: pa.DataType) -> @define class AffixFuzzer3: - inner: Union[float, list[datatypes.AffixFuzzer1], npt.NDArray[np.float32]] = field() + inner: float | list[datatypes.AffixFuzzer1] | npt.NDArray[np.float32] = field() """ degrees (float): @@ -323,7 +323,7 @@ def _native_to_pa_array(data: AffixFuzzer3ArrayLike, data_type: pa.DataType) -> @define class AffixFuzzer4: - inner: Union[datatypes.AffixFuzzer3, list[datatypes.AffixFuzzer3]] = field() + inner: datatypes.AffixFuzzer3 | list[datatypes.AffixFuzzer3] = field() """ single_required (datatypes.AffixFuzzer3): diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/point3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/point3d.py new file mode 100644 index 000000000000..5fc17487be80 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/point3d.py @@ -0,0 +1,74 @@ +# NOTE: This file was autogenerated by re_types_builder; DO NOT EDIT. + +from __future__ import annotations + +from typing import Any, Sequence, Tuple, Union + +import numpy.typing as npt +import pyarrow as pa +from attrs import define, field + +from .._baseclasses import ( + BaseExtensionArray, + BaseExtensionType, +) +from ._overrides import point3d_as_array, point3d_native_to_pa_array # noqa: F401 + +__all__ = ["Point3D", "Point3DArray", "Point3DArrayLike", "Point3DLike", "Point3DType"] + + +@define +class Point3D: + """A point in 3D space.""" + + x: float = field(converter=float) + y: float = field(converter=float) + z: float = field(converter=float) + + def __array__(self, dtype: npt.DTypeLike = None) -> npt.NDArray[Any]: + return point3d_as_array(self, dtype=dtype) + + +Point3DLike = Union[Point3D, Sequence[float]] + +Point3DArrayLike = Union[ + Point3D, + Sequence[Point3DLike], + npt.NDArray[Any], + Sequence[npt.NDArray[Any]], + Sequence[Tuple[float, float]], + Sequence[float], +] + + +# --- Arrow support --- + + +class Point3DType(BaseExtensionType): + def __init__(self) -> None: + pa.ExtensionType.__init__( + self, + pa.struct( + [ + pa.field("x", pa.float32(), False, {}), + pa.field("y", pa.float32(), False, {}), + pa.field("z", pa.float32(), False, {}), + ] + ), + "rerun.datatypes.Point3D", + ) + + +class Point3DArray(BaseExtensionArray[Point3DArrayLike]): + _EXTENSION_NAME = "rerun.datatypes.Point3D" + _EXTENSION_TYPE = Point3DType + + @staticmethod + def _native_to_pa_array(data: Point3DArrayLike, data_type: pa.DataType) -> pa.Array: + return point3d_native_to_pa_array(data, data_type) + + +Point3DType._ARRAY_TYPE = Point3DArray + +# TODO(cmc): bring back registration to pyarrow once legacy types are gone +# pa.register_extension_type(Point3DType()) diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/rotation3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/rotation3d.py index 3beed260f921..76a5518a8374 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/rotation3d.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/rotation3d.py @@ -21,7 +21,7 @@ class Rotation3D: """A 3D rotation.""" - inner: Union[datatypes.Quaternion, datatypes.RotationAxisAngle] = field(converter=rotation3d_inner_converter) + inner: datatypes.Quaternion | datatypes.RotationAxisAngle = field(converter=rotation3d_inner_converter) """ Quaternion (datatypes.Quaternion): Rotation defined by a quaternion. diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/scale3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/scale3d.py index 995b695f3e31..d6e3db7632a7 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/scale3d.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/scale3d.py @@ -34,7 +34,7 @@ class Scale3D: ``` """ - inner: Union[datatypes.Vec3D, float] = field(converter=scale3d_inner_converter) + inner: datatypes.Vec3D | float = field(converter=scale3d_inner_converter) """ ThreeD (datatypes.Vec3D): Individual scaling factors for each axis, distorting the original object. diff --git a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/transform3d.py b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/transform3d.py index 0e82fee5e47a..af7d518e3aeb 100644 --- a/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/transform3d.py +++ b/rerun_py/rerun_sdk/rerun/_rerun2/datatypes/transform3d.py @@ -21,7 +21,7 @@ class Transform3D: """Representation of a 3D affine transform.""" - inner: Union[datatypes.TranslationAndMat3x3, datatypes.TranslationRotationScale3D] = field() + inner: datatypes.TranslationAndMat3x3 | datatypes.TranslationRotationScale3D = field() """ TranslationAndMat3x3 (datatypes.TranslationAndMat3x3): diff --git a/rerun_py/rerun_sdk/rerun/experimental.py b/rerun_py/rerun_sdk/rerun/experimental.py index 07778172f290..2e4fe99bd4ea 100644 --- a/rerun_py/rerun_sdk/rerun/experimental.py +++ b/rerun_py/rerun_sdk/rerun/experimental.py @@ -19,6 +19,7 @@ "cmp", "dt", "Points2D", + "Points3D", "Transform3D", "log_any", ] @@ -27,5 +28,5 @@ from ._rerun2 import archetypes as arch from ._rerun2 import components as cmp from ._rerun2 import datatypes as dt -from ._rerun2.archetypes import Points2D, Transform3D +from ._rerun2.archetypes import Points2D, Points3D, Transform3D from ._rerun2.log_any import log_any diff --git a/rerun_py/tests/unit/test_points3d.py b/rerun_py/tests/unit/test_points3d.py new file mode 100644 index 000000000000..58845506b907 --- /dev/null +++ b/rerun_py/tests/unit/test_points3d.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +import itertools +from typing import Optional, cast + +import numpy as np +import pytest +import rerun.experimental as rr_exp +from rerun.experimental import cmp as rr_cmp +from rerun.experimental import dt as rr_dt + +# TODO(cmc): roundtrips (serialize in python, deserialize in rust) + +U64_MAX_MINUS_1 = 2**64 - 2 +U64_MAX = 2**64 - 1 + + +def test_points3d() -> None: + points_arrays: list[rr_dt.Point3DArrayLike] = [ + [], + np.array([]), + # Point3DArrayLike: Sequence[Point3DLike]: Point3D + [ + rr_dt.Point3D(1, 2, 3), + rr_dt.Point3D(4, 5, 6), + ], + # Point3DArrayLike: Sequence[Point3DLike]: npt.NDArray[np.float32] + [ + np.array([1, 2, 3], dtype=np.float32), + np.array([4, 5, 6], dtype=np.float32), + ], + # Point3DArrayLike: Sequence[Point3DLike]: Tuple[float, float] + [(1, 2, 3), (4, 5, 6)], + # Point3DArrayLike: Sequence[Point3DLike]: Sequence[float] + [1, 2, 3, 4, 5, 6], + # Point3DArrayLike: npt.NDArray[np.float32] + np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), + # Point3DArrayLike: npt.NDArray[np.float32] + np.array([1, 2, 3, 4, 5, 6], dtype=np.float32), + ] + + radii_arrays: list[rr_cmp.RadiusArrayLike | None] = [ + None, + [], + np.array([]), + # RadiusArrayLike: Sequence[RadiusLike]: float + [42, 43], + # RadiusArrayLike: Sequence[RadiusLike]: Radius + [ + rr_cmp.Radius(42), + rr_cmp.Radius(43), + ], + # RadiusArrayLike: npt.NDArray[np.float32] + np.array([42, 43], dtype=np.float32), + ] + + colors_arrays: list[rr_cmp.ColorArrayLike | None] = [ + None, + [], + np.array([]), + # ColorArrayLike: Sequence[ColorLike]: int + [ + 0xAA0000CC, + 0x00BB00DD, + ], + # ColorArrayLike: Sequence[ColorLike]: Color + [ + rr_cmp.Color(0xAA0000CC), + rr_cmp.Color(0x00BB00DD), + ], + # ColorArrayLike: Sequence[ColorLike]: npt.NDArray[np.uint8] + np.array( + [ + [0xAA, 0x00, 0x00, 0xCC], + [0x00, 0xBB, 0x00, 0xDD], + ], + dtype=np.uint8, + ), + # ColorArrayLike: Sequence[ColorLike]: npt.NDArray[np.uint32] + np.array( + [ + [0xAA0000CC], + [0x00BB00DD], + ], + dtype=np.uint32, + ), + # ColorArrayLike: Sequence[ColorLike]: npt.NDArray[np.float32] + np.array( + [ + [0xAA / 0xFF, 0.0, 0.0, 0xCC / 0xFF], + [0.0, 0xBB / 0xFF, 0.0, 0xDD / 0xFF], + ], + dtype=np.float32, + ), + # ColorArrayLike: Sequence[ColorLike]: npt.NDArray[np.float64] + np.array( + [ + [0xAA / 0xFF, 0.0, 0.0, 0xCC / 0xFF], + [0.0, 0xBB / 0xFF, 0.0, 0xDD / 0xFF], + ], + dtype=np.float64, + ), + # ColorArrayLike: npt.NDArray[np.uint8] + np.array( + [ + 0xAA, + 0x00, + 0x00, + 0xCC, + 0x00, + 0xBB, + 0x00, + 0xDD, + ], + dtype=np.uint8, + ), + # ColorArrayLike: npt.NDArray[np.uint32] + np.array( + [ + 0xAA0000CC, + 0x00BB00DD, + ], + dtype=np.uint32, + ), + # ColorArrayLike: npt.NDArray[np.float32] + np.array( + [ + 0xAA / 0xFF, + 0.0, + 0.0, + 0xCC / 0xFF, + 0.0, + 0xBB / 0xFF, + 0.0, + 0xDD / 0xFF, + ], + dtype=np.float32, + ), + # ColorArrayLike: npt.NDArray[np.float64] + np.array( + [ + 0xAA / 0xFF, + 0.0, + 0.0, + 0xCC / 0xFF, + 0.0, + 0xBB / 0xFF, + 0.0, + 0xDD / 0xFF, + ], + dtype=np.float64, + ), + ] + + labels_arrays: list[rr_cmp.LabelArrayLike | None] = [ + None, + [], + # LabelArrayLike: Sequence[LabelLike]: str + ["hello", "friend"], + # LabelArrayLike: Sequence[LabelLike]: Label + [ + rr_cmp.Label("hello"), + rr_cmp.Label("friend"), + ], + ] + + draw_orders: list[rr_cmp.DrawOrderLike | None] = [ + None, + # DrawOrderLike: float + 300, + # DrawOrderLike: DrawOrder + rr_cmp.DrawOrder(300), + ] + + class_id_arrays = [ + [], + np.array([]), + # ClassIdArrayLike: Sequence[ClassIdLike]: int + [126, 127], + # ClassIdArrayLike: Sequence[ClassIdLike]: ClassId + [rr_cmp.ClassId(126), rr_cmp.ClassId(127)], + # ClassIdArrayLike: np.NDArray[np.uint8] + np.array([126, 127], dtype=np.uint8), + # ClassIdArrayLike: np.NDArray[np.uint16] + np.array([126, 127], dtype=np.uint16), + # ClassIdArrayLike: np.NDArray[np.uint32] + np.array([126, 127], dtype=np.uint32), + # ClassIdArrayLike: np.NDArray[np.uint64] + np.array([126, 127], dtype=np.uint64), + ] + + keypoint_id_arrays = [ + [], + np.array([]), + # KeypointIdArrayLike: Sequence[KeypointIdLike]: int + [2, 3], + # KeypointIdArrayLike: Sequence[KeypointIdLike]: KeypointId + [rr_cmp.KeypointId(2), rr_cmp.KeypointId(3)], + # KeypointIdArrayLike: np.NDArray[np.uint8] + np.array([2, 3], dtype=np.uint8), + # KeypointIdArrayLike: np.NDArray[np.uint16] + np.array([2, 3], dtype=np.uint16), + # KeypointIdArrayLike: np.NDArray[np.uint32] + np.array([2, 3], dtype=np.uint32), + # KeypointIdArrayLike: np.NDArray[np.uint64] + np.array([2, 3], dtype=np.uint64), + ] + + instance_key_arrays = [ + [], + np.array([]), + # InstanceKeyArrayLike: Sequence[InstanceKeyLike]: int + [U64_MAX_MINUS_1, U64_MAX], + # InstanceKeyArrayLike: Sequence[InstanceKeyLike]: InstanceKey + [rr_cmp.InstanceKey(U64_MAX_MINUS_1), rr_cmp.InstanceKey(U64_MAX)], + # InstanceKeyArrayLike: np.NDArray[np.uint64] + np.array([U64_MAX_MINUS_1, U64_MAX], dtype=np.uint64), + ] + + all_arrays = itertools.zip_longest( + points_arrays, + radii_arrays, + colors_arrays, + labels_arrays, + draw_orders, + class_id_arrays, + keypoint_id_arrays, + instance_key_arrays, + ) + + for points, radii, colors, labels, draw_order, class_ids, keypoint_ids, instance_keys in all_arrays: + points = points if points is not None else points_arrays[-1] + + # make Pyright happy as it's apparently not able to track typing info trough zip_longest + points = cast(Optional[rr_dt.Point3DArrayLike], points) + radii = cast(Optional[rr_cmp.RadiusArrayLike], radii) + colors = cast(Optional[rr_cmp.ColorArrayLike], colors) + labels = cast(Optional[rr_cmp.LabelArrayLike], labels) + draw_order = cast(Optional[rr_cmp.DrawOrderArrayLike], draw_order) + class_ids = cast(Optional[rr_cmp.ClassIdArrayLike], class_ids) + keypoint_ids = cast(Optional[rr_cmp.KeypointIdArrayLike], keypoint_ids) + instance_keys = cast(Optional[rr_cmp.InstanceKeyArrayLike], instance_keys) + + print( + f"rr_exp.Points3D(\n" + f" {points}\n" + f" radii={radii}\n" + f" colors={colors}\n" + f" labels={labels}\n" + f" draw_order={draw_order}\n" + f" class_ids={class_ids}\n" + f" keypoint_ids={keypoint_ids}\n" + f" instance_keys={instance_keys}\n" + f")" + ) + arch = rr_exp.Points3D( + points, + radii=radii, + colors=colors, + labels=labels, + draw_order=draw_order, + class_ids=class_ids, + keypoint_ids=keypoint_ids, + instance_keys=instance_keys, + ) + print(f"{arch}\n") + + assert arch.points == rr_cmp.Point3DArray.from_similar( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] if non_empty(points) else [] + ) + assert arch.radii == rr_cmp.RadiusArray.from_similar([42, 43] if non_empty(radii) else []) + assert arch.colors == rr_cmp.ColorArray.from_similar([0xAA0000CC, 0x00BB00DD] if non_empty(colors) else []) + assert arch.labels == rr_cmp.LabelArray.from_similar(["hello", "friend"] if non_empty(labels) else []) + assert arch.draw_order == rr_cmp.DrawOrderArray.from_similar([300] if draw_order is not None else []) + assert arch.class_ids == rr_cmp.ClassIdArray.from_similar([126, 127] if non_empty(class_ids) else []) + assert arch.keypoint_ids == rr_cmp.KeypointIdArray.from_similar([2, 3] if non_empty(keypoint_ids) else []) + assert arch.instance_keys == rr_cmp.InstanceKeyArray.from_similar( + [U64_MAX_MINUS_1, U64_MAX] if non_empty(instance_keys) else [] + ) + + +def non_empty(v: object) -> bool: + return v is not None and len(v) > 0 # type: ignore[arg-type] + + +@pytest.mark.parametrize( + "data", + [ + [0, 128, 0, 255], + [0, 128, 0], + np.array((0, 128, 0, 255)), + [0.0, 0.5, 0.0, 1.0], + np.array((0.0, 0.5, 0.0, 1.0)), + ], +) +def test_point3d_single_color(data: rr_cmp.ColorArrayLike) -> None: + pts = rr_exp.Points3D(points=np.zeros((5, 3)), colors=data) + + assert pts.colors == rr_cmp.ColorArray.from_similar(rr_cmp.Color([0, 128, 0, 255])) + + +@pytest.mark.parametrize( + "data", + [ + [[0, 128, 0, 255], [128, 0, 0, 255]], + [[0, 128, 0], [128, 0, 0]], + np.array([[0, 128, 0, 255], [128, 0, 0, 255]]), + np.array([0, 128, 0, 255, 128, 0, 0, 255], dtype=np.uint8), + np.array([8388863, 2147483903], dtype=np.uint32), + np.array([[0, 128, 0], [128, 0, 0]]), + [[0.0, 0.5, 0.0, 1.0], [0.5, 0.0, 0.0, 1.0]], + [[0.0, 0.5, 0.0], [0.5, 0.0, 0.0]], + np.array([[0.0, 0.5, 0.0, 1.0], [0.5, 0.0, 0.0, 1.0]]), + np.array([[0.0, 0.5, 0.0], [0.5, 0.0, 0.0]]), + np.array([0.0, 0.5, 0.0, 1.0, 0.5, 0.0, 0.0, 1.0]), + # Note: Sequence[int] is interpreted as a single color when they are 3 or 4 long. For other lengths, they + # are interpreted as list of packed uint32 colors. Note that this means one cannot pass an len=N*4 flat list of + # color components. + [8388863, 2147483903], + ], +) +def test_point3d_multiple_colors(data: rr_cmp.ColorArrayLike) -> None: + pts = rr_exp.Points3D(points=np.zeros((5, 3)), colors=data) + + assert pts.colors == rr_cmp.ColorArray.from_similar( + [ + rr_cmp.Color([0, 128, 0, 255]), + rr_cmp.Color([128, 0, 0, 255]), + ] + ) + + +if __name__ == "__main__": + test_points3d() diff --git a/tests/python/roundtrips/points2d/main.py b/tests/python/roundtrips/points2d/main.py index e718efbc120d..7786a1dc5817 100755 --- a/tests/python/roundtrips/points2d/main.py +++ b/tests/python/roundtrips/points2d/main.py @@ -1,7 +1,7 @@ -"""Logs a `Points2D` archetype for roundtrip checks.""" - #!/usr/bin/env python3 +"""Logs a `Points2D` archetype for roundtrip checks.""" + from __future__ import annotations import argparse diff --git a/tests/python/roundtrips/points3d/main.py b/tests/python/roundtrips/points3d/main.py new file mode 100755 index 000000000000..104096f61f2d --- /dev/null +++ b/tests/python/roundtrips/points3d/main.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +"""Logs a `Points3D` archetype for roundtrip checks.""" + +from __future__ import annotations + +import argparse + +import numpy as np +import rerun as rr +import rerun.experimental as rr_exp + + +def main() -> None: + points = np.array([1, 2, 3, 4, 5, 6], dtype=np.float32) + radii = np.array([0.42, 0.43], dtype=np.float32) + colors = np.array( + [ + 0xAA0000CC, + 0x00BB00DD, + ], + dtype=np.uint32, + ) + labels = ["hello", "friend"] + draw_order = 300 + class_ids = np.array([126, 127], dtype=np.uint64) + keypoint_ids = np.array([2, 3], dtype=np.uint64) + instance_keys = np.array([66, 666], dtype=np.uint64) + + points3d = rr_exp.Points3D( + points, + radii=radii, + colors=colors, + labels=labels, + draw_order=draw_order, + class_ids=class_ids, + keypoint_ids=keypoint_ids, + instance_keys=instance_keys, + ) + + parser = argparse.ArgumentParser(description="Logs rich data using the Rerun SDK.") + rr.script_add_args(parser) + args = parser.parse_args() + + rr.script_setup(args, "roundtrip_points3d") + + rr_exp.log_any("points3d", points3d) + + rr.script_teardown(args) + + +if __name__ == "__main__": + main() diff --git a/tests/python/roundtrips/transform3d/main.py b/tests/python/roundtrips/transform3d/main.py index f82c8adf3408..9494850c5e7a 100644 --- a/tests/python/roundtrips/transform3d/main.py +++ b/tests/python/roundtrips/transform3d/main.py @@ -1,7 +1,7 @@ -"""Logs a `Transform3D` archetype for roundtrip checks.""" - #!/usr/bin/env python3 +"""Logs a `Transform3D` archetype for roundtrip checks.""" + from __future__ import annotations import argparse diff --git a/tests/rust/roundtrips/points3d/Cargo.toml b/tests/rust/roundtrips/points3d/Cargo.toml new file mode 100644 index 000000000000..f90573d6faa6 --- /dev/null +++ b/tests/rust/roundtrips/points3d/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "roundtrip_points3d" +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true +version.workspace = true + +[dependencies] +rerun = { path = "../../../../crates/rerun", features = ["native_viewer"] } + +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } diff --git a/tests/rust/roundtrips/points3d/src/main.rs b/tests/rust/roundtrips/points3d/src/main.rs new file mode 100644 index 000000000000..2d46cf01706a --- /dev/null +++ b/tests/rust/roundtrips/points3d/src/main.rs @@ -0,0 +1,41 @@ +//! Logs a `Points3D` archetype for roundtrip checks. + +use rerun::{experimental::archetypes::Points3D, external::re_log, MsgSender, RecordingStream}; + +#[derive(Debug, clap::Parser)] +#[clap(author, version, about)] +struct Args { + #[command(flatten)] + rerun: rerun::clap::RerunArgs, +} + +fn run(rec_stream: &RecordingStream, _args: &Args) -> anyhow::Result<()> { + MsgSender::from_archetype( + "points3d", + &Points3D::new([(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]) + .with_radii([0.42, 0.43]) + .with_colors([0xAA0000CC, 0x00BB00DD]) + .with_labels(["hello", "friend"]) + .with_draw_order(300.0) + .with_class_ids([126, 127]) + .with_keypoint_ids([2, 3]) + .with_instance_keys([66, 666]), + )? + .send(rec_stream)?; + + Ok(()) +} + +fn main() -> anyhow::Result<()> { + re_log::setup_native_logging(); + + use clap::Parser as _; + let args = Args::parse(); + + let default_enabled = true; + args.rerun + .clone() + .run("roundtrip_points3d", default_enabled, move |rec_stream| { + run(&rec_stream, &args).unwrap(); + }) +}