Skip to main content

cumulus3d_cli/
events.rs

1/*
2 * events.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * SPDX-License-Identifier: Apache-2.0
26 */
27
28//! Pipeline events. Each time a new frame bundle is processed, the result decomposed by stage is emitted.
29//! This file is the shared contract of three modules (session, pipeline, sinks). Adding fields is allowed; changing or removing existing
30//! fields is forbidden.
31
32use cumulus3d_core::io::ply::PointCloud;
33use cumulus3d_core::{ImageId, Reconstruction, Rigid3, Sim3};
34use std::sync::Arc;
35use std::time::{Duration, SystemTime};
36
37/// Header attached to every event.
38#[derive(Clone, Debug)]
39pub struct Meta {
40    /// Session version (a monotonically increasing number of the state that emitted the event).
41    pub version: u64,
42    /// Time of occurrence.
43    pub at: SystemTime,
44}
45
46/// Zone index and position range [lo, hi).
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct ZoneRange {
49    /// Zone index.
50    pub zone: usize,
51    /// First position (inclusive).
52    pub lo: usize,
53    /// End position (exclusive).
54    pub hi: usize,
55}
56
57/// A decoded input frame (one camera image). It is delivered first as [`Event::FrameDecoded`], before processing (features, matching,
58/// registration).
59#[derive(Clone, Debug)]
60pub struct Frame {
61    /// Position index.
62    pub position: usize,
63    /// Camera name (e.g. `camF`).
64    pub camera: String,
65    /// Image name (path relative to `image_root`).
66    pub name: String,
67    /// Image file path.
68    pub path: std::path::PathBuf,
69    /// Width in pixels.
70    pub width: u32,
71    /// Height in pixels.
72    pub height: u32,
73    /// RGB8 pixels (row-major, length `width * height * 3`). Shared between hooks without copying.
74    pub rgb: Arc<[u8]>,
75    /// GPS record of this image (if any).
76    pub gps: Option<cumulus3d_core::io::GpsRecord>,
77}
78
79/// Pipeline events (per-stage results). Every variant carries a [`Meta`].
80#[derive(Clone, Debug)]
81#[non_exhaustive]
82pub enum Event {
83    /// The frame bundle of one position has arrived.
84    FrameIngested {
85        /// Common header.
86        meta: Meta,
87        /// Position index.
88        position: usize,
89        /// Image names of this position.
90        images: Vec<String>,
91    },
92    /// Decoding of one input frame is complete. Comes first, before the position is processed (only when `SessionConfig::decode_frames`
93    /// is on).
94    FrameDecoded {
95        /// Common header.
96        meta: Meta,
97        /// Decoded frame.
98        frame: Arc<Frame>,
99    },
100    /// Session start (once, at the first input). `positions` = total number of known positions. (added by session)
101    Started {
102        /// Common header.
103        meta: Meta,
104        /// Total number of known positions (None if unknown).
105        positions: Option<usize>,
106    },
107    /// One run.log line. If `stamped`, it is written with a `[HH:MM:SS] ` prefix (meta.at local time). (added by session)
108    Log {
109        /// Common header.
110        meta: Meta,
111        /// Line to record.
112        line: String,
113        /// Whether to add the `[HH:MM:SS] ` prefix.
114        stamped: bool,
115    },
116    /// Feature extraction of one image is complete.
117    FeaturesExtracted {
118        /// Common header.
119        meta: Meta,
120        /// Image name.
121        image: String,
122        /// Number of extracted features.
123        count: usize,
124        /// Position index.
125        position: usize,
126        /// Image id.
127        image_id: ImageId,
128    },
129    /// Result of matching new pairs.
130    /// `pairs` = pairs matched at this position, `verified` = pairs with valid geometry, `matched` = pairs for which descriptor matching
131    /// was performed.
132    PairsMatched {
133        /// Common header.
134        meta: Meta,
135        /// Pairs matched at this position.
136        pairs: usize,
137        /// Pairs with valid geometry.
138        verified: usize,
139        /// Position index.
140        position: usize,
141        /// Pairs for which descriptor matching was performed.
142        matched: usize,
143    },
144    /// The first model has been built.
145    ModelInitialized {
146        /// Common header.
147        meta: Meta,
148        /// Number of registered images.
149        registered: usize,
150        /// Position index.
151        position: usize,
152        /// Copy of the relevant model.
153        model: Arc<Reconstruction>,
154    },
155    /// An image has been registered to the model.
156    FrameRegistered {
157        /// Common header.
158        meta: Meta,
159        /// Position index.
160        position: usize,
161        /// Image name.
162        image: String,
163        /// Image id.
164        image_id: ImageId,
165        /// Registered pose (world -> camera).
166        cam_from_world: Rigid3,
167    },
168    /// Registration and triangulation of one position are finished.
169    PositionDone {
170        /// Common header.
171        meta: Meta,
172        /// Position index.
173        position: usize,
174        /// Number of registered images.
175        registered: usize,
176        /// Number of images expected up to this position.
177        expected: usize,
178    },
179    /// The last position of a zone has arrived.
180    /// `model` = copy of the chain model at that point (before alignment).
181    ZoneArrived {
182        /// Common header.
183        meta: Meta,
184        /// Zone range.
185        range: ZoneRange,
186        /// Copy of the relevant model.
187        model: Arc<Reconstruction>,
188    },
189    /// Zone preview point cloud (no BA).
190    /// `model` = preview model (zone 0 is the GPS-aligned one), `dense` = false means densification was not run or failed and `cloud` is
191    /// an empty placeholder.
192    /// `frame` = point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
193    ZonePreview {
194        /// Common header.
195        meta: Meta,
196        /// Zone range.
197        range: ZoneRange,
198        /// Point cloud (an empty placeholder if there is no densification).
199        cloud: Arc<PointCloud>,
200        /// Copy of the relevant model.
201        model: Arc<Reconstruction>,
202        /// Whether densification was actually performed.
203        dense: bool,
204        /// Point-cloud coordinate frame ("gps" | "refined_K" | "unaligned").
205        frame: String,
206    },
207    /// Refinement job start (background). (added by session)
208    RefineStarted {
209        /// Common header.
210        meta: Meta,
211        /// Zone index.
212        zone: usize,
213    },
214    /// Model right after BA of the refinement job (before GPS alignment). (added by session)
215    ZoneAdjusted {
216        /// Common header.
217        meta: Meta,
218        /// Zone index.
219        zone: usize,
220        /// Copy of the relevant model.
221        model: Arc<Reconstruction>,
222    },
223    /// Zone refined pose finalized (BA + GPS alignment).
224    /// `model` = refined model after BA + GPS alignment.
225    ZoneRefinedPose {
226        /// Common header.
227        meta: Meta,
228        /// Zone range.
229        range: ZoneRange,
230        /// Number of registered images.
231        registered: usize,
232        /// Mean reprojection error (pixels).
233        mean_reproj_px: f64,
234        /// Copy of the relevant model.
235        model: Arc<Reconstruction>,
236    },
237    /// Zone refined point cloud. It replaces the preview of the same zone.
238    /// `dense` = false means `cloud` is an empty placeholder.
239    ZoneRefined {
240        /// Common header.
241        meta: Meta,
242        /// Zone range.
243        range: ZoneRange,
244        /// Point cloud (an empty placeholder if there is no densification).
245        cloud: Arc<PointCloud>,
246        /// Copy of the relevant model.
247        model: Arc<Reconstruction>,
248        /// Whether densification was actually performed.
249        dense: bool,
250    },
251    /// The refined model was adopted as the reference for subsequent registration.
252    BaseAdopted {
253        /// Common header.
254        meta: Meta,
255        /// Zone index.
256        zone: usize,
257    },
258    /// Coordinate-frame transform of an already sent zone (the point cloud is not sent again): new_from_old.
259    Reanchored {
260        /// Common header.
261        meta: Meta,
262        /// Zone index.
263        zone: usize,
264        /// new_from_old transform.
265        transform: Sim3,
266    },
267    /// Snapshot for display (event sequence number and composite point cloud).
268    Snapshot {
269        /// Common header.
270        meta: Meta,
271        /// Event sequence number.
272        index: usize,
273        /// Point cloud (an empty placeholder if there is no densification).
274        cloud: Arc<PointCloud>,
275    },
276    /// All positions processed.
277    AllPositionsDone {
278        /// Common header.
279        meta: Meta,
280    },
281    /// All refinement jobs complete.
282    /// `chain` = final chain model.
283    AllRefinedDone {
284        /// Common header.
285        meta: Meta,
286        /// Final chain model.
287        chain: Option<Arc<Reconstruction>>,
288    },
289    /// Session end summary (the last of all events). `stage_times` = (stage, accumulated time, count) in stage-name order,
290    /// `pipeline_secs` = start to all refinements complete, `chain_stats` = statistics line of the final chain model. (added by session)
291    Finished {
292        /// Common header.
293        meta: Meta,
294        /// Start to all refinements complete (seconds).
295        pipeline_secs: f64,
296        /// (stage, accumulated time, count) in stage-name order.
297        stage_times: Vec<(String, Duration, usize)>,
298        /// Statistics line of the final chain model.
299        chain_stats: Vec<String>,
300    },
301    /// `Command::ResetFrom` was handled. (added by session)
302    Reset {
303        /// Common header.
304        meta: Meta,
305        /// Position index.
306        position: usize,
307    },
308    /// `Command::InvalidateZone` was handled. (added by session)
309    ZoneInvalidated {
310        /// Common header.
311        meta: Meta,
312        /// Zone index.
313        zone: usize,
314    },
315    /// Incremental display update computed by the library (opt-in, see `NodeBuilder::display_deltas` and
316    /// [`crate::live::DisplayFeed`]): the point-keyed difference between the previous and the new composed display cloud after
317    /// a zone result. Emitted right after the event that caused it.
318    DisplayDelta {
319        /// Common header (of the causing event).
320        meta: Meta,
321        /// Adds, moves and removes with dense slot ids (apply: removes, adds, moves).
322        delta: Arc<crate::live::DisplayDelta>,
323        /// Zone whose result caused the delta.
324        zone: usize,
325        /// Why it was emitted.
326        kind: crate::live::DeltaKind,
327        /// Seconds since the session start.
328        t: f64,
329        /// Coordinate frame of the display points.
330        frame: crate::live::DisplayFrame,
331    },
332    /// Warning.
333    Warning {
334        /// Common header.
335        meta: Meta,
336        /// Message.
337        message: String,
338    },
339    /// Error (including hook panics).
340    Error {
341        /// Common header.
342        meta: Meta,
343        /// Message.
344        message: String,
345    },
346}
347
348/// Event kind (for hook registration and filtering).
349#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
350#[non_exhaustive]
351pub enum EventKind {
352    /// [`Event::FrameIngested`].
353    FrameIngested,
354    /// [`Event::FrameDecoded`].
355    FrameDecoded,
356    /// [`Event::Started`].
357    Started,
358    /// [`Event::Log`].
359    Log,
360    /// [`Event::RefineStarted`].
361    RefineStarted,
362    /// [`Event::ZoneAdjusted`].
363    ZoneAdjusted,
364    /// [`Event::Finished`].
365    Finished,
366    /// [`Event::Reset`].
367    Reset,
368    /// [`Event::ZoneInvalidated`].
369    ZoneInvalidated,
370    /// [`Event::FeaturesExtracted`].
371    FeaturesExtracted,
372    /// [`Event::PairsMatched`].
373    PairsMatched,
374    /// [`Event::ModelInitialized`].
375    ModelInitialized,
376    /// [`Event::FrameRegistered`].
377    FrameRegistered,
378    /// [`Event::PositionDone`].
379    PositionDone,
380    /// [`Event::ZoneArrived`].
381    ZoneArrived,
382    /// [`Event::ZonePreview`].
383    ZonePreview,
384    /// [`Event::ZoneRefinedPose`].
385    ZoneRefinedPose,
386    /// [`Event::ZoneRefined`].
387    ZoneRefined,
388    /// [`Event::BaseAdopted`].
389    BaseAdopted,
390    /// [`Event::Reanchored`].
391    Reanchored,
392    /// [`Event::Snapshot`].
393    Snapshot,
394    /// [`Event::AllPositionsDone`].
395    AllPositionsDone,
396    /// [`Event::AllRefinedDone`].
397    AllRefinedDone,
398    /// [`Event::DisplayDelta`].
399    DisplayDelta,
400    /// [`Event::Warning`].
401    Warning,
402    /// [`Event::Error`].
403    Error,
404}
405
406impl Event {
407    /// Event kind.
408    pub fn kind(&self) -> EventKind {
409        use Event::*;
410        match self {
411            FrameIngested { .. } => EventKind::FrameIngested,
412            FrameDecoded { .. } => EventKind::FrameDecoded,
413            Started { .. } => EventKind::Started,
414            Log { .. } => EventKind::Log,
415            RefineStarted { .. } => EventKind::RefineStarted,
416            ZoneAdjusted { .. } => EventKind::ZoneAdjusted,
417            Finished { .. } => EventKind::Finished,
418            Reset { .. } => EventKind::Reset,
419            ZoneInvalidated { .. } => EventKind::ZoneInvalidated,
420            FeaturesExtracted { .. } => EventKind::FeaturesExtracted,
421            PairsMatched { .. } => EventKind::PairsMatched,
422            ModelInitialized { .. } => EventKind::ModelInitialized,
423            FrameRegistered { .. } => EventKind::FrameRegistered,
424            PositionDone { .. } => EventKind::PositionDone,
425            ZoneArrived { .. } => EventKind::ZoneArrived,
426            ZonePreview { .. } => EventKind::ZonePreview,
427            ZoneRefinedPose { .. } => EventKind::ZoneRefinedPose,
428            ZoneRefined { .. } => EventKind::ZoneRefined,
429            BaseAdopted { .. } => EventKind::BaseAdopted,
430            Reanchored { .. } => EventKind::Reanchored,
431            Snapshot { .. } => EventKind::Snapshot,
432            AllPositionsDone { .. } => EventKind::AllPositionsDone,
433            AllRefinedDone { .. } => EventKind::AllRefinedDone,
434            DisplayDelta { .. } => EventKind::DisplayDelta,
435            Warning { .. } => EventKind::Warning,
436            Error { .. } => EventKind::Error,
437        }
438    }
439    /// Common header.
440    pub fn meta(&self) -> &Meta {
441        use Event::*;
442        match self {
443            FrameIngested { meta, .. }
444            | FeaturesExtracted { meta, .. }
445            | PairsMatched { meta, .. }
446            | ModelInitialized { meta, .. }
447            | FrameRegistered { meta, .. }
448            | PositionDone { meta, .. }
449            | ZoneArrived { meta, .. }
450            | ZonePreview { meta, .. }
451            | ZoneRefinedPose { meta, .. }
452            | ZoneRefined { meta, .. }
453            | BaseAdopted { meta, .. }
454            | Reanchored { meta, .. }
455            | Snapshot { meta, .. }
456            | AllPositionsDone { meta }
457            | AllRefinedDone { meta, .. }
458            | FrameDecoded { meta, .. }
459            | Started { meta, .. }
460            | Log { meta, .. }
461            | RefineStarted { meta, .. }
462            | ZoneAdjusted { meta, .. }
463            | Finished { meta, .. }
464            | Reset { meta, .. }
465            | ZoneInvalidated { meta, .. }
466            | DisplayDelta { meta, .. }
467            | Warning { meta, .. }
468            | Error { meta, .. } => meta,
469        }
470    }
471}
472
473impl Event {
474    /// Event text for timeline.txt (None if there is none). The wording and format are the same as the existing record.
475    pub fn timeline_text(&self) -> Option<String> {
476        use Event::*;
477        Some(match self {
478            Started { positions, .. } => match positions {
479                Some(n) => format!("start NPOS={n}"),
480                None => "start".to_string(),
481            },
482            ModelInitialized { registered, .. } => format!("init_model Registered images: {registered}"),
483            BaseAdopted { zone, .. } => format!("adopt refined {zone} as base"),
484            PositionDone { position, registered, expected, .. } => format!("pos {position} registered {registered}/{expected}"),
485            ZoneArrived { range, .. } => format!("region {} arrived pos[{},{})", range.zone, range.lo, range.hi),
486            ZonePreview { range, .. } => format!("preview {} ready", range.zone),
487            RefineStarted { zone, .. } => format!("refined {zone} ba_start"),
488            ZoneRefinedPose { range, registered, mean_reproj_px, .. } => {
489                format!("refined {} pose_ready Registered images: {registered} Mean reprojection error: {mean_reproj_px:.6}px ", range.zone)
490            }
491            ZoneRefined { range, .. } => format!("refined {} ready", range.zone),
492            AllPositionsDone { .. } => "all positions done".to_string(),
493            AllRefinedDone { .. } => "all refined done".to_string(),
494            _ => return None,
495        })
496    }
497}
498
499/// Commands fed into the reducer.
500#[derive(Clone, Debug)]
501pub enum Command {
502    /// Reprocess from this position (discard later state).
503    ResetFrom(usize),
504    /// Invalidate the zone result and have it rebuilt.
505    InvalidateZone(usize),
506}