feat: coordinates and magnification can be set to specific values

This commit is contained in:
2026-09-15 09:48:53 +02:00
parent 96c5d6344e
commit 9069d23b21
2 changed files with 106 additions and 12 deletions
+106 -7
View File
@@ -8,7 +8,8 @@ use crate::fractal::{FractalCallback, FractalRenderer, MAX_REF_POINTS, ShareStat
#[cfg(target_arch = "wasm32")]
use crate::fractal::{compute_mandelbrot_reference, compute_reference};
use crate::view::{
Big, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str, precision_for,
Big, DEFAULT_HALF_HEIGHT, ViewState, big_from_decimal_str, big_from_f64, big_to_decimal_str,
precision_for,
};
const BAILOUT_SQ: f32 = 1.0e6;
@@ -80,6 +81,36 @@ pub struct FractalApp {
export_requested: bool,
/// Short status line (saved path, "link copied", errors).
status: Option<String>,
/// Editable text buffers for the center coordinates (decimal, full
/// precision). Kept in sync with the live view except while the field is
/// focused, so the user's in-progress typing is not clobbered by pan/zoom.
center_re_edit: String,
center_im_edit: String,
/// Editable magnification (×). Its display is lossy, so `zoom_edited` guards
/// applying it: without that, clicking in and out would round-trip the value
/// through the display format and drift the zoom.
zoom_edit: String,
zoom_edited: bool,
}
/// Significant decimal digits to show for a center at the given precision (bits).
fn sig_digits_for(bits: usize) -> usize {
((bits as f64) * std::f64::consts::LOG10_2).ceil() as usize + 3
}
/// Format a magnification for the editable field (compact scientific).
fn format_magnification(m: f64) -> String {
format!("{m:.4e}")
}
/// Precision (bits) to parse a typed center at: at least what the current zoom
/// needs, but enough to preserve every digit the user pasted, so a deep
/// coordinate entered while zoomed out is not truncated. Capped like `view`.
fn parse_bits_for(s: &str, min_bits: usize) -> usize {
let digits = s.chars().filter(char::is_ascii_digit).count();
let from_input = (digits as f64 * std::f64::consts::LOG2_10).ceil() as usize + 16;
min_bits.max(from_input).min(2048)
}
impl FractalApp {
@@ -100,6 +131,10 @@ impl FractalApp {
let ref_center_re = view.center_re.clone();
let ref_center_im = view.center_im.clone();
let ref_half_height = view.half_height;
let sig = sig_digits_for(view.precision_bits());
let center_re_edit = big_to_decimal_str(&view.center_re, sig);
let center_im_edit = big_to_decimal_str(&view.center_im, sig);
let zoom_edit = format_magnification(view.magnification());
let mut app = Self {
view,
@@ -123,6 +158,10 @@ impl FractalApp {
last_size_px: egui::vec2(1280.0, 720.0),
export_requested: false,
status: None,
center_re_edit,
center_im_edit,
zoom_edit,
zoom_edited: false,
};
// On the web, restore a shared view from the URL fragment (#...).
@@ -195,8 +234,7 @@ impl FractalApp {
/// Snapshot the current view as a shareable state.
fn share_state(&self) -> ShareState {
let bits = self.view.precision_bits();
let sig_digits = ((bits as f64) * std::f64::consts::LOG10_2).ceil() as usize + 3;
let sig_digits = sig_digits_for(self.view.precision_bits());
ShareState {
julia: matches!(self.mode, FractalMode::Julia),
center_re: big_to_decimal_str(&self.view.center_re, sig_digits),
@@ -491,10 +529,71 @@ impl FractalApp {
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
ui.separator();
let (cre, cim) = self.view.center_f64();
ui.label(format!("center re:\n {cre:+.17}"));
ui.label(format!("center im:\n {cim:+.17}"));
ui.label(format!("magnification: {:.3e}×", self.view.magnification()));
// Editable center coordinates. Shown at full precision; parsed
// losslessly on commit (Enter or focus loss). While a field is focused
// we leave the user's text alone; otherwise we refresh it from the live
// view, which panning and zooming keep changing.
let bits = self.view.precision_bits();
let sig = sig_digits_for(bits);
ui.label("center re:");
let re_resp = ui.add(
egui::TextEdit::singleline(&mut self.center_re_edit)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace),
);
if re_resp.lost_focus()
&& let Some(v) =
big_from_decimal_str(&self.center_re_edit, parse_bits_for(&self.center_re_edit, bits))
{
self.view.center_re = v;
self.view.sync_precision();
}
if !re_resp.has_focus() {
self.center_re_edit = big_to_decimal_str(&self.view.center_re, sig);
}
ui.label("center im:");
let im_resp = ui.add(
egui::TextEdit::singleline(&mut self.center_im_edit)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace),
);
if im_resp.lost_focus()
&& let Some(v) =
big_from_decimal_str(&self.center_im_edit, parse_bits_for(&self.center_im_edit, bits))
{
self.view.center_im = v;
self.view.sync_precision();
}
if !im_resp.has_focus() {
self.center_im_edit = big_to_decimal_str(&self.view.center_im, sig);
}
ui.label("magnification (×):");
let zoom_resp = ui.add(
egui::TextEdit::singleline(&mut self.zoom_edit)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace),
);
if zoom_resp.changed() {
self.zoom_edited = true;
}
if zoom_resp.lost_focus() {
if self.zoom_edited
&& let Ok(m) = self.zoom_edit.trim().parse::<f64>()
{
let hh = DEFAULT_HALF_HEIGHT / m;
if m > 0.0 && hh > 0.0 && hh.is_finite() {
self.view.half_height = hh;
self.view.sync_precision();
}
}
self.zoom_edited = false;
}
if !zoom_resp.has_focus() {
self.zoom_edit = format_magnification(self.view.magnification());
}
ui.label(format!("reference: {} pts", self.reference.len()));
ui.label(format!("precision: {} bits", self.view.precision_bits()));
if self.pending {
-5
View File
@@ -101,11 +101,6 @@ impl ViewState {
self.half_height *= factor;
}
/// f64 approximation of the center, for display.
pub fn center_f64(&self) -> (f64, f64) {
(self.center_re.to_f64().value(), self.center_im.to_f64().value())
}
/// Build a view from full-precision center coordinates and a half-height.
pub fn with_center(center_re: Big, center_im: Big, half_height: f64) -> Self {
let mut v = Self {