1use anyhow::Result;
9use clap::Subcommand;
10use colored::Colorize;
11use dialoguer::{Input, Select};
12use indicatif::{ProgressBar, ProgressStyle};
13#[allow(unused_imports)]
14use raps_kernel::{progress, prompts};
15use serde::Serialize;
16use std::path::PathBuf;
17use std::time::Duration;
18
19use crate::output::OutputFormat;
20use raps_kernel::interactive;
21use raps_reality::{OutputFormat as RealityOutputFormat, RealityCaptureClient, SceneType};
23
24#[derive(Debug, Subcommand)]
25pub enum RealityCommands {
26 List,
28
29 Create {
31 #[arg(short, long)]
33 name: Option<String>,
34
35 #[arg(short, long)]
37 scene_type: Option<String>,
38
39 #[arg(short, long)]
41 format: Option<String>,
42 },
43
44 Upload {
46 photoscene_id: String,
48
49 #[arg(required = true)]
51 photos: Vec<PathBuf>,
52 },
53
54 Process {
56 photoscene_id: String,
58 },
59
60 Status {
62 photoscene_id: String,
64
65 #[arg(short, long)]
67 wait: bool,
68 },
69
70 Result {
72 photoscene_id: String,
74
75 #[arg(short, long, default_value = "obj")]
77 format: String,
78 },
79
80 Formats,
82
83 Delete {
85 photoscene_id: String,
87 },
88}
89
90impl RealityCommands {
91 pub async fn execute(
92 self,
93 client: &RealityCaptureClient,
94 output_format: OutputFormat,
95 ) -> Result<()> {
96 match self {
97 RealityCommands::List => list_photoscenes(client, output_format).await,
98 RealityCommands::Create {
99 name,
100 scene_type,
101 format,
102 } => create_photoscene(client, name, scene_type, format, output_format).await,
103 RealityCommands::Upload {
104 photoscene_id,
105 photos,
106 } => upload_photos(client, &photoscene_id, photos, output_format).await,
107 RealityCommands::Process { photoscene_id } => {
108 start_processing(client, &photoscene_id, output_format).await
109 }
110 RealityCommands::Status {
111 photoscene_id,
112 wait,
113 } => check_status(client, &photoscene_id, wait, output_format).await,
114 RealityCommands::Result {
115 photoscene_id,
116 format,
117 } => get_result(client, &photoscene_id, &format, output_format).await,
118 RealityCommands::Formats => list_formats(client, output_format),
119 RealityCommands::Delete { photoscene_id } => {
120 delete_photoscene(client, &photoscene_id, output_format).await
121 }
122 }
123 }
124}
125
126async fn list_photoscenes(
127 client: &RealityCaptureClient,
128 output_format: OutputFormat,
129) -> Result<()> {
130 if output_format.supports_colors() {
131 println!("{}", "Fetching photoscenes...".dimmed());
132 }
133
134 let photoscenes = client.list_photoscenes().await?;
135
136 #[derive(Serialize)]
137 struct PhotosceneOutput {
138 id: String,
139 name: String,
140 scene_type: String,
141 status: String,
142 progress: String,
143 }
144
145 let photoscene_outputs: Vec<PhotosceneOutput> = photoscenes
146 .iter()
147 .map(|p| PhotosceneOutput {
148 id: p.photoscene_id.clone(),
149 name: p.name.clone().unwrap_or_default(),
150 scene_type: p.scene_type.clone().unwrap_or_default(),
151 status: p.status.clone().unwrap_or_default(),
152 progress: p.progress.clone().unwrap_or_default(),
153 })
154 .collect();
155
156 if photoscene_outputs.is_empty() {
157 match output_format {
158 OutputFormat::Table => println!("{}", "No photoscenes found.".yellow()),
159 _ => {
160 output_format.write(&Vec::<PhotosceneOutput>::new())?;
161 }
162 }
163 return Ok(());
164 }
165
166 match output_format {
167 OutputFormat::Table => {
168 println!("\n{}", "Photoscenes:".bold());
169 println!(
170 "{:<30} {:<20} {:<10} {:<12} {}",
171 "ID".bold(),
172 "Name".bold(),
173 "Type".bold(),
174 "Status".bold(),
175 "Progress".bold()
176 );
177 println!("{}", "-".repeat(90));
178
179 for scene in &photoscene_outputs {
180 let status_colored = match scene.status.as_str() {
181 "Done" | "Created" => scene.status.green().to_string(),
182 "Error" => scene.status.red().to_string(),
183 "Processing" => scene.status.yellow().to_string(),
184 _ => scene.status.clone(),
185 };
186 println!(
187 "{:<30} {:<20} {:<10} {:<12} {}",
188 scene.id, scene.name, scene.scene_type, status_colored, scene.progress
189 );
190 }
191
192 println!("{}", "-".repeat(90));
193 }
194 _ => {
195 output_format.write(&photoscene_outputs)?;
196 }
197 }
198 Ok(())
199}
200
201async fn create_photoscene(
202 client: &RealityCaptureClient,
203 name: Option<String>,
204 scene_type: Option<String>,
205 format: Option<String>,
206 output_format: OutputFormat,
207) -> Result<()> {
208 let scene_name = match name {
210 Some(n) => n,
211 None => {
212 if interactive::is_non_interactive() {
214 anyhow::bail!(
215 "Photoscene name is required in non-interactive mode. Use --name flag."
216 );
217 }
218 Input::new()
219 .with_prompt("Enter photoscene name")
220 .interact_text()?
221 }
222 };
223
224 let selected_scene_type = match scene_type {
226 Some(t) => match t.to_lowercase().as_str() {
227 "aerial" => SceneType::Aerial,
228 "object" => SceneType::Object,
229 _ => anyhow::bail!("Invalid scene type. Use 'aerial' or 'object'"),
230 },
231 None => {
232 if interactive::is_non_interactive() {
234 SceneType::Object
235 } else {
236 let types = vec!["aerial (drone/outdoor)", "object (turntable/indoor)"];
237 let selection = Select::new()
238 .with_prompt("Select scene type")
239 .items(&types)
240 .interact()?;
241 if selection == 0 {
242 SceneType::Aerial
243 } else {
244 SceneType::Object
245 }
246 }
247 }
248 };
249
250 let selected_format = match format {
252 Some(f) => parse_format(&f)?,
253 None => {
254 if interactive::is_non_interactive() {
256 RealityOutputFormat::Obj
257 } else {
258 let formats = RealityOutputFormat::all();
259 let format_labels: Vec<String> = formats
260 .iter()
261 .map(|f| format!("{} - {}", f, f.description()))
262 .collect();
263
264 let selection = Select::new()
265 .with_prompt("Select output format")
266 .items(&format_labels)
267 .default(2) .interact()?;
269
270 formats[selection]
271 }
272 }
273 };
274
275 if output_format.supports_colors() {
276 println!("{}", "Creating photoscene...".dimmed());
277 }
278
279 let photoscene = client
280 .create_photoscene(&scene_name, selected_scene_type, selected_format)
281 .await?;
282
283 #[derive(Serialize)]
284 struct CreatePhotosceneOutput {
285 success: bool,
286 photoscene_id: String,
287 name: String,
288 }
289
290 let output = CreatePhotosceneOutput {
291 success: true,
292 photoscene_id: photoscene.photoscene_id.clone(),
293 name: scene_name.clone(),
294 };
295
296 match output_format {
297 OutputFormat::Table => {
298 println!("{} Photoscene created!", "✓".green().bold());
299 println!(" {} {}", "ID:".bold(), output.photoscene_id.cyan());
300 println!(" {} {}", "Name:".bold(), output.name);
301
302 println!("\n{}", "Next steps:".yellow());
303 println!(
304 " 1. Upload photos: raps reality upload {} <photo1.jpg> <photo2.jpg> ...",
305 output.photoscene_id
306 );
307 println!(
308 " 2. Start processing: raps reality process {}",
309 output.photoscene_id
310 );
311 println!(
312 " 3. Check status: raps reality status {} --wait",
313 output.photoscene_id
314 );
315 }
316 _ => {
317 output_format.write(&output)?;
318 }
319 }
320
321 Ok(())
322}
323
324async fn upload_photos(
325 client: &RealityCaptureClient,
326 photoscene_id: &str,
327 photos: Vec<PathBuf>,
328 _output_format: OutputFormat,
329) -> Result<()> {
330 for photo in &photos {
332 if !photo.exists() {
333 anyhow::bail!("File not found: {}", photo.display());
334 }
335 }
336
337 let pb = ProgressBar::new(photos.len() as u64);
338 pb.set_style(
339 ProgressStyle::default_bar()
340 .template("{msg} [{bar:40.cyan/blue}] {pos}/{len}")
341 .expect("valid progress template")
342 .progress_chars("█▓░"),
343 );
344 pb.set_message("Uploading photos");
345
346 let photo_refs: Vec<&std::path::Path> = photos.iter().map(|p| p.as_path()).collect();
348
349 for chunk in photo_refs.chunks(5) {
350 client.upload_photos(photoscene_id, chunk).await?;
351 pb.inc(chunk.len() as u64);
352 }
353
354 pb.finish_with_message("Upload complete");
355
356 println!("{} Uploaded {} photos!", "✓".green().bold(), photos.len());
357 Ok(())
358}
359
360async fn start_processing(
361 client: &RealityCaptureClient,
362 photoscene_id: &str,
363 _output_format: OutputFormat,
364) -> Result<()> {
365 println!("{}", "Starting processing...".dimmed());
366
367 client.start_processing(photoscene_id).await?;
368
369 println!("{} Processing started!", "✓".green().bold());
370 println!(
371 " {}",
372 "Use 'raps reality status <id> --wait' to monitor progress".dimmed()
373 );
374 Ok(())
375}
376
377async fn check_status(
378 client: &RealityCaptureClient,
379 photoscene_id: &str,
380 wait: bool,
381 _output_format: OutputFormat,
382) -> Result<()> {
383 if wait {
384 let spinner = ProgressBar::new_spinner();
385 spinner.set_style(
386 ProgressStyle::default_spinner()
387 .template("{spinner:.cyan} {msg}")
388 .expect("valid progress template"),
389 );
390 spinner.enable_steady_tick(Duration::from_millis(100));
391
392 let timeout = Duration::from_secs(4 * 60 * 60);
394 let start = std::time::Instant::now();
395
396 loop {
397 if start.elapsed() > timeout {
398 spinner.finish_with_message(format!(
399 "{} Timed out after {} hours. Use 'raps reality status {}' to check later.",
400 "⏱".yellow().bold(),
401 timeout.as_secs() / 3600,
402 photoscene_id
403 ));
404 break;
405 }
406
407 let progress = client.get_progress(photoscene_id).await?;
408 let msg = progress.progress_msg.as_deref().unwrap_or("");
409 spinner.set_message(format!("Progress: {}% - {}", progress.progress, msg));
410
411 if progress.progress == "100" || progress.status.as_deref() == Some("Done") {
412 spinner.finish_with_message(format!("{} Processing complete!", "✓".green().bold()));
413 break;
414 }
415
416 if progress.status.as_deref() == Some("Error") {
417 spinner.finish_with_message(format!(
418 "{} Processing failed: {}",
419 "✗".red().bold(),
420 msg
421 ));
422 break;
423 }
424
425 tokio::time::sleep(Duration::from_secs(10)).await;
426 }
427 } else {
428 let progress = client.get_progress(photoscene_id).await?;
429
430 println!("{}", "Photoscene Status:".bold());
431 println!(" {} {}%", "Progress:".bold(), progress.progress.cyan());
432
433 if let Some(ref status) = progress.status {
434 println!(" {} {}", "Status:".bold(), status);
435 }
436
437 if let Some(ref msg) = progress.progress_msg {
438 println!(" {} {}", "Message:".bold(), msg.dimmed());
439 }
440 }
441
442 Ok(())
443}
444
445async fn get_result(
446 client: &RealityCaptureClient,
447 photoscene_id: &str,
448 format: &str,
449 _output_format: OutputFormat,
450) -> Result<()> {
451 let output_format = parse_format(format)?;
452
453 println!("{}", "Fetching result...".dimmed());
454
455 let result = client.get_result(photoscene_id, output_format).await?;
456
457 println!("{}", "Photoscene Result:".bold());
458 println!(" {} {}", "ID:".bold(), result.photoscene_id);
459 println!(" {} {}%", "Progress:".bold(), result.progress);
460
461 if let Some(ref link) = result.scene_link {
462 println!("\n{}", "Download Link:".green().bold());
463 println!(" {}", link);
464 } else {
465 println!(
466 "{}",
467 "No download link available yet. Processing may still be in progress.".yellow()
468 );
469 }
470
471 if let Some(bytes) = result.filesize_bytes() {
472 let display = if bytes >= 1_073_741_824 {
473 format!("{:.2} GB", bytes as f64 / 1_073_741_824.0)
474 } else if bytes >= 1_048_576 {
475 format!("{:.2} MB", bytes as f64 / 1_048_576.0)
476 } else if bytes >= 1024 {
477 format!("{:.2} KB", bytes as f64 / 1024.0)
478 } else {
479 format!("{bytes} B")
480 };
481 println!(" {} {}", "File Size:".bold(), display);
482 } else if let Some(ref size) = result.file_size {
483 println!(" {} {}", "File Size:".bold(), size);
484 }
485
486 Ok(())
487}
488
489fn list_formats(client: &RealityCaptureClient, _output_format: OutputFormat) -> Result<()> {
490 let formats = client.available_formats();
491
492 println!("\n{}", "Available Output Formats:".bold());
493 println!("{}", "─".repeat(60));
494
495 for format in formats {
496 println!(
497 " {} {} - {}",
498 "•".cyan(),
499 format,
500 format.description().dimmed()
501 );
502 }
503
504 println!("{}", "─".repeat(60));
505 Ok(())
506}
507
508async fn delete_photoscene(
509 client: &RealityCaptureClient,
510 photoscene_id: &str,
511 _output_format: OutputFormat,
512) -> Result<()> {
513 println!("{}", "Deleting photoscene...".dimmed());
514
515 client.delete_photoscene(photoscene_id).await?;
516
517 println!(
518 "{} Photoscene '{}' deleted!",
519 "✓".green().bold(),
520 photoscene_id
521 );
522 Ok(())
523}
524
525fn parse_format(s: &str) -> Result<RealityOutputFormat> {
526 match s.to_lowercase().as_str() {
527 "rcm" => Ok(RealityOutputFormat::Rcm),
528 "rcs" => Ok(RealityOutputFormat::Rcs),
529 "obj" => Ok(RealityOutputFormat::Obj),
530 "fbx" => Ok(RealityOutputFormat::Fbx),
531 "ortho" => Ok(RealityOutputFormat::Ortho),
532 _ => anyhow::bail!("Invalid format. Use: rcm, rcs, obj, fbx, ortho"),
533 }
534}