From 90d917ede0806befe12056a3da48905583facf21 Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Sun, 20 Sep 2026 13:20:50 +0200 Subject: [PATCH] refactor: cleanup code --- CLAUDE.md | 50 ++++++------ src/app.rs | 70 ++--------------- src/fractal/kind.rs | 165 +++++++++++++++++++++++++++++++++++++++ src/fractal/mod.rs | 4 +- src/fractal/reference.rs | 50 +----------- src/fractal/share.rs | 30 +------ 6 files changed, 205 insertions(+), 164 deletions(-) create mode 100644 src/fractal/kind.rs diff --git a/CLAUDE.md b/CLAUDE.md index dd0ff73..b68a294 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,11 +61,15 @@ pixel is a handful of `f32` complex multiplies. - `src/view.rs` — `ViewState`; center is arbitrary-precision `FBig` (`Big` type alias), pixel scale stays `f64` (still in-range at 10³⁰×). Precision (bits) scales with zoom depth (`precision_for`). -- `src/fractal/reference.rs` — `FractalKind` enum (Mandelbrot, Burning Ship, - Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda) and - `compute_reference`/`compute_set_reference`: iterate the chosen formula at - high precision on the CPU, emitting `Z_n` as `f32` pairs — that's the - reference orbit the GPU perturbs from. +- `src/fractal/kind.rs` — the `FractalKind` enum (Mandelbrot, Burning Ship, + Tricorn, Multibrot, Celtic, Perpendicular, Buffalo, Phoenix, Lambda, + Complex Multibrot) plus everything that only needs to switch on it: + `label`/`description`/`formula` (UI text), `share_tag`/`from_share_tag` + (share-link encoding), `default_set_view` (per-kind starting view), and the + `ALL` array used to enumerate every kind. +- `src/fractal/reference.rs` — `compute_reference`/`compute_set_reference`: + iterate the chosen formula at high precision on the CPU, emitting `Z_n` as + `f32` pairs — that's the reference orbit the GPU perturbs from. - `src/shaders/*.wgsl` — none of these are standalone WGSL modules; WGSL has no `#include`, so each is compiled by concatenating plain-text fragments with `concat!`/`include_str!` at the `create_shader_module` call site (see @@ -106,29 +110,31 @@ pixel is a handful of `f32` complex multiplies. `should_request`/`ensure_reference` (decide when the reference is stale and dispatch/collect it), `make_uniforms` (assemble the per-frame `Uniforms`), `tick_animations` (drives the "morph c/p/λ" and auto-zoom animations), - `default_view_for` (per-kind starting view). `KINDS`, `JULIA_PRESETS`, and - `SET_PRESETS` are sized as `[T; FractalKind:: as usize + 1]` — - adding a new `FractalKind` means bumping all three (and adding an empty - `&[]` slot to the two preset arrays if the kind has none). + `default_view_for` (wraps `FractalKind::default_set_view`, adding the + kind-independent Julia case). `JULIA_PRESETS` and `SET_PRESETS` are sized as + `[T; FractalKind:: as usize + 1]` — adding a new `FractalKind` + means bumping both (and adding an empty `&[]` slot to each if the kind has + none), plus adding it to `FractalKind::ALL` in `kind.rs`. - `src/fractal/share.rs` — `ShareState`: encodes the full view (mode, kind, full-precision decimal center, zoom, iterations, per-kind constants, coloring) as a `#`-fragment URL for bookmarking/sharing deep-zoom locations. ### Adding a new `FractalKind` -Touches, in order: `reference.rs` (enum variant + CPU iteration formula, and a -test comparing against a naive `f64` iteration), `common.wgsl` (matching -`KIND_*` const), `mandelbrot.wgsl` (matching `advance_delta`/`fprime` arms), -`buddhabrot.wgsl` (matching arm in `advance()`, if the kind makes sense as a -Buddhabrot), `renderer.rs` -`Uniforms` (only if the kind needs a new per-kind constant, e.g. Phoenix's -`phoenix_p`), `share.rs` (encode/decode string tag), `app.rs` (`KINDS` label, -`JULIA_PRESETS`/`SET_PRESETS` slot, `default_view_for` entry, and optionally a -UI control for its constant + an animation toggle, following the -Phoenix/Lambda pattern). If `c` doesn't enter the formula additively (e.g. a -rational map with `c` in a denominator), the `advance_delta`/`step_add` split -doesn't work — that needs its own step function plus extra per-step reference -data uploaded in a second GPU buffer alongside the orbit. +Touches, in order: `kind.rs` (enum variant + `ALL` slot + `label`/ +`description`/`formula`/`share_tag`/`from_share_tag`/`default_set_view` +arms), `reference.rs` (CPU iteration formula arm, and a test comparing +against a naive `f64` iteration), `common.wgsl` (matching `KIND_*` const), +`mandelbrot.wgsl` (matching `advance_delta`/`fprime` arms), `buddhabrot.wgsl` +(matching arm in `advance()`, if the kind makes sense as a Buddhabrot), +`renderer.rs` `Uniforms` (only if the kind needs a new per-kind constant, +e.g. Phoenix's `phoenix_p`), `app.rs` (`JULIA_PRESETS`/`SET_PRESETS` slot, +and optionally a UI control for its constant + an animation toggle, +following the Phoenix/Lambda pattern). If `c` doesn't enter the formula +additively (e.g. a rational map with `c` in a denominator), the +`advance_delta`/`step_add` split doesn't work — that needs its own step +function plus extra per-step reference data uploaded in a second GPU buffer +alongside the orbit. ### Buddhabrot is a separate pipeline diff --git a/src/app.rs b/src/app.rs index b4636f9..6048d42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,49 +46,6 @@ pub enum FractalMode { Buddhabrot, } -/// Selectable fractal formulas, with UI labels. -const KINDS: &[(FractalKind, &str)] = &[ - (FractalKind::Mandelbrot, "Mandelbrot"), - (FractalKind::BurningShip, "Burning Ship"), - (FractalKind::Tricorn, "Tricorn"), - (FractalKind::Multibrot, "Multibrot"), - (FractalKind::Celtic, "Celtic"), - (FractalKind::Perpendicular, "Perpendicular"), - (FractalKind::Buffalo, "Buffalo"), - (FractalKind::Phoenix, "Phoenix"), - (FractalKind::Lambda, "Lambda"), - (FractalKind::ComplexMultibrot, "Complex Multibrot"), -]; - -/// UI label for a fractal kind. -fn kind_label(kind: FractalKind) -> &'static str { - KINDS - .iter() - .find(|(k, _)| *k == kind) - .map(|(_, name)| *name) - .unwrap_or("Mandelbrot") -} - -/// The iteration formula for a kind, in human-readable notation (mirrors the -/// doc comments on `FractalKind`'s variants). `power` is only used by -/// Multibrot; `complex_power` only by Complex Multibrot. -fn kind_formula(kind: FractalKind, power: u32, complex_power: (f64, f64)) -> String { - match kind { - FractalKind::Mandelbrot => "z = z² + c".to_string(), - FractalKind::BurningShip => "z = (|Re(z)| + i|Im(z)|)² + c".to_string(), - FractalKind::Tricorn => "z = conj(z)² + c".to_string(), - FractalKind::Multibrot => format!("z = z^{power} + c"), - FractalKind::Celtic => "z = |Re(z²)| + i·Im(z²) + c".to_string(), - FractalKind::Perpendicular => "z = (x² − y²) − 2x|y|i + c".to_string(), - FractalKind::Buffalo => "z = |Re(z²)| − i|Im(z²)| + c".to_string(), - FractalKind::Phoenix => "z = z² + c + p·z_prev".to_string(), - FractalKind::Lambda => "z = λ·z(1 − z)".to_string(), - FractalKind::ComplexMultibrot => { - format!("z = z^({:.3}{:+.3}i) + c", complex_power.0, complex_power.1) - } - } -} - type JuliaPreset = (&'static str, f64, f64, u32, Option<(f64, f64)>); /// Nice-looking Julia constants offered as presets. @@ -697,18 +654,7 @@ impl FractalApp { if mode == FractalMode::Julia { return ViewState::with_center(big_from_f64(0.0, 53), big_from_f64(0.0, 53), 1.5); } - let (cr, ci, hh) = match kind { - FractalKind::Mandelbrot => (-0.5, 0.0, 1.25), - FractalKind::BurningShip => (-0.5, -0.5, 1.3), - FractalKind::Tricorn => (-0.25, 0.0, 1.6), - FractalKind::Multibrot => (0.0, 0.0, 1.5), - FractalKind::Celtic => (-0.5, 0.0, 1.6), - FractalKind::Perpendicular => (-0.5, 0.0, 1.5), - FractalKind::Buffalo => (-0.5, -0.5, 1.5), - FractalKind::Phoenix => (0.0, 0.0, 1.6), - FractalKind::Lambda => (0.0, 0.0, 1.6), - FractalKind::ComplexMultibrot => (0.0, 0.0, 1.5), - }; + let (cr, ci, hh) = kind.default_set_view(); ViewState::with_center(big_from_f64(cr, 53), big_from_f64(ci, 53), hh) } @@ -1249,11 +1195,7 @@ impl FractalApp { .resizable(false) .anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(8.0, -44.0)) .show(ctx, |ui| { - ui.label( - egui::RichText::new(kind_label(self.kind)) - .strong() - .heading(), - ); + ui.label(egui::RichText::new(self.kind.label()).strong().heading()); let mode_label = match self.mode { FractalMode::Mandelbrot => { "Mandelbrot mode — parameter space (c varies per pixel, z₀ = 0)" @@ -1268,7 +1210,7 @@ impl FractalApp { ui.label(format!( "formula: {}", - kind_formula(self.kind, self.power, self.complex_power) + self.kind.formula(self.power, self.complex_power) )); if self.mode == FractalMode::Julia { ui.label(format!("c = {:.6} {:+.6}i", self.julia_c.0, self.julia_c.1)); @@ -1517,10 +1459,10 @@ impl FractalApp { // since interesting regions differ between fractals. let prev_kind = self.kind; egui::ComboBox::from_label("fractal") - .selected_text(kind_label(self.kind)) + .selected_text(self.kind.label()) .show_ui(ui, |ui| { - for &(kind, name) in KINDS { - ui.selectable_value(&mut self.kind, kind, name); + for kind in FractalKind::ALL { + ui.selectable_value(&mut self.kind, kind, kind.label()); } }); if self.kind == FractalKind::Multibrot { diff --git a/src/fractal/kind.rs b/src/fractal/kind.rs new file mode 100644 index 0000000..8ff29b9 --- /dev/null +++ b/src/fractal/kind.rs @@ -0,0 +1,165 @@ +//! `FractalKind`: the enum selecting which iteration formula is in use, plus +//! everything that only needs to switch on it (UI label/description/formula +//! text, share-link tag, default parameter-plane view). The CPU/GPU orbit +//! math itself lives in `reference.rs` (CPU reference orbit) and +//! `shaders/mandelbrot.wgsl` (GPU perturbation delta) since both must also +//! stay in sync with `common.wgsl`'s `KIND_*` constants. + +/// The iteration formula. Must be kept in sync with `advance_delta` and the +/// `KIND_*` constants in the shader. +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum FractalKind { + /// `z -> z^2 + c`. + Mandelbrot = 0, + /// `z -> (|Re z| + i|Im z|)^2 + c`. + BurningShip = 1, + /// `z -> conj(z)^2 + c` (the Mandelbar). + Tricorn = 2, + /// `z -> z^power + c` (power >= 2). + Multibrot = 3, + /// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square). + Celtic = 4, + /// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input). + Perpendicular = 5, + /// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs). + Buffalo = 6, + /// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`). + Phoenix = 7, + /// `z -> lambda·z(1 - z)` (logistic map). + Lambda = 8, + /// `z -> z^power + c`, where `power` is a complex constant (the + /// `complex_power` argument), via the principal branch `z^p = exp(p·ln z)`. + ComplexMultibrot = 9, +} + +impl FractalKind { + /// Every kind, in declaration/discriminant order. Sized arrays keyed by + /// `kind as usize` (`JULIA_PRESETS`, `SET_PRESETS`) must have one slot per + /// entry here. + pub const ALL: [FractalKind; 10] = [ + FractalKind::Mandelbrot, + FractalKind::BurningShip, + FractalKind::Tricorn, + FractalKind::Multibrot, + FractalKind::Celtic, + FractalKind::Perpendicular, + FractalKind::Buffalo, + FractalKind::Phoenix, + FractalKind::Lambda, + FractalKind::ComplexMultibrot, + ]; + + pub fn description(&self) -> &'static str { + match self { + FractalKind::Mandelbrot => { + "The Mandelbrot set is the most famous fractal set, obtained with the simplest escape-time formula. This set represents all Julia fractals: each points of the Mandelbrot set is related to a specific Julia fractal." + } + FractalKind::BurningShip => { + "A variation of the famous Mandelbrot set, using absolute values on the real and imaginary part of each iterations." + } + FractalKind::Tricorn => { + "The Tricorn set is obtained using the same formula as the Mandelbrot set, taking the complex conjugate of the previous iteration." + } + FractalKind::Multibrot => { + "Multibrot use the same formula as the Mandelbrot set, with a bigger exposant." + } + FractalKind::Celtic => "", + FractalKind::Perpendicular => "", + FractalKind::Buffalo => "", + FractalKind::Phoenix => "", + FractalKind::Lambda => "", + FractalKind::ComplexMultibrot => { + "Like Multibrot, but the exponent itself is a complex number instead of a plain integer, via z^p = exp(p·ln z)." + } + } + } + + /// UI label for this kind (combo box / info panel heading). + pub fn label(&self) -> &'static str { + match self { + FractalKind::Mandelbrot => "Mandelbrot", + FractalKind::BurningShip => "Burning Ship", + FractalKind::Tricorn => "Tricorn", + FractalKind::Multibrot => "Multibrot", + FractalKind::Celtic => "Celtic", + FractalKind::Perpendicular => "Perpendicular", + FractalKind::Buffalo => "Buffalo", + FractalKind::Phoenix => "Phoenix", + FractalKind::Lambda => "Lambda", + FractalKind::ComplexMultibrot => "Complex Multibrot", + } + } + + /// The iteration formula in human-readable notation (mirrors the doc + /// comments on the variants above). `power` is only used by Multibrot; + /// `complex_power` only by Complex Multibrot. + pub fn formula(&self, power: u32, complex_power: (f64, f64)) -> String { + match self { + FractalKind::Mandelbrot => "z = z² + c".to_string(), + FractalKind::BurningShip => "z = (|Re(z)| + i|Im(z)|)² + c".to_string(), + FractalKind::Tricorn => "z = conj(z)² + c".to_string(), + FractalKind::Multibrot => format!("z = z^{power} + c"), + FractalKind::Celtic => "z = |Re(z²)| + i·Im(z²) + c".to_string(), + FractalKind::Perpendicular => "z = (x² − y²) − 2x|y|i + c".to_string(), + FractalKind::Buffalo => "z = |Re(z²)| − i|Im(z²)| + c".to_string(), + FractalKind::Phoenix => "z = z² + c + p·z_prev".to_string(), + FractalKind::Lambda => "z = λ·z(1 − z)".to_string(), + FractalKind::ComplexMultibrot => { + format!("z = z^({:.3}{:+.3}i) + c", complex_power.0, complex_power.1) + } + } + } + + /// Short tag used to identify this kind in a share-link fragment. + pub fn share_tag(&self) -> &'static str { + match self { + FractalKind::Mandelbrot => "mandel", + FractalKind::BurningShip => "burning", + FractalKind::Tricorn => "tricorn", + FractalKind::Multibrot => "multi", + FractalKind::Celtic => "celtic", + FractalKind::Perpendicular => "perp", + FractalKind::Buffalo => "buffalo", + FractalKind::Phoenix => "phoenix", + FractalKind::Lambda => "lambda", + FractalKind::ComplexMultibrot => "cmulti", + } + } + + /// Inverse of `share_tag`; unknown tags fall back to `None` so the caller + /// can decide the default (matches historical share-link behavior). + pub fn from_share_tag(tag: &str) -> Option { + Some(match tag { + "mandel" => FractalKind::Mandelbrot, + "burning" => FractalKind::BurningShip, + "tricorn" => FractalKind::Tricorn, + "multi" => FractalKind::Multibrot, + "celtic" => FractalKind::Celtic, + "perp" => FractalKind::Perpendicular, + "buffalo" => FractalKind::Buffalo, + "phoenix" => FractalKind::Phoenix, + "lambda" => FractalKind::Lambda, + "cmulti" => FractalKind::ComplexMultibrot, + _ => return None, + }) + } + + /// Default parameter-plane (Mandelbrot-mode) view for this kind, as + /// `(center_re, center_im, half_height)`. The Julia (dynamical) plane + /// doesn't vary by kind, so it isn't covered here. + pub fn default_set_view(&self) -> (f64, f64, f64) { + match self { + FractalKind::Mandelbrot => (-0.5, 0.0, 1.25), + FractalKind::BurningShip => (-0.5, -0.5, 1.3), + FractalKind::Tricorn => (-0.25, 0.0, 1.6), + FractalKind::Multibrot => (0.0, 0.0, 1.5), + FractalKind::Celtic => (-0.5, 0.0, 1.6), + FractalKind::Perpendicular => (-0.5, 0.0, 1.5), + FractalKind::Buffalo => (-0.5, -0.5, 1.5), + FractalKind::Phoenix => (0.0, 0.0, 1.6), + FractalKind::Lambda => (0.0, 0.0, 1.6), + FractalKind::ComplexMultibrot => (0.0, 0.0, 1.5), + } + } +} diff --git a/src/fractal/mod.rs b/src/fractal/mod.rs index b620602..23ca04d 100644 --- a/src/fractal/mod.rs +++ b/src/fractal/mod.rs @@ -2,12 +2,14 @@ //! egui paint callback. pub mod buddhabrot; +pub mod kind; pub mod reference; pub mod renderer; pub mod share; pub use buddhabrot::{BuddhabrotCallback, BuddhabrotRenderer, BuddhabrotUniforms}; -pub use reference::{FractalKind, compute_reference, compute_set_reference}; +pub use kind::FractalKind; +pub use reference::{compute_reference, compute_set_reference}; #[cfg(target_arch = "wasm32")] pub use renderer::encode_png_with_progress; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/fractal/reference.rs b/src/fractal/reference.rs index 8a1189a..4af7e74 100644 --- a/src/fractal/reference.rs +++ b/src/fractal/reference.rs @@ -10,57 +10,9 @@ //! * Mandelbrot-set: `z0 = 0`, `c = view center` (the c-plane point per pixel). //! * Julia-set: `z0 = view center`, `c = fractal constant` (fixed per view). +use super::kind::FractalKind; use crate::view::{Big, big_from_f64}; -/// The iteration formula. Must be kept in sync with `advance_delta` and the -/// `KIND_*` constants in the shader. -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum FractalKind { - /// `z -> z^2 + c`. - Mandelbrot = 0, - /// `z -> (|Re z| + i|Im z|)^2 + c`. - BurningShip = 1, - /// `z -> conj(z)^2 + c` (the Mandelbar). - Tricorn = 2, - /// `z -> z^power + c` (power >= 2). - Multibrot = 3, - /// `z -> |Re(z^2)| + i·Im(z^2) + c` (abs on the real output of the square). - Celtic = 4, - /// `z -> (x^2 - y^2) - 2·x·|y|·i + c` (abs on the imaginary input). - Perpendicular = 5, - /// `z -> |Re(z^2)| - |Im(z^2)|·i + c` (abs on both outputs). - Buffalo = 6, - /// `z -> z^2 + c + p·z_{n-1}` (two-term recurrence; `p` is `phoenix_p`). - Phoenix = 7, - /// `z -> lambda·z(1 - z)` (logistic map). - Lambda = 8, - /// `z -> z^power + c`, where `power` is a complex constant (the - /// `complex_power` argument), via the principal branch `z^p = exp(p·ln z)`. - ComplexMultibrot = 9, -} - -impl FractalKind { - pub fn description(&self) -> &str { - match self { - FractalKind::Mandelbrot => { - "The Mandelbrot set is the most famous fractal set, obtained with the simplest escape-time formula. This set represents all Julia fractals: each points of the Mandelbrot set is related to a specific Julia fractal." - } - FractalKind::BurningShip => { - "A variation of the famous Mandelbrot set, using absolute values on the real and imaginary part of each iterations." - } - FractalKind::Tricorn => "The Tricorn set is obtained using the same formula as the Mandelbrot set, taking the complex conjugate of the previous iteration.", - FractalKind::Multibrot => "Multibrot use the same formula as the Mandelbrot set, with a bigger exposant.", - FractalKind::Celtic => "", - FractalKind::Perpendicular => "", - FractalKind::Buffalo => "", - FractalKind::Phoenix => "", - FractalKind::Lambda => "", - FractalKind::ComplexMultibrot => "Like Multibrot, but the exponent itself is a complex number instead of a plain integer, via z^p = exp(p·ln z).", - } - } -} - /// Reference orbit escapes once |Z|^2 exceeds this. Kept larger than the pixel /// bailout so pixels escaping alongside the reference can still reach their /// bailout before the stored orbit runs out. diff --git a/src/fractal/share.rs b/src/fractal/share.rs index 49a17b1..c70574f 100644 --- a/src/fractal/share.rs +++ b/src/fractal/share.rs @@ -39,21 +39,7 @@ impl ShareState { pub fn encode(&self) -> String { let mut s = String::new(); s.push_str(if self.julia { "m=j" } else { "m=m" }); - s.push_str(&format!( - "&f={}", - match self.kind { - FractalKind::Mandelbrot => "mandel", - FractalKind::BurningShip => "burning", - FractalKind::Multibrot => "multi", - FractalKind::Tricorn => "tricorn", - FractalKind::Celtic => "celtic", - FractalKind::Perpendicular => "perp", - FractalKind::Buffalo => "buffalo", - FractalKind::Phoenix => "phoenix", - FractalKind::Lambda => "lambda", - FractalKind::ComplexMultibrot => "cmulti", - } - )); + s.push_str(&format!("&f={}", self.kind.share_tag())); s.push_str(&format!("&pw={}", self.power)); s.push_str(&format!( "&re={}&im={}&hh={}&it={}", @@ -86,19 +72,7 @@ impl ShareState { julia: map.get("m").map(|m| *m == "j").unwrap_or(false), kind: map .get("f") - .map(|f| match *f { - "mandel" => FractalKind::Mandelbrot, - "multi" => FractalKind::Multibrot, - "burning" => FractalKind::BurningShip, - "tricorn" => FractalKind::Tricorn, - "celtic" => FractalKind::Celtic, - "perp" => FractalKind::Perpendicular, - "buffalo" => FractalKind::Buffalo, - "phoenix" => FractalKind::Phoenix, - "lambda" => FractalKind::Lambda, - "cmulti" => FractalKind::ComplexMultibrot, - _ => FractalKind::Mandelbrot, - }) + .and_then(|f| FractalKind::from_share_tag(f)) .unwrap_or(FractalKind::Mandelbrot), power: map.get("pw").and_then(|s| s.parse().ok()).unwrap_or(2), center_re: (*map.get("re")?).to_string(),