#![expect(
clippy::cast_sign_loss,
reason = "level (i32) cast to u32 for pow — always non-negative"
)]
#![expect(
clippy::cast_possible_truncation,
reason = "level (i32->u32) for pow — always non-negative"
)]
use crate::r3::Matrix3x3;
use crate::s1::Angle;
use crate::s2::Loop;
use crate::s2::point::{Point, from_frame, get_frame};
#[derive(Debug)]
struct Rng {
state: u64,
}
impl Rng {
fn new(seed: u64) -> Self {
Rng {
state: if seed == 0 {
0x_dead_beef_cafe_babe
} else {
seed
},
}
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn bernoulli(&mut self, p: f64) -> bool {
self.next_f64() < p
}
}
#[derive(Clone, Copy)]
struct R2Point {
x: f64,
y: f64,
}
impl R2Point {
fn new(x: f64, y: f64) -> Self {
R2Point { x, y }
}
fn ortho(self) -> R2Point {
R2Point::new(-self.y, self.x)
}
}
impl std::ops::Add for R2Point {
type Output = R2Point;
fn add(self, rhs: R2Point) -> R2Point {
R2Point::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl std::ops::Sub for R2Point {
type Output = R2Point;
fn sub(self, rhs: R2Point) -> R2Point {
R2Point::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl std::ops::Mul<f64> for R2Point {
type Output = R2Point;
fn mul(self, s: f64) -> R2Point {
R2Point::new(self.x * s, self.y * s)
}
}
impl std::ops::Mul<R2Point> for f64 {
type Output = R2Point;
fn mul(self, p: R2Point) -> R2Point {
R2Point::new(self * p.x, self * p.y)
}
}
#[derive(Debug)]
pub struct S2Fractal {
rng: Rng,
max_level: i32,
min_level_arg: i32,
min_level: i32,
dimension: f64,
edge_fraction: f64,
offset_fraction: f64,
}
impl S2Fractal {
pub fn new(seed: u64) -> Self {
let dimension = (4.0_f64).ln() / (3.0_f64).ln(); let edge_fraction = 4.0_f64.powf(-1.0 / dimension);
let offset_fraction = (edge_fraction - 0.25).sqrt();
S2Fractal {
rng: Rng::new(seed),
max_level: -1,
min_level_arg: -1,
min_level: -1,
dimension,
edge_fraction,
offset_fraction,
}
}
pub fn set_max_level(&mut self, max_level: i32) {
assert!(max_level >= 0);
self.max_level = max_level;
self.compute_min_level();
}
pub fn max_level(&self) -> i32 {
self.max_level
}
pub fn set_min_level(&mut self, min_level_arg: i32) {
assert!(min_level_arg >= -1);
self.min_level_arg = min_level_arg;
self.compute_min_level();
}
pub fn min_level(&self) -> i32 {
self.min_level
}
pub fn level_for_approx_min_edges(&mut self, min_edges: i32) {
let level = (0.5 * (f64::from(min_edges) / 3.0).log2()).round() as i32;
self.set_min_level(level.max(0));
}
pub fn level_for_approx_max_edges(&mut self, max_edges: i32) {
let level = (0.5 * (f64::from(max_edges) / 3.0).log2()).round() as i32;
self.set_max_level(level.max(0));
}
pub fn set_fractal_dimension(&mut self, dimension: f64) {
assert!((1.0..2.0).contains(&dimension));
self.dimension = dimension;
self.compute_offsets();
}
pub fn fractal_dimension(&self) -> f64 {
self.dimension
}
pub fn min_radius_factor(&self) -> f64 {
const MIN_DIMENSION_FOR_MIN_RADIUS_AT_LEVEL1: f64 = 1.0852230903040407;
if self.dimension >= MIN_DIMENSION_FOR_MIN_RADIUS_AT_LEVEL1 {
(1.0 + 3.0 * self.edge_fraction * (self.edge_fraction - 1.0)).sqrt()
} else {
0.5
}
}
pub fn max_radius_factor(&self) -> f64 {
1.0_f64.max(self.offset_fraction * 3.0_f64.sqrt() + 0.5)
}
pub fn make_loop(&mut self, frame: &Matrix3x3, nominal_radius: Angle) -> Loop {
let r2_vertices = self.get_r2_vertices();
let r = nominal_radius.radians();
let mut vertices = Vec::with_capacity(r2_vertices.len());
for v in &r2_vertices {
let p = Point(crate::r3::Vector {
x: v.x * r,
y: v.y * r,
z: 1.0,
});
vertices.push(from_frame(frame, p).normalize());
}
Loop::new(vertices)
}
pub fn make_loop_at(&mut self, center: Point, nominal_radius: Angle) -> Loop {
let frame = get_frame(center);
self.make_loop(&frame, nominal_radius)
}
fn compute_min_level(&mut self) {
if self.min_level_arg >= 0 && self.min_level_arg <= self.max_level {
self.min_level = self.min_level_arg;
} else {
self.min_level = self.max_level;
}
}
fn compute_offsets(&mut self) {
self.edge_fraction = 4.0_f64.powf(-1.0 / self.dimension);
self.offset_fraction = (self.edge_fraction - 0.25).sqrt();
}
fn get_r2_vertices(&mut self) -> Vec<R2Point> {
let mut vertices = Vec::new();
let sqrt3_2 = 3.0_f64.sqrt() / 2.0;
let v0 = R2Point::new(1.0, 0.0);
let v1 = R2Point::new(-0.5, sqrt3_2);
let v2 = R2Point::new(-0.5, -sqrt3_2);
self.get_r2_vertices_helper(v0, v1, 0, &mut vertices);
self.get_r2_vertices_helper(v1, v2, 0, &mut vertices);
self.get_r2_vertices_helper(v2, v0, 0, &mut vertices);
vertices
}
fn get_r2_vertices_helper(
&mut self,
v0: R2Point,
v4: R2Point,
level: i32,
vertices: &mut Vec<R2Point>,
) {
if level >= self.min_level {
let levels_remaining = self.max_level - level + 1;
if self.rng.bernoulli(1.0 / f64::from(levels_remaining)) {
vertices.push(v0);
return;
}
}
let dir = v4 - v0;
let v1 = v0 + dir * self.edge_fraction;
let v2 = 0.5 * (v0 + v4) - dir.ortho() * self.offset_fraction;
let v3 = v4 - dir * self.edge_fraction;
self.get_r2_vertices_helper(v0, v1, level + 1, vertices);
self.get_r2_vertices_helper(v1, v2, level + 1, vertices);
self.get_r2_vertices_helper(v2, v3, level + 1, vertices);
self.get_r2_vertices_helper(v3, v4, level + 1, vertices);
}
}
pub fn num_vertices_at_level(level: i32) -> usize {
3 * (1usize << (2 * level as u32))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::s1::Angle;
use crate::s2::point::get_frame;
#[test]
fn test_num_vertices_at_level() {
assert_eq!(num_vertices_at_level(0), 3);
assert_eq!(num_vertices_at_level(1), 12);
assert_eq!(num_vertices_at_level(2), 48);
assert_eq!(num_vertices_at_level(3), 192);
assert_eq!(num_vertices_at_level(4), 768);
}
#[test]
fn test_default_dimension() {
let f = S2Fractal::new(1);
let expected = (4.0_f64).ln() / (3.0_f64).ln();
assert!((f.fractal_dimension() - expected).abs() < 1e-14);
}
fn test_fractal(min_level: i32, max_level: i32, dimension: f64) {
let mut fractal = S2Fractal::new(42);
fractal.set_fractal_dimension(dimension);
fractal.set_max_level(max_level);
if min_level >= 0 {
fractal.set_min_level(min_level);
}
let min_rf = fractal.min_radius_factor();
let max_rf = fractal.max_radius_factor();
let frame = get_frame(Point::from_coords(0.0, 0.0, 1.0));
let radius = Angle::from_degrees(10.0);
let lp = fractal.make_loop(&frame, radius);
let nv = lp.num_vertices();
let effective_min = if min_level >= 0 { min_level } else { max_level };
let min_verts = num_vertices_at_level(effective_min);
let max_verts = num_vertices_at_level(max_level);
assert!(
nv >= min_verts && nv <= max_verts,
"vertex count {nv} not in range [{min_verts}, {max_verts}] \
for levels [{effective_min}, {max_level}], dim={dimension}",
);
assert!(min_rf > 0.0, "min_radius_factor = {min_rf}");
assert!(max_rf >= min_rf, "max_rf {max_rf} < min_rf {min_rf}");
assert!(lp.validate().is_ok(), "loop validation failed");
}
#[test]
fn test_triangle_fractal() {
test_fractal(-1, 5, 1.0);
}
#[test]
fn test_triangle_multi_fractal() {
test_fractal(2, 6, 1.0);
}
#[test]
fn test_koch_curve_fractal() {
let dim = (4.0_f64).ln() / (3.0_f64).ln();
test_fractal(-1, 5, dim);
}
#[test]
fn test_koch_curve_multi_fractal() {
let dim = (4.0_f64).ln() / (3.0_f64).ln();
test_fractal(3, 6, dim);
}
#[test]
fn test_space_filling_fractal() {
test_fractal(-1, 3, 1.999);
}
#[test]
fn test_cesaro_fractal() {
test_fractal(-1, 5, 1.8);
}
#[test]
fn test_cesaro_multi_fractal() {
test_fractal(2, 5, 1.8);
}
#[test]
fn test_make_loop_at() {
let mut fractal = S2Fractal::new(123);
fractal.set_max_level(3);
let center = Point::from_coords(1.0, 0.0, 0.0);
let lp = fractal.make_loop_at(center, Angle::from_degrees(5.0));
assert!(lp.num_vertices() >= 3);
assert!(lp.validate().is_ok());
}
#[test]
fn test_level_for_approx_edges() {
let mut fractal = S2Fractal::new(1);
fractal.level_for_approx_max_edges(100);
assert!(fractal.max_level() >= 2 && fractal.max_level() <= 4);
fractal.level_for_approx_min_edges(50);
assert!(fractal.min_level() >= 1 && fractal.min_level() <= 3);
}
#[test]
fn test_radius_factors() {
let dim = (4.0_f64).ln() / (3.0_f64).ln();
let mut fractal = S2Fractal::new(1);
fractal.set_fractal_dimension(dim);
fractal.set_max_level(5);
let min_rf = fractal.min_radius_factor();
let max_rf = fractal.max_radius_factor();
assert!(
min_rf > 0.5 && min_rf < 1.0,
"min_rf = {min_rf}, expected ~0.866"
);
assert!(
(1.0..1.5).contains(&max_rf),
"max_rf = {max_rf}, expected ~1.115"
);
}
#[test]
fn test_deterministic_with_same_seed() {
let mut f1 = S2Fractal::new(42);
f1.set_max_level(3);
let frame = get_frame(Point::from_coords(0.0, 0.0, 1.0));
let l1 = f1.make_loop(&frame, Angle::from_degrees(5.0));
let mut f2 = S2Fractal::new(42);
f2.set_max_level(3);
let l2 = f2.make_loop(&frame, Angle::from_degrees(5.0));
assert_eq!(l1.num_vertices(), l2.num_vertices());
for i in 0..l1.num_vertices() {
assert_eq!(l1.vertex(i), l2.vertex(i));
}
}
#[test]
fn test_different_seeds_differ() {
let frame = get_frame(Point::from_coords(0.0, 0.0, 1.0));
let mut f1 = S2Fractal::new(1);
f1.set_max_level(4);
f1.set_min_level(2);
let l1 = f1.make_loop(&frame, Angle::from_degrees(5.0));
let mut f2 = S2Fractal::new(999);
f2.set_max_level(4);
f2.set_min_level(2);
let l2 = f2.make_loop(&frame, Angle::from_degrees(5.0));
let same = l1.num_vertices() == l2.num_vertices()
&& (0..l1.num_vertices()).all(|i| l1.vertex(i) == l2.vertex(i));
assert!(!same, "different seeds should produce different loops");
}
}