Skip to content

Commit 4abf140

Browse files
committed
Implement new lens kinds OneToOne, Static, and OneToMany
Rethink target entity paths Clean up terminology: `Timeline` vs `Time`
1 parent f80ce8b commit 4abf140

File tree

8 files changed

+1033
-325
lines changed

8 files changed

+1033
-325
lines changed

crates/top/re_sdk/src/lenses/ast.rs

Lines changed: 615 additions & 291 deletions
Large diffs are not rendered by default.
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
//! Builder API for constructing lenses.
2+
3+
use re_chunk::{ComponentIdentifier, EntityPath, TimelineName};
4+
use re_log_types::{EntityPathFilter, TimeType};
5+
use re_types::ComponentDescriptor;
6+
7+
use super::Op;
8+
use super::ast;
9+
10+
/// Builder for lenses with support for multiple output modes.
11+
pub struct LensBuilder {
12+
input: ast::InputColumn,
13+
outputs: Vec<ast::LensKind>,
14+
}
15+
16+
impl LensBuilder {
17+
pub(crate) fn new(
18+
entity_path_filter: EntityPathFilter,
19+
component: impl Into<ComponentIdentifier>,
20+
) -> Self {
21+
Self {
22+
input: ast::InputColumn {
23+
entity_path_filter,
24+
component: component.into(),
25+
},
26+
outputs: vec![],
27+
}
28+
}
29+
30+
/// Adds a temporal output with 1:1 row mapping.
31+
///
32+
/// Each input row produces exactly one output row. Outputs inherit time columns from
33+
/// the input, plus any additional time columns specified via `.time()`.
34+
///
35+
/// The output will use the same entity path as the input.
36+
pub fn output_columns(mut self, builder: impl FnOnce(ColumnsBuilder) -> ColumnsBuilder) -> Self {
37+
let output_builder = ColumnsBuilder::new(ast::TargetEntity::SameAsInput);
38+
let output = builder(output_builder).build();
39+
self.outputs.push(output);
40+
self
41+
}
42+
43+
/// Adds a temporal output with 1:1 row mapping at a specific entity path.
44+
///
45+
/// Each input row produces exactly one output row. Outputs inherit time columns from
46+
/// the input, plus any additional time columns specified via `.time()`.
47+
pub fn output_columns_at(
48+
mut self,
49+
entity_path: impl Into<EntityPath>,
50+
builder: impl FnOnce(ColumnsBuilder) -> ColumnsBuilder,
51+
) -> Self {
52+
let output_builder =
53+
ColumnsBuilder::new(ast::TargetEntity::Explicit(entity_path.into()));
54+
let output = builder(output_builder).build();
55+
self.outputs.push(output);
56+
self
57+
}
58+
59+
/// Adds a static output (timeless data).
60+
///
61+
/// Creates data that does not change over time and has no associated time columns.
62+
///
63+
/// The output will use the same entity path as the input.
64+
pub fn output_static_columns(
65+
mut self,
66+
builder: impl FnOnce(StaticColumnsBuilder) -> StaticColumnsBuilder,
67+
) -> Self {
68+
let output_builder = StaticColumnsBuilder::new(ast::TargetEntity::SameAsInput);
69+
let output = builder(output_builder).build();
70+
self.outputs.push(output);
71+
self
72+
}
73+
74+
/// Adds a static output (timeless data) at a specific entity path.
75+
///
76+
/// Creates data that does not change over time and has no associated time columns.
77+
pub fn output_static_columns_at(
78+
mut self,
79+
entity_path: impl Into<EntityPath>,
80+
builder: impl FnOnce(StaticColumnsBuilder) -> StaticColumnsBuilder,
81+
) -> Self {
82+
let output_builder =
83+
StaticColumnsBuilder::new(ast::TargetEntity::Explicit(entity_path.into()));
84+
let output = builder(output_builder).build();
85+
self.outputs.push(output);
86+
self
87+
}
88+
89+
/// Adds a temporal output with 1:N row mapping (scatter).
90+
///
91+
/// Each input row produces multiple output rows at the same timepoint. The timepoint
92+
/// is replicated/scattered across the output rows. Useful for flattening lists or
93+
/// exploding batches.
94+
///
95+
/// The output will use the same entity path as the input.
96+
pub fn output_scatter_columns(
97+
mut self,
98+
builder: impl FnOnce(ScatterColumnsBuilder) -> ScatterColumnsBuilder,
99+
) -> Self {
100+
let output_builder = ScatterColumnsBuilder::new(ast::TargetEntity::SameAsInput);
101+
let output = builder(output_builder).build();
102+
self.outputs.push(output);
103+
self
104+
}
105+
106+
/// Adds a temporal output with 1:N row mapping (scatter) at a specific entity path.
107+
///
108+
/// Each input row produces multiple output rows at the same timepoint. The timepoint
109+
/// is replicated/scattered across the output rows. Useful for flattening lists or
110+
/// exploding batches.
111+
pub fn output_scatter_columns_at(
112+
mut self,
113+
entity_path: impl Into<EntityPath>,
114+
builder: impl FnOnce(ScatterColumnsBuilder) -> ScatterColumnsBuilder,
115+
) -> Self {
116+
let output_builder =
117+
ScatterColumnsBuilder::new(ast::TargetEntity::Explicit(entity_path.into()));
118+
let output = builder(output_builder).build();
119+
self.outputs.push(output);
120+
self
121+
}
122+
123+
/// Finalizes this builder and returns the corresponding lens.
124+
pub fn build(self) -> ast::Lens {
125+
ast::Lens {
126+
input: self.input,
127+
outputs: self.outputs,
128+
}
129+
}
130+
}
131+
132+
// ==================== Output Builders ====================
133+
134+
/// Builder for temporal outputs with 1:1 row mapping.
135+
///
136+
/// Each input row produces exactly one output row. Outputs inherit time columns
137+
/// from the input, plus any additional time columns specified.
138+
pub struct ColumnsBuilder {
139+
target_entity: ast::TargetEntity,
140+
components: Vec<ast::ComponentOutput>,
141+
time_outputs: Vec<ast::TimeOutput>,
142+
}
143+
144+
impl ColumnsBuilder {
145+
fn new(target_entity: ast::TargetEntity) -> Self {
146+
Self {
147+
target_entity,
148+
components: vec![],
149+
time_outputs: vec![],
150+
}
151+
}
152+
153+
/// Adds a component output column.
154+
///
155+
/// # Arguments
156+
/// * `component_descr` - The descriptor for the output component
157+
/// * `ops` - Sequence of operations to apply to transform the input column
158+
pub fn component(
159+
mut self,
160+
component_descr: ComponentDescriptor,
161+
ops: impl IntoIterator<Item = Op>,
162+
) -> Self {
163+
self.components.push(ast::ComponentOutput {
164+
component_descr,
165+
ops: ops.into_iter().collect(),
166+
});
167+
self
168+
}
169+
170+
/// Adds a time extraction.
171+
///
172+
/// Extracts data from the input column to create a new time column for the output rows.
173+
///
174+
/// # Arguments
175+
/// * `timeline_name` - Name of the timeline to create
176+
/// * `timeline_type` - Type of timeline (Sequence or Time)
177+
/// * `ops` - Sequence of operations to extract time values (must produce [`arrow::array::Int64Array`])
178+
pub fn time(
179+
mut self,
180+
timeline_name: impl Into<TimelineName>,
181+
timeline_type: TimeType,
182+
ops: impl IntoIterator<Item = Op>,
183+
) -> Self {
184+
self.time_outputs.push(ast::TimeOutput {
185+
timeline_name: timeline_name.into(),
186+
timeline_type,
187+
ops: ops.into_iter().collect(),
188+
});
189+
self
190+
}
191+
192+
fn build(self) -> ast::LensKind {
193+
if self.components.is_empty() {
194+
re_log::warn_once!(
195+
"Lens output created without any components. At least one component is required."
196+
);
197+
}
198+
199+
ast::LensKind::OneToOne {
200+
target_entity: self.target_entity,
201+
components: self.components,
202+
timelines: self.time_outputs,
203+
}
204+
}
205+
}
206+
207+
/// Builder for static outputs (timeless data).
208+
///
209+
/// Creates data that does not change over time. Static outputs have no associated time columns.
210+
pub struct StaticColumnsBuilder {
211+
target_entity: ast::TargetEntity,
212+
components: Vec<ast::ComponentOutput>,
213+
}
214+
215+
impl StaticColumnsBuilder {
216+
fn new(target_entity: ast::TargetEntity) -> Self {
217+
Self {
218+
target_entity,
219+
components: vec![],
220+
}
221+
}
222+
223+
/// Adds a component output column.
224+
///
225+
/// # Arguments
226+
/// * `component_descr` - The descriptor for the output component
227+
/// * `ops` - Sequence of operations to apply to transform the input column
228+
pub fn component(
229+
mut self,
230+
component_descr: ComponentDescriptor,
231+
ops: impl IntoIterator<Item = Op>,
232+
) -> Self {
233+
self.components.push(ast::ComponentOutput {
234+
component_descr,
235+
ops: ops.into_iter().collect(),
236+
});
237+
self
238+
}
239+
240+
fn build(self) -> ast::LensKind {
241+
if self.components.is_empty() {
242+
re_log::warn_once!(
243+
"Lens output created without any components. At least one component is required."
244+
);
245+
}
246+
247+
ast::LensKind::Static {
248+
target_entity: self.target_entity,
249+
components: self.components,
250+
}
251+
}
252+
}
253+
254+
/// Builder for temporal outputs with 1:N row mapping (scatter).
255+
///
256+
/// Each input row produces multiple output rows at the same timepoint. The timepoint
257+
/// is replicated/scattered across all output rows. This is useful for flattening lists
258+
/// or exploding batches while maintaining temporal alignment.
259+
pub struct ScatterColumnsBuilder {
260+
target_entity: ast::TargetEntity,
261+
components: Vec<ast::ComponentOutput>,
262+
time_outputs: Vec<ast::TimeOutput>,
263+
}
264+
265+
impl ScatterColumnsBuilder {
266+
fn new(target_entity: ast::TargetEntity) -> Self {
267+
Self {
268+
target_entity,
269+
components: vec![],
270+
time_outputs: vec![],
271+
}
272+
}
273+
274+
/// Adds a component output column.
275+
///
276+
/// # Arguments
277+
/// * `component_descr` - The descriptor for the output component
278+
/// * `ops` - Sequence of operations to apply to transform the input column
279+
pub fn component(
280+
mut self,
281+
component_descr: ComponentDescriptor,
282+
ops: impl IntoIterator<Item = Op>,
283+
) -> Self {
284+
self.components.push(ast::ComponentOutput {
285+
component_descr,
286+
ops: ops.into_iter().collect(),
287+
});
288+
self
289+
}
290+
291+
/// Adds a time extraction.
292+
///
293+
/// Extracts data from the input column to create a new time column for the output rows.
294+
///
295+
/// # Arguments
296+
/// * `timeline_name` - Name of the timeline to create
297+
/// * `timeline_type` - Type of timeline (Sequence or Time)
298+
/// * `ops` - Sequence of operations to extract time values (must produce [`arrow::array::Int64Array`])
299+
pub fn time(
300+
mut self,
301+
timeline_name: impl Into<TimelineName>,
302+
timeline_type: TimeType,
303+
ops: impl IntoIterator<Item = Op>,
304+
) -> Self {
305+
self.time_outputs.push(ast::TimeOutput {
306+
timeline_name: timeline_name.into(),
307+
timeline_type,
308+
ops: ops.into_iter().collect(),
309+
});
310+
self
311+
}
312+
313+
fn build(self) -> ast::LensKind {
314+
if self.components.is_empty() {
315+
re_log::warn_once!(
316+
"Lens scatter output created without any components. At least one component is required."
317+
);
318+
}
319+
320+
ast::LensKind::ToMany {
321+
target_entity: self.target_entity,
322+
components: self.components,
323+
timelines: self.time_outputs,
324+
}
325+
}
326+
}

crates/top/re_sdk/src/lenses/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
//! See [`lenses::Lens`] for more details and assumptions. One way to make use of lenses is
66
//! by using the [`lenses::LensesSink`].
77
8-
pub(crate) mod ast;
8+
mod ast;
9+
mod builder;
910
mod error;
1011
mod op;
1112
mod sink;
1213

1314
pub use self::{
1415
// We should be careful not to expose to much implementation details here.
15-
ast::{Lens, LensBuilder, Op},
16+
ast::{Lens, Op},
17+
builder::{ColumnsBuilder, LensBuilder, ScatterColumnsBuilder, StaticColumnsBuilder},
1618
error::Error,
1719
sink::LensesSink,
1820
};

crates/top/re_sdk/src/lenses/op.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77
use std::sync::Arc;
88

99
use re_arrow_combinators::{Transform as _, map::MapList, reshape::GetField};
10-
use re_chunk::{
11-
ArrowArray as _,
12-
external::arrow::{
13-
array::ListArray,
14-
compute,
15-
datatypes::{DataType, Field},
16-
},
10+
use re_chunk::external::arrow::{
11+
array::{Array as _, ListArray},
12+
compute,
13+
datatypes::{DataType, Field},
1714
};
1815

1916
use super::Error;
@@ -50,3 +47,4 @@ impl Cast {
5047
))
5148
}
5249
}
50+

0 commit comments

Comments
 (0)