alice-sdf 5.0.0

ALICE-SDF: 3D/Spatial Data Specialist — Store laws, not polygons. GLSL/WGSL/HLSL transpile, SVO, GPU marching cubes, texture-fit, procedural CSG. Optional bridges to alice-physics / alice-codec / libasp / alice-cache behind features.
Documentation
//! Polar repetition modifier — repeat around Y-axis
//!
//! Ported from Baker's `opPolarRepeat(p, count)`.
//! Repeats the child SDF `count` times evenly around the Y-axis.
//!
//! Author: Moroya Sakamoto

use glam::Vec3;

/// Repeat point around Y-axis by `count` copies.
///
/// Returns the transformed point (pass to child SDF).
/// Uses atan2 + the round trick to fold the angle into one sector
/// (delegates to [`modifier_polar_repeat_rk`]).
#[inline(always)]
pub fn modifier_polar_repeat(p: Vec3, count: u32) -> Vec3 {
    // Same `sector` / `recip_sector` the compiler bakes into the PolarRepeat
    // instruction (`Instruction::polar_repeat`), so the tree evaluator and
    // the compiled / SIMD / JIT paths evaluate one law with identical
    // operands — the old `%`-based fold here picked a different sector at
    // exact sector boundaries.
    let count_f = count as f32;
    let sector = std::f32::consts::TAU / count_f;
    let recip_sector = count_f / std::f32::consts::TAU;
    modifier_polar_repeat_rk(p, sector, recip_sector)
}

/// Polar repeat — Division Exorcism edition.
///
/// Takes precomputed `sector = TAU / count` and `recip_sector = count / TAU`
/// to eliminate division and replace `%` with the round-trick:
/// `a - sector * round(a * recip_sector)` instead of `a % sector`.
#[inline(always)]
pub fn modifier_polar_repeat_rk(p: Vec3, sector: f32, recip_sector: f32) -> Vec3 {
    // One law: the compiled / SIMD generic `real::polar_repeat`, instantiated
    // for f32. A separate scalar copy (hypot + mul_add) rounded differently by
    // an ulp, and four nested polar repeats amplified that across a sector
    // boundary (found by `fuzz_eval_parity`).
    crate::compiled::real::polar_repeat::<f32>(p.into(), sector, recip_sector).into()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_polar_repeat_on_axis() {
        // On Y-axis, r=0, result should be on Y-axis
        let p = Vec3::new(0.0, 5.0, 0.0);
        let q = modifier_polar_repeat(p, 6);
        assert!(q.x.abs() < 0.001, "x should be ~0: {}", q.x);
        assert!((q.y - 5.0).abs() < 0.001);
    }

    #[test]
    fn test_polar_repeat_preserves_radius() {
        // Radius should be preserved
        let p = Vec3::new(3.0, 0.0, 4.0);
        let q = modifier_polar_repeat(p, 8);
        let r_original = p.x.hypot(p.z);
        let r_result = q.x.hypot(q.z);
        assert!((r_original - r_result).abs() < 0.001);
    }

    #[test]
    fn test_polar_repeat_preserves_y() {
        let p = Vec3::new(1.0, 7.0, 2.0);
        let q = modifier_polar_repeat(p, 4);
        assert!((q.y - 7.0).abs() < 0.001);
    }

    #[test]
    fn test_polar_repeat_symmetry() {
        // Points at equal angles should map to the same result
        let r = 5.0;
        let count = 6u32;
        let angle = std::f32::consts::TAU / count as f32;

        let p1 = Vec3::new(r * 0.0_f32.cos(), 0.0, r * 0.0_f32.sin());
        let p2 = Vec3::new(r * angle.cos(), 0.0, r * angle.sin());

        let q1 = modifier_polar_repeat(p1, count);
        let q2 = modifier_polar_repeat(p2, count);

        assert!(
            (q1 - q2).length() < 0.01,
            "Symmetric points should map to same result: {:?} vs {:?}",
            q1,
            q2
        );
    }
}