1use crate::blocks::{DiffStream, Heartbeat, LiveStream};
59use crate::events::{Event, EventKind, ZoneRange};
60use crate::pipeline::{Pipeline, Reducer};
61use crate::post::{self, PostZones, ZoneCloud, ZoneTimes};
62use crate::util::Logger;
63use cumulus3d_core::interop::{write_model_binary, ImageOrder};
64use cumulus3d_core::io::{write_ply, PlyLayout, PointCloud};
65use cumulus3d_core::Reconstruction;
66use std::collections::{BTreeMap, HashMap};
67use std::fs::File;
68use std::io::Write;
69use std::path::{Path, PathBuf};
70use std::sync::{Arc, Mutex};
71use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
72
73pub type Hook = Box<dyn FnMut(&Event) + Send>;
75
76pub const TIMELINE_TABLE: [(EventKind, &str); 11] = [
78 (EventKind::Started, "start NPOS={positions}"),
79 (EventKind::ModelInitialized, "init_model Registered images: {registered}"),
80 (EventKind::BaseAdopted, "adopt refined {zone} as base"),
81 (EventKind::PositionDone, "pos {position} registered {registered}/{expected}"),
82 (EventKind::ZoneArrived, "region {zone} arrived pos[{lo},{hi})"),
83 (EventKind::ZonePreview, "preview {zone} ready"),
84 (EventKind::RefineStarted, "refined {zone} ba_start"),
85 (
86 EventKind::ZoneRefinedPose,
87 "refined {zone} pose_ready Registered images: {registered} Mean reprojection error: {mean_reproj_px:.6}px ",
88 ),
89 (EventKind::ZoneRefined, "refined {zone} ready"),
90 (EventKind::AllPositionsDone, "all positions done"),
91 (EventKind::AllRefinedDone, "all refined done"),
92];
93
94pub trait Sink: Send {
96 fn kinds(&self) -> Vec<EventKind>;
98 fn handle(&mut self, e: &Event);
100}
101
102pub fn hooks<S: Sink + 'static>(sink: S) -> Vec<(EventKind, Hook)> {
104 let kinds = sink.kinds();
105 let shared = Arc::new(Mutex::new(sink));
106 kinds
107 .into_iter()
108 .map(|k| {
109 let s = Arc::clone(&shared);
110 let h: Hook = Box::new(move |e: &Event| s.lock().unwrap_or_else(|p| p.into_inner()).handle(e));
111 (k, h)
112 })
113 .collect()
114}
115
116pub fn any_hook<S: Sink + 'static>(mut sink: S) -> Hook {
118 let kinds = sink.kinds();
119 Box::new(move |e: &Event| {
120 if kinds.contains(&e.kind()) {
121 sink.handle(e)
122 }
123 })
124}
125
126fn epoch_secs(t: SystemTime) -> f64 {
127 t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs_f64()
128}
129
130fn zone_file(kind: &str, r: &ZoneRange) -> String {
131 format!("{kind}_{:02}_pos{}-{}.ply", r.zone, r.lo, r.hi)
132}
133
134#[derive(Clone)]
138pub struct RunLog {
139 inner: Arc<RunLogInner>,
140}
141
142struct RunLogInner {
143 logger: Logger,
144 extra_times: Mutex<Vec<(String, Duration)>>,
146}
147
148impl RunLog {
149 pub fn open(path: Option<&Path>, echo: bool) -> std::io::Result<Self> {
151 Ok(Self { inner: Arc::new(RunLogInner { logger: Logger::new(path, echo)?, extra_times: Mutex::new(Vec::new()) }) })
152 }
153 pub fn logger(&self) -> &Logger {
155 &self.inner.logger
156 }
157 pub fn line(&self, s: &str) {
159 self.inner.logger.line(s)
160 }
161 pub fn stamped_at(&self, at: SystemTime, s: &str) {
163 self.inner.logger.stamped_at(at, s)
164 }
165 pub fn add_time(&self, stage: &str, d: Duration) {
167 if let Ok(mut v) = self.inner.extra_times.lock() {
168 v.push((stage.to_string(), d));
169 }
170 }
171 fn take_times(&self) -> Vec<(String, Duration)> {
172 self.inner.extra_times.lock().map(|mut v| std::mem::take(&mut *v)).unwrap_or_default()
173 }
174}
175
176pub struct TimelineSink {
180 file: File,
181}
182
183impl TimelineSink {
184 pub fn create(path: &Path) -> std::io::Result<Self> {
186 Ok(Self { file: File::create(path)? })
187 }
188
189 pub fn line(e: &Event) -> Option<String> {
191 let s = e.timeline_text()?;
192 let t = e.meta().at.duration_since(UNIX_EPOCH).unwrap_or_default();
193 Some(format!("{}.{:09} {s}", t.as_secs(), t.subsec_nanos()))
194 }
195}
196
197impl Sink for TimelineSink {
198 fn kinds(&self) -> Vec<EventKind> {
199 TIMELINE_TABLE.iter().map(|(k, _)| *k).collect()
200 }
201 fn handle(&mut self, e: &Event) {
202 if let Some(l) = Self::line(e) {
203 let _ = writeln!(self.file, "{l}");
204 let _ = self.file.flush();
205 }
206 }
207}
208
209pub struct RunLogSink {
216 log: RunLog,
217 done: Option<PathBuf>,
218}
219
220impl RunLogSink {
221 pub fn new(log: RunLog, done: Option<PathBuf>) -> Self {
223 Self { log, done }
224 }
225}
226
227impl Sink for RunLogSink {
228 fn kinds(&self) -> Vec<EventKind> {
229 let mut v: Vec<EventKind> = TIMELINE_TABLE.iter().map(|(k, _)| *k).collect();
230 v.extend([EventKind::Log, EventKind::Finished]);
231 v
232 }
233 fn handle(&mut self, e: &Event) {
234 if let Some(s) = e.timeline_text() {
235 self.log.stamped_at(e.meta().at, &s);
236 return;
237 }
238 match e {
239 Event::Log { meta, line, stamped } => {
240 if *stamped {
241 self.log.stamped_at(meta.at, line)
242 } else {
243 self.log.line(line)
244 }
245 }
246 Event::Finished { pipeline_secs, stage_times, chain_stats, .. } => {
247 let mut m: BTreeMap<String, (Duration, usize)> = BTreeMap::new();
248 for (s, d, n) in stage_times {
249 let x = m.entry(s.clone()).or_default();
250 x.0 += *d;
251 x.1 += n;
252 }
253 for (s, d) in self.log.take_times() {
254 let x = m.entry(s).or_default();
255 x.0 += d;
256 x.1 += 1;
257 }
258 self.log.line(&format!("== 단계별 시간 (파이프라인 {pipeline_secs:.1}s, 배경 스레드와 겹친 누적)"));
259 for (stage, (d, n)) in m {
260 self.log.line(&format!("{stage:<14} {:>9.2}s ({n}회)", d.as_secs_f64()));
261 }
262 if !chain_stats.is_empty() {
263 self.log.line("== 최종 체인 모델");
264 for l in chain_stats {
265 self.log.line(l);
266 }
267 }
268 if let Some(p) = &self.done {
269 if let Err(err) = std::fs::write(p, b"") {
270 self.log.line(&format!("{}: {err}", p.display()));
271 }
272 }
273 }
274 _ => {}
275 }
276 }
277}
278
279pub struct ZonePlySink {
284 full: PathBuf,
285 log: Option<RunLog>,
286}
287
288impl ZonePlySink {
289 pub fn new(full: PathBuf, log: Option<RunLog>) -> Self {
291 Self { full, log }
292 }
293
294 fn write(&self, kind: &str, r: &ZoneRange, c: &PointCloud) {
295 let path = self.full.join(kind).join(zone_file(kind, r));
296 let res = path
297 .parent()
298 .map_or(Ok(()), std::fs::create_dir_all)
299 .map_err(|e| e.to_string())
300 .and_then(|_| write_ply(&path, c, PlyLayout::XyzNormalRgb).map_err(|e| e.to_string()));
301 if let (Err(e), Some(l)) = (res, &self.log) {
302 l.line(&format!("[dense] {kind} {} PLY 쓰기 실패: {e}", r.zone));
303 }
304 }
305}
306
307impl Sink for ZonePlySink {
308 fn kinds(&self) -> Vec<EventKind> {
309 vec![EventKind::ZonePreview, EventKind::ZoneRefined]
310 }
311 fn handle(&mut self, e: &Event) {
312 match e {
313 Event::ZonePreview { range, cloud, dense: true, .. } => self.write("preview", range, cloud),
314 Event::ZoneRefined { range, cloud, dense: true, .. } => self.write("refined", range, cloud),
315 _ => {}
316 }
317 }
318}
319
320pub struct ModelSink {
326 dir: PathBuf,
327 log: Option<RunLog>,
328}
329
330impl ModelSink {
331 pub fn new(dir: PathBuf, log: Option<RunLog>) -> Self {
333 Self { dir, log }
334 }
335
336 fn save(&self, rec: &Reconstruction, name: &str) {
337 let d = self.dir.join(name);
338 let _ = std::fs::create_dir_all(&d);
339 if let (Err(e), Some(l)) = (write_model_binary(rec, &d, ImageOrder::Registration), &self.log) {
340 l.line(&format!("모델 저장 실패 {}: {e}", d.display()));
341 }
342 }
343}
344
345impl Sink for ModelSink {
346 fn kinds(&self) -> Vec<EventKind> {
347 vec![EventKind::ZoneArrived, EventKind::ZonePreview, EventKind::ZoneAdjusted, EventKind::ZoneRefinedPose, EventKind::AllRefinedDone]
348 }
349 fn handle(&mut self, e: &Event) {
350 match e {
351 Event::ZoneArrived { range, model, .. } => self.save(model, &format!("snap_{}", range.zone)),
352 Event::ZonePreview { range, model, .. } => self.save(model, &format!("preview_{}", range.zone)),
353 Event::ZoneAdjusted { zone, model, .. } => self.save(model, &format!("ba_{zone}")),
354 Event::ZoneRefinedPose { range, model, .. } => self.save(model, &format!("refined_{}", range.zone)),
355 Event::AllRefinedDone { chain: Some(c), .. } => self.save(c, "chain"),
356 _ => {}
357 }
358 }
359}
360
361pub struct SnapshotSink {
371 out: PathBuf,
372 log: RunLog,
373 t0: Option<SystemTime>,
374 ranges: BTreeMap<usize, ZoneRange>,
375 times: BTreeMap<usize, Stamps>,
376 preview: BTreeMap<usize, Arc<PointCloud>>,
377 refined: BTreeMap<usize, Arc<PointCloud>>,
378 preview_models: BTreeMap<usize, Arc<Reconstruction>>,
379 refined_models: BTreeMap<usize, Arc<Reconstruction>>,
380 positions: HashMap<String, usize>,
381 live: post::LiveDisplay,
382 live_count: usize,
383 diff: Option<DiffStream>,
385 last_display: Option<(PointCloud, Vec<usize>)>,
386 beat: Option<Heartbeat>,
388}
389
390#[derive(Clone, Copy, Default)]
392struct Stamps {
393 arrived: Option<SystemTime>,
394 preview: Option<SystemTime>,
395 pose: Option<SystemTime>,
396 refined: Option<SystemTime>,
397}
398
399impl SnapshotSink {
400 pub fn new(out: PathBuf, log: RunLog) -> Self {
402 Self {
403 out,
404 log,
405 t0: None,
406 ranges: BTreeMap::new(),
407 times: BTreeMap::new(),
408 preview: BTreeMap::new(),
409 refined: BTreeMap::new(),
410 preview_models: BTreeMap::new(),
411 refined_models: BTreeMap::new(),
412 positions: HashMap::new(),
413 live: post::LiveDisplay::default(),
414 live_count: 0,
415 diff: None,
416 last_display: None,
417 beat: None,
418 }
419 }
420
421 fn beat(&mut self, at: SystemTime, f: impl FnOnce(&mut Heartbeat)) {
423 let Some(b) = self.beat.as_mut() else { return };
424 f(b);
425 let t = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
426 if let Err(e) = b.write((t * 10.0).round() / 10.0) {
427 self.log.line(&format!("[live] status.json 실패: {e}"));
428 }
429 }
430
431 pub fn with_live_stride(mut self, stride: usize) -> Self {
433 self.live = post::LiveDisplay::new(post::LiveOptions { stride, ..post::LiveOptions::default() });
434 self
435 }
436
437 pub fn with_live_stream(mut self, cfg: LiveStream) -> Self {
439 let dir = self.out.join("live").join("stream");
440 self.beat = Some(Heartbeat { dir: dir.clone(), stage: "registration", ..Default::default() });
441 self.diff = Some(DiffStream::new(dir, cfg));
442 self
443 }
444
445 fn drop_zone(&mut self, k: usize) {
446 self.ranges.remove(&k);
447 self.times.remove(&k);
448 self.preview.remove(&k);
449 self.refined.remove(&k);
450 self.preview_models.remove(&k);
451 self.refined_models.remove(&k);
452 self.live.forget(k);
453 }
454
455 fn write_live(&mut self, k: usize, at: SystemTime, kind: &str) {
458 let t = Instant::now();
459 let clouds: BTreeMap<usize, &PointCloud> = self.refined.iter().map(|(j, c)| (*j, &**c)).collect();
460 let models: BTreeMap<usize, &Reconstruction> = self.refined_models.iter().map(|(j, m)| (*j, &**m)).collect();
461 self.live.set_relocate(!self.preview_models.is_empty());
464 let (cloud, zones) = self.live.compose_zoned(&clouds, &models);
465 let dir = self.out.join("live");
466 let rel = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
467 self.live_count += 1;
468 let name = format!("display_{:02}_{rel:06.1}s_refined_{k}.ply", self.live_count);
469 let tmp = dir.join(".latest.ply.tmp");
470 let r = std::fs::create_dir_all(&dir)
471 .map_err(|e| e.to_string())
472 .and_then(|_| write_ply(dir.join(&name), &cloud, PlyLayout::XyzRgbNormal).map_err(|e| e.to_string()))
473 .and_then(|_| write_ply(&tmp, &cloud, PlyLayout::XyzRgbNormal).map_err(|e| e.to_string()))
474 .and_then(|_| std::fs::rename(&tmp, dir.join("latest.ply")).map_err(|e| e.to_string()));
475 self.log.add_time("live", t.elapsed());
476 match r {
477 Ok(()) => {
478 self.log.line(&format!("[live] {name}: 구역 {} 개, 점 {}, {:.2}s", clouds.len(), cloud.len(), t.elapsed().as_secs_f64()))
479 }
480 Err(e) => self.log.line(&format!("[live] {name} 실패: {e}")),
481 }
482 if self.diff.is_some() {
483 self.write_stream(k, rel, kind, cloud, zones);
484 }
485 }
486
487 fn write_stream(&mut self, zone: usize, t_rel: f64, kind: &str, cloud: PointCloud, zones: Vec<usize>) {
489 let t = Instant::now();
490 let Some(bs) = self.diff.as_mut() else { return };
491 let r = bs.write_event(t_rel, zone, kind, &cloud, &zones);
492 self.log.add_time("live_stream", t.elapsed());
493 match r {
494 Ok(st) => self.log.line(&format!(
495 "[live] stream seq {}: 추가 {} 이동 {} 제거 {}, 점 {}, {:.2} MB, {:.2}s",
496 bs.events_written(),
497 st.n_add,
498 st.n_move,
499 st.n_remove,
500 st.points,
501 st.bytes as f64 / 1e6,
502 t.elapsed().as_secs_f64()
503 )),
504 Err(e) => self.log.line(&format!("[live] stream {kind} 구역 {zone} 실패: {e}")),
505 }
506 self.last_display = Some((cloud, zones));
507 }
508
509 pub fn finish_with(&mut self, final_model: Option<&Reconstruction>) {
512 if self.preview_models.is_empty() && !self.refined.is_empty() {
513 let t0 = epoch_secs(self.t0.unwrap_or(UNIX_EPOCH));
514 let rel = |t: Option<SystemTime>| t.map(|t| epoch_secs(t) - t0);
515 let times: BTreeMap<usize, ZoneTimes> = self
516 .times
517 .iter()
518 .map(|(k, s)| {
519 (
520 *k,
521 ZoneTimes {
522 arrived: rel(s.arrived),
523 preview_ready: None,
524 refined_pose: rel(s.pose),
525 refined_ready: rel(s.refined),
526 },
527 )
528 })
529 .collect();
530 let files: BTreeMap<usize, String> =
531 self.refined.keys().filter_map(|k| self.ranges.get(k).map(|r| (*k, zone_file("refined", r)))).collect();
532 let rf = self.refined.iter().filter_map(|(k, c)| files.get(k).map(|f| (*k, ZoneCloud { file: f.clone(), cloud: c }))).collect();
533 let display = self.refined_models.values().next_back().map(|m| &**m);
534 let t = Instant::now();
535 let models: BTreeMap<usize, &Reconstruction> = self.refined_models.iter().map(|(k, m)| (*k, &**m)).collect();
536 let r = post::run_stable(&self.out, &rf, &models, ×, display, final_model, self.log.logger());
537 self.log.add_time("post", t.elapsed());
538 if let Err(e) = r {
539 self.log.line(&format!("[post] 실패: {e}"));
540 }
541 let _ = std::fs::copy(self.out.join("timeline.txt"), self.out.join("snapshots").join("timeline.txt"));
542 return;
543 }
544 self.finish();
545 }
546
547 pub fn finish(&mut self) {
550 let t0 = epoch_secs(self.t0.unwrap_or(UNIX_EPOCH));
551 let rel = |t: Option<SystemTime>| t.map(|t| epoch_secs(t) - t0);
552 let times: BTreeMap<usize, ZoneTimes> = self
553 .times
554 .iter()
555 .map(|(k, s)| {
556 (
557 *k,
558 ZoneTimes {
559 arrived: rel(s.arrived),
560 preview_ready: rel(s.preview),
561 refined_pose: rel(s.pose),
562 refined_ready: rel(s.refined),
563 },
564 )
565 })
566 .collect();
567 let windows = self
568 .ranges
569 .iter()
570 .map(|(k, r)| {
571 let hi = k.checked_sub(1).and_then(|j| self.ranges.get(&j)).map_or(r.hi, |p| p.hi);
572 (*k, (r.lo, hi))
573 })
574 .collect();
575 let zc = |kind: &str, m: &BTreeMap<usize, Arc<PointCloud>>| -> BTreeMap<usize, (String, Arc<PointCloud>)> {
576 m.iter().filter_map(|(k, c)| self.ranges.get(k).map(|r| (*k, (zone_file(kind, r), Arc::clone(c))))).collect()
577 };
578 let (pv, rf) = (zc("preview", &self.preview), zc("refined", &self.refined));
579 let positions = &self.positions;
580 let pos = move |n: &str| positions.get(n).copied();
581 let z = PostZones {
582 out: &self.out,
583 preview: pv.iter().map(|(k, (f, c))| (*k, ZoneCloud { file: f.clone(), cloud: c })).collect(),
584 refined: rf.iter().map(|(k, (f, c))| (*k, ZoneCloud { file: f.clone(), cloud: c })).collect(),
585 preview_models: self.preview_models.iter().map(|(k, m)| (*k, &**m)).collect(),
586 refined_models: self.refined_models.iter().map(|(k, m)| (*k, &**m)).collect(),
587 times,
588 windows,
589 position: &pos,
590 };
591 let t = Instant::now();
592 let r = post::run_zones(&z, self.log.logger());
593 self.log.add_time("post", t.elapsed());
594 if let Err(e) = r {
595 self.log.line(&format!("[post] 실패: {e}"));
596 }
597 let _ = std::fs::copy(self.out.join("timeline.txt"), self.out.join("snapshots").join("timeline.txt"));
598 }
599}
600
601impl Sink for SnapshotSink {
602 fn kinds(&self) -> Vec<EventKind> {
603 vec![
604 EventKind::Started,
605 EventKind::FrameIngested,
606 EventKind::FeaturesExtracted,
607 EventKind::ZoneArrived,
608 EventKind::ZonePreview,
609 EventKind::PositionDone,
610 EventKind::RefineStarted,
611 EventKind::ZoneRefinedPose,
612 EventKind::ZoneRefined,
613 EventKind::Reset,
614 EventKind::ZoneInvalidated,
615 EventKind::AllRefinedDone,
616 ]
617 }
618
619 fn handle(&mut self, e: &Event) {
620 let at = e.meta().at;
621 if matches!(e, Event::Started { .. }) || self.t0.is_none() {
622 self.t0 = Some(at);
623 }
624 match e {
625 Event::Started { positions, .. } => self.beat(at, |b| b.positions_total = *positions),
626 Event::PositionDone { position, registered, .. } => {
627 let (p, r) = (*position + 1, *registered);
628 self.beat(at, |b| {
629 b.stage = "triangulation";
630 b.positions = p;
631 b.images = r;
632 });
633 }
634 Event::RefineStarted { zone, .. } => self.beat(at, |b| {
635 b.stage = "ba";
636 b.zone = *zone;
637 }),
638 Event::FrameIngested { position, images, .. } => {
639 for n in images {
640 self.positions.insert(n.clone(), *position);
641 }
642 }
643 Event::FeaturesExtracted { image, position, .. } => {
644 self.positions.insert(image.clone(), *position);
645 }
646 Event::ZoneArrived { range, .. } => {
647 self.ranges.insert(range.zone, *range);
648 self.times.entry(range.zone).or_default().arrived.get_or_insert(at);
649 }
650 Event::ZonePreview { range, cloud, model, dense, .. } => {
651 self.ranges.entry(range.zone).or_insert(*range);
652 self.times.entry(range.zone).or_default().preview.get_or_insert(at);
653 self.preview_models.insert(range.zone, Arc::clone(model));
654 if *dense {
655 self.preview.insert(range.zone, Arc::clone(cloud));
656 }
657 }
658 Event::ZoneRefinedPose { range, model, .. } => {
659 self.beat(at, |b| {
660 b.stage = "dense";
661 b.zone = range.zone;
662 });
663 self.ranges.entry(range.zone).or_insert(*range);
664 self.times.entry(range.zone).or_default().pose.get_or_insert(at);
665 self.refined_models.insert(range.zone, Arc::clone(model));
666 self.live.forget(range.zone);
667 }
668 Event::ZoneRefined { range, cloud, dense, .. } => {
669 self.ranges.entry(range.zone).or_insert(*range);
670 self.times.entry(range.zone).or_default().refined.get_or_insert(at);
671 if *dense {
672 let kind = if self.refined.contains_key(&range.zone) { "replace" } else { "refined" };
674 self.refined.insert(range.zone, Arc::clone(cloud));
675 self.write_live(range.zone, at, kind);
676 }
677 }
678 Event::Reset { position, .. } => {
679 let ks: Vec<usize> = self.ranges.values().filter(|r| r.hi > *position).map(|r| r.zone).collect();
680 for k in ks {
681 self.drop_zone(k);
682 }
683 }
684 Event::ZoneInvalidated { zone, .. } => self.drop_zone(*zone),
685 Event::AllRefinedDone { chain, .. } => {
686 self.beat(at, |b| b.stage = "done");
687 if let Some((cloud, zones)) = self.last_display.take() {
689 let rel = epoch_secs(at) - epoch_secs(self.t0.unwrap_or(at));
690 let zone = self.refined.keys().next_back().copied().unwrap_or(0);
691 self.write_stream(zone, rel, "final", cloud, zones);
692 }
693 self.finish_with(chain.as_deref())
694 }
695 _ => {}
696 }
697 }
698}
699
700#[derive(Clone, Debug, Default)]
704pub struct SinkOptions {
705 pub echo: bool,
707 pub save_models: bool,
709}
710
711#[derive(Clone, Copy, Debug, PartialEq)]
713pub struct SinkSet {
714 pub timeline: bool,
716 pub run_log: bool,
718 pub zone_ply: bool,
720 pub models: bool,
722 pub snapshots: bool,
724 pub live_stream: Option<LiveStream>,
726 pub live_stride: usize,
728}
729
730impl SinkSet {
731 pub fn all() -> Self {
733 Self {
734 timeline: true,
735 run_log: true,
736 zone_ply: true,
737 models: true,
738 snapshots: true,
739 live_stream: None,
740 live_stride: post::LiveOptions::default().stride,
741 }
742 }
743}
744
745pub struct DefaultSinks {
747 sinks: Vec<Box<dyn Sink>>,
748 log: RunLog,
749}
750
751impl DefaultSinks {
752 pub fn new(out: &Path, opts: &SinkOptions) -> std::io::Result<Self> {
755 Self::with_set(out, opts.echo, SinkSet { models: opts.save_models, ..SinkSet::all() })
756 }
757
758 pub fn with_set(out: &Path, echo: bool, set: SinkSet) -> std::io::Result<Self> {
762 let dirs: &[&str] = if set.zone_ply { &["full/preview", "full/refined", "work"] } else { &["work"] };
763 for d in dirs {
764 std::fs::create_dir_all(out.join(d))?;
765 }
766 let run_log = out.join("run.log");
767 let log = RunLog::open(set.run_log.then_some(run_log.as_path()), echo)?;
768 let mut sinks: Vec<Box<dyn Sink>> = Vec::new();
769 if set.timeline {
770 sinks.push(Box::new(TimelineSink::create(&out.join("timeline.txt"))?));
771 }
772 sinks.push(Box::new(RunLogSink::new(log.clone(), set.run_log.then(|| out.join("DONE")))));
773 if set.zone_ply {
774 sinks.push(Box::new(ZonePlySink::new(out.join("full"), Some(log.clone()))));
775 }
776 if set.models {
777 sinks.push(Box::new(ModelSink::new(out.join("work").join("models"), Some(log.clone()))));
778 }
779 if set.snapshots {
780 let mut snap = SnapshotSink::new(out.to_path_buf(), log.clone()).with_live_stride(set.live_stride);
781 if let Some(cfg) = set.live_stream {
782 snap = snap.with_live_stream(cfg);
783 }
784 sinks.push(Box::new(snap));
785 }
786 Ok(Self { sinks, log })
787 }
788
789 pub fn run_log(&self) -> RunLog {
791 self.log.clone()
792 }
793
794 pub fn into_hook(self) -> Hook {
796 any_hook(self)
797 }
798}
799
800impl Sink for DefaultSinks {
801 fn kinds(&self) -> Vec<EventKind> {
802 let mut v: Vec<EventKind> = Vec::new();
803 for s in &self.sinks {
804 for k in s.kinds() {
805 if !v.contains(&k) {
806 v.push(k);
807 }
808 }
809 }
810 v
811 }
812 fn handle(&mut self, e: &Event) {
813 let k = e.kind();
814 for s in &mut self.sinks {
815 if s.kinds().contains(&k) {
816 s.handle(e);
817 }
818 }
819 }
820}
821
822pub fn default_sinks(out: &Path, opts: &SinkOptions) -> std::io::Result<Vec<(EventKind, Hook)>> {
824 Ok(hooks(DefaultSinks::new(out, opts)?))
825}
826
827pub fn attach<R: Reducer>(p: Pipeline<R>, out: &Path, opts: &SinkOptions) -> std::io::Result<Pipeline<R>> {
829 Ok(p.on_any(DefaultSinks::new(out, opts)?.into_hook()))
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use crate::events::Meta;
836
837 fn meta(s: u64, n: u32) -> Meta {
838 Meta { version: 0, at: UNIX_EPOCH + Duration::new(s, n) }
839 }
840 fn r(zone: usize, lo: usize, hi: usize) -> ZoneRange {
841 ZoneRange { zone, lo, hi }
842 }
843
844 #[test]
845 fn heartbeat_status_json_follows_events() {
846 let dir = std::env::temp_dir().join(format!("c3d_status_{}", std::process::id()));
847 let _ = std::fs::remove_dir_all(&dir);
848 let log = RunLog::open(None, false).unwrap();
849 let cfg = LiveStream { enu: false, origin_gps: None };
850 let mut s = SnapshotSink::new(dir.clone(), log).with_live_stream(cfg);
851 let read = || crate::util::Json::parse(&std::fs::read_to_string(dir.join("live/stream/status.json")).unwrap()).unwrap();
852 s.handle(&Event::Started { meta: meta(100, 0), positions: Some(26) });
853 s.handle(&Event::PositionDone { meta: meta(102, 500_000_000), position: 14, registered: 44, expected: 45 });
854 let j = read();
855 assert_eq!(
856 j.dump(),
857 crate::util::Json::obj(vec![
858 ("t", crate::util::Json::Num("2.5".into())),
859 ("stage", crate::util::Json::Str("triangulation".into())),
860 ("zone", crate::util::Json::Int(0)),
861 ("positions", crate::util::Json::Int(15)),
862 ("positions_total", crate::util::Json::Int(26)),
863 ("images", crate::util::Json::Int(44)),
864 ])
865 .dump()
866 );
867 s.handle(&Event::RefineStarted { meta: meta(103, 0), zone: 1 });
868 let j = read();
869 assert_eq!(j.get("stage").map(|v| v.dump()), Some("\"ba\"".into()));
870 assert_eq!(j.get("zone").map(|v| v.dump()), Some("1".into()));
871 assert!(!dir.join("live/stream/status.json.tmp").exists());
872 let _ = std::fs::remove_dir_all(&dir);
873 }
874
875 #[test]
876 fn timeline_table() {
877 let m = || meta(1_700_000_000, 500);
879 let model = Arc::new(Reconstruction::new());
880 let cloud = Arc::new(PointCloud::default());
881 let cases: Vec<(Event, &str)> = vec![
882 (Event::Started { meta: m(), positions: Some(26) }, "start NPOS=26"),
883 (Event::ModelInitialized { meta: m(), registered: 42, position: 13, model: model.clone() }, "init_model Registered images: 42"),
884 (Event::BaseAdopted { meta: m(), zone: 0 }, "adopt refined 0 as base"),
885 (Event::PositionDone { meta: m(), position: 14, registered: 44, expected: 45 }, "pos 14 registered 44/45"),
886 (Event::ZoneArrived { meta: m(), range: r(1, 10, 26), model: model.clone() }, "region 1 arrived pos[10,26)"),
887 (
888 Event::ZonePreview {
889 meta: m(),
890 range: r(1, 10, 26),
891 cloud: cloud.clone(),
892 model: model.clone(),
893 dense: true,
894 frame: "gps".into(),
895 },
896 "preview 1 ready",
897 ),
898 (Event::RefineStarted { meta: m(), zone: 1 }, "refined 1 ba_start"),
899 (
900 Event::ZoneRefinedPose { meta: m(), range: r(1, 10, 26), registered: 48, mean_reproj_px: 0.25095, model: model.clone() },
901 "refined 1 pose_ready Registered images: 48 Mean reprojection error: 0.250950px ",
902 ),
903 (Event::ZoneRefined { meta: m(), range: r(1, 10, 26), cloud, model, dense: true }, "refined 1 ready"),
904 (Event::AllPositionsDone { meta: m() }, "all positions done"),
905 (Event::AllRefinedDone { meta: m(), chain: None }, "all refined done"),
906 ];
907 assert_eq!(cases.len(), TIMELINE_TABLE.len());
908 for ((e, want), (k, _)) in cases.iter().zip(TIMELINE_TABLE.iter()) {
909 assert_eq!(e.kind(), *k);
910 assert_eq!(TimelineSink::line(e).unwrap(), format!("1700000000.000000500 {want}"));
911 }
912 assert!(TimelineSink::line(&Event::Warning { meta: m(), message: "x".into() }).is_none());
913 }
914}