feat: coordinates and magnification can be set to specific values
This commit is contained in:
+106
-7
@@ -8,7 +8,8 @@ use crate::fractal::{FractalCallback, FractalRenderer, MAX_REF_POINTS, ShareStat
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::fractal::{compute_mandelbrot_reference, compute_reference};
|
use crate::fractal::{compute_mandelbrot_reference, compute_reference};
|
||||||
use crate::view::{
|
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;
|
const BAILOUT_SQ: f32 = 1.0e6;
|
||||||
@@ -80,6 +81,36 @@ pub struct FractalApp {
|
|||||||
export_requested: bool,
|
export_requested: bool,
|
||||||
/// Short status line (saved path, "link copied", errors).
|
/// Short status line (saved path, "link copied", errors).
|
||||||
status: Option<String>,
|
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 {
|
impl FractalApp {
|
||||||
@@ -100,6 +131,10 @@ impl FractalApp {
|
|||||||
let ref_center_re = view.center_re.clone();
|
let ref_center_re = view.center_re.clone();
|
||||||
let ref_center_im = view.center_im.clone();
|
let ref_center_im = view.center_im.clone();
|
||||||
let ref_half_height = view.half_height;
|
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 {
|
let mut app = Self {
|
||||||
view,
|
view,
|
||||||
@@ -123,6 +158,10 @@ impl FractalApp {
|
|||||||
last_size_px: egui::vec2(1280.0, 720.0),
|
last_size_px: egui::vec2(1280.0, 720.0),
|
||||||
export_requested: false,
|
export_requested: false,
|
||||||
status: None,
|
status: None,
|
||||||
|
center_re_edit,
|
||||||
|
center_im_edit,
|
||||||
|
zoom_edit,
|
||||||
|
zoom_edited: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// On the web, restore a shared view from the URL fragment (#...).
|
// 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.
|
/// Snapshot the current view as a shareable state.
|
||||||
fn share_state(&self) -> ShareState {
|
fn share_state(&self) -> ShareState {
|
||||||
let bits = self.view.precision_bits();
|
let sig_digits = sig_digits_for(self.view.precision_bits());
|
||||||
let sig_digits = ((bits as f64) * std::f64::consts::LOG10_2).ceil() as usize + 3;
|
|
||||||
ShareState {
|
ShareState {
|
||||||
julia: matches!(self.mode, FractalMode::Julia),
|
julia: matches!(self.mode, FractalMode::Julia),
|
||||||
center_re: big_to_decimal_str(&self.view.center_re, sig_digits),
|
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).");
|
.on_hover_text("Supersample each pixel for smoother edges (~4× slower).");
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
let (cre, cim) = self.view.center_f64();
|
// Editable center coordinates. Shown at full precision; parsed
|
||||||
ui.label(format!("center re:\n {cre:+.17}"));
|
// losslessly on commit (Enter or focus loss). While a field is focused
|
||||||
ui.label(format!("center im:\n {cim:+.17}"));
|
// we leave the user's text alone; otherwise we refresh it from the live
|
||||||
ui.label(format!("magnification: {:.3e}×", self.view.magnification()));
|
// 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!("reference: {} pts", self.reference.len()));
|
||||||
ui.label(format!("precision: {} bits", self.view.precision_bits()));
|
ui.label(format!("precision: {} bits", self.view.precision_bits()));
|
||||||
if self.pending {
|
if self.pending {
|
||||||
|
|||||||
@@ -101,11 +101,6 @@ impl ViewState {
|
|||||||
self.half_height *= factor;
|
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.
|
/// 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 {
|
pub fn with_center(center_re: Big, center_im: Big, half_height: f64) -> Self {
|
||||||
let mut v = Self {
|
let mut v = Self {
|
||||||
|
|||||||
Reference in New Issue
Block a user