diff --git a/Cargo.toml b/Cargo.toml index ae9329b..80d0dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ png = "0.18.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] env_logger = "0.11.11" +clap = { version = "4.5.51", features = ["derive"] } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-channel = { version = "0.3.34", default-features = false, features = ["alloc", "std"] } diff --git a/src/app.rs b/src/app.rs index 686e223..92b1cd5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,8 @@ use eframe::CreationContext; use eframe::egui_wgpu; use eframe::egui_wgpu::wgpu; +#[cfg(not(target_arch = "wasm32"))] +use crate::cli::Cli; use crate::fractal::{ BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms, ExportRender, FractalCallback, FractalKind, FractalRenderer, MAX_REF_POINTS, ShareState, Uniforms, @@ -15,6 +17,8 @@ use crate::view::{ Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str, precision_for, }; +#[cfg(not(target_arch = "wasm32"))] +use clap::Parser; const BAILOUT_SQ: f32 = 1.0e6; /// Cap on exported image dimension (px), to stay within GPU texture limits. @@ -338,6 +342,9 @@ pub struct FractalApp { export_requested: bool, /// Progress/handle for an in-flight PNG export, if any. export: Option>>, + /// Output path for `--export` (native CLI only); falls back to a + /// timestamped name when unset. + export_path: Option, /// Short status line (saved path, "link copied", errors). status: Option, @@ -442,6 +449,7 @@ impl FractalApp { last_interact_time: -1.0e9, export_requested: false, export: None, + export_path: None, status: None, center_re_edit, center_im_edit, @@ -457,28 +465,19 @@ impl FractalApp { } } - // Debug/testing hooks. + // Debug/testing hooks, driven by CLI flags. #[cfg(not(target_arch = "wasm32"))] { - if let Ok(k) = std::env::var("MANDEL_KIND") { - app.kind = match k.trim().to_ascii_lowercase().as_str() { - "burningship" | "burning_ship" | "ship" => FractalKind::BurningShip, - "tricorn" | "mandelbar" => FractalKind::Tricorn, - "multibrot" | "multi" => FractalKind::Multibrot, - "celtic" => FractalKind::Celtic, - "perpendicular" | "perp" => FractalKind::Perpendicular, - "buffalo" => FractalKind::Buffalo, - "phoenix" => FractalKind::Phoenix, - _ => FractalKind::Mandelbrot, - }; - if let Ok(p) = std::env::var("MANDEL_POWER") - && let Ok(p) = p.trim().parse::() - { + let cli = Cli::parse(); + + if let Some(k) = cli.kind { + app.kind = k.into(); + if let Some(p) = cli.power { app.power = p.clamp(2, 8); } app.view = Self::default_view_for(app.mode, app.kind); } - if let Ok(jc) = std::env::var("MANDEL_JULIA") { + if let Some(jc) = cli.julia { let p: Vec<&str> = jc.split(',').collect(); if let (Some(Ok(re)), Some(Ok(im))) = ( p.first().map(|s| s.trim().parse::()), @@ -489,26 +488,25 @@ impl FractalApp { app.view = Self::default_view_for(FractalMode::Julia, app.kind); } } - if let Ok(frag) = std::env::var("MANDEL_SHARE") + if let Some(frag) = cli.share && let Some(state) = ShareState::decode(&frag) { app.apply_share(&state); } - if let Ok(spec) = std::env::var("MANDEL_VIEW") { + if let Some(spec) = cli.view { app.apply_view_spec(&spec); } - if std::env::var("MANDEL_DE").is_ok() { + if cli.de { app.de_coloring = true; } - if std::env::var("MANDEL_BUDDHABROT").is_ok() { + if cli.buddhabrot { app.buddhabrot = true; } - if let Ok(p) = std::env::var("MANDEL_BUDDHA_PALETTE") - && let Ok(p) = p.trim().parse::() - { + if let Some(p) = cli.buddha_palette { app.buddha_palette = p.min(BUDDHA_PALETTE_NAMES.len() as u32 - 1); } - if std::env::var("MANDEL_EXPORT").is_ok() { + app.export_path = cli.export_path; + if cli.export { app.export_requested = true; } } @@ -912,8 +910,10 @@ impl FractalApp { #[cfg(not(target_arch = "wasm32"))] { - let name = std::env::var("MANDEL_EXPORT_PATH") - .unwrap_or_else(|_| format!("fractal-{}.png", unix_timestamp())); + let name = self + .export_path + .clone() + .unwrap_or_else(|| format!("fractal-{}.png", unix_timestamp())); std::thread::spawn(move || { let er = ExportRender::new( &device, diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..95568fe --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,84 @@ +// Native command-line arguments. Currently mirrors the old `MANDEL_*` debug +// env vars one-for-one; this is the foundation a future headless (no-window, +// render-to-file) mode will build on. + +use clap::{Parser, ValueEnum}; + +use crate::fractal::FractalKind; + +#[derive(Parser, Debug, Default)] +#[command(name = "mandelbrot", about = "Deep-zoom fractal explorer", version)] +pub struct Cli { + /// Fractal formula to render. + #[arg(long, value_enum)] + pub kind: Option, + + /// Exponent for the Multibrot kind (z -> z^power + c), clamped to [2, 8]. + #[arg(long)] + pub power: Option, + + /// Start in Julia mode with this seed constant. + #[arg(long, value_name = "RE,IM")] + pub julia: Option, + + /// Restore a view from a share-link fragment (the part after '#'). + #[arg(long, value_name = "FRAGMENT")] + pub share: Option, + + /// Jump to a view on startup. + #[arg(long, value_name = "RE,IM,HALF_HEIGHT[,ITERATIONS]")] + pub view: Option, + + /// Enable distance-estimation shading. + #[arg(long)] + pub de: bool, + + /// Switch to the Buddhabrot renderer. + #[arg(long)] + pub buddhabrot: bool, + + /// Buddhabrot tonemap palette index. + #[arg(long, value_name = "INDEX")] + pub buddha_palette: Option, + + /// Render a PNG export on startup. + #[arg(long)] + pub export: bool, + + /// Output path for --export (default: fractal-.png). + #[arg(long, value_name = "PATH")] + pub export_path: Option, +} + +#[derive(Copy, Clone, Debug, ValueEnum)] +pub enum KindArg { + Mandelbrot, + #[value(alias = "ship")] + BurningShip, + #[value(alias = "mandelbar")] + Tricorn, + #[value(alias = "multi")] + Multibrot, + Celtic, + #[value(alias = "perp")] + Perpendicular, + Buffalo, + Phoenix, + Lambda, +} + +impl From for FractalKind { + fn from(k: KindArg) -> Self { + match k { + KindArg::Mandelbrot => FractalKind::Mandelbrot, + KindArg::BurningShip => FractalKind::BurningShip, + KindArg::Tricorn => FractalKind::Tricorn, + KindArg::Multibrot => FractalKind::Multibrot, + KindArg::Celtic => FractalKind::Celtic, + KindArg::Perpendicular => FractalKind::Perpendicular, + KindArg::Buffalo => FractalKind::Buffalo, + KindArg::Phoenix => FractalKind::Phoenix, + KindArg::Lambda => FractalKind::Lambda, + } + } +} diff --git a/src/main.rs b/src/main.rs index a9268da..9734236 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,10 +9,12 @@ // and calls the wasm `main`, which boots eframe onto the page's . mod app; -mod lights; mod fractal; +mod lights; mod view; +#[cfg(not(target_arch = "wasm32"))] +mod cli; #[cfg(not(target_arch = "wasm32"))] mod worker;