#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
use roxlap_formats::kv6::{Kv6, Voxel};
use crate::world_query::{getcube, Cube};
pub(crate) const SETSPHMAXRAD: usize = 256;
pub(crate) const MAXZDIM: i32 = 256;
#[must_use]
pub(crate) fn lightvox(i: u32) -> u32 {
let b = i >> 24;
let r = ((((i >> 16) & 0xff) * b) >> 7).min(255);
let g = ((((i >> 8) & 0xff) * b) >> 7).min(255);
let bl = (((i & 0xff) * b) >> 7).min(255);
(r << 16) | (g << 8) | bl
}
#[derive(Debug, Clone)]
pub struct PowerTables {
pub factr: [[u32; 2]; SETSPHMAXRAD],
pub logint: [f64; SETSPHMAXRAD],
}
impl Default for PowerTables {
fn default() -> Self {
Self::new()
}
}
impl PowerTables {
#[must_use]
pub fn new() -> Self {
let mut factr = [[0u32; 2]; SETSPHMAXRAD];
factr[2][0] = 0;
let mut i: u32 = 1;
let mut j: u32 = 9;
let mut k: usize = 0;
let mut z: u32 = 3;
while (z as usize) < SETSPHMAXRAD {
if z == j {
j += (i << 2) + 12;
i += 2;
}
factr[z as usize][0] = 0;
factr[k][1] = z;
let mut zz: u32 = 3;
while zz <= i {
if z % zz == 0 {
factr[z as usize][0] = zz;
factr[z as usize][1] = z / zz;
break;
}
zz = factr[zz as usize][1];
}
if factr[z as usize][0] == 0 {
k = z as usize;
}
if (z as usize) + 1 < SETSPHMAXRAD {
factr[(z as usize) + 1][0] = (z + 1) >> 1;
factr[(z as usize) + 1][1] = 2;
}
z += 2;
}
let mut logint = [0.0f64; SETSPHMAXRAD];
for (zz, slot) in logint.iter_mut().enumerate().skip(1) {
*slot = f64::ln(zz as f64);
}
Self { factr, logint }
}
#[must_use]
pub fn build_tempfloatbuf(&self, hitrad: i32, curpow: f32) -> [f32; SETSPHMAXRAD] {
let mut buf = [0.0f32; SETSPHMAXRAD];
let hitrad_clamped = (hitrad.max(0) as usize).min(SETSPHMAXRAD - 2);
buf[0] = 0.0;
if hitrad_clamped >= 1 {
buf[1] = 1.0;
}
let curpow_d = f64::from(curpow);
for i in 2..=hitrad_clamped {
if self.factr[i][0] == 0 {
buf[i] = (self.logint[i] * curpow_d).exp() as f32;
} else {
let a = self.factr[i][0] as usize;
let b = self.factr[i][1] as usize;
buf[i] = buf[a] * buf[b];
}
}
buf[hitrad_clamped + 1] = f32::from_bits(0x7f7f_ffff);
buf
}
}
#[derive(Debug, Clone)]
pub struct MeltsphereOutput {
pub kv6: Kv6,
pub p: [f32; 3],
pub cw: u32,
}
#[allow(clippy::too_many_lines, clippy::similar_names)]
#[must_use]
pub fn meltsphere(
slab_buf: &[u8],
column_offsets: &[u32],
vsid: u32,
hit: [i32; 3],
hitrad: i32,
curpow: f32,
tables: &PowerTables,
) -> Option<MeltsphereOutput> {
let vsid_i = vsid as i32;
let xs = (hit[0] - hitrad).max(0);
let xe = (hit[0] + hitrad).min(vsid_i - 1);
let ys = (hit[1] - hitrad).max(0);
let ye = (hit[1] + hitrad).min(vsid_i - 1);
let zs = (hit[2] - hitrad).max(0);
let ze = (hit[2] + hitrad).min(MAXZDIM - 1);
if xs > xe || ys > ye || zs > ze {
return None;
}
let hitrad_clamped = if hitrad >= (SETSPHMAXRAD as i32) - 1 {
(SETSPHMAXRAD as i32) - 2
} else {
hitrad
};
let buf = tables.build_tempfloatbuf(hitrad_clamped, curpow);
let h = hitrad_clamped as usize;
let mut cx: i32 = 0;
let mut cy: i32 = 0;
let mut cz: i32 = 0;
let mut cw: i32 = 0;
let mut numvoxs: u32 = 0;
let mut sq: usize = 0;
for x in xs..=xe {
let dx = (x - hit[0]).unsigned_abs() as usize;
let ff = buf[h] - buf[dx];
for y in ys..=ye {
let dy = (y - hit[1]).unsigned_abs() as usize;
let f = ff - buf[dy];
if f > 0.0 {
while buf[sq] < f {
sq += 1;
}
while sq > 0 && buf[sq] >= f {
sq -= 1;
}
let sq_i = sq as i32;
let z0 = (hit[2] - sq_i).max(zs);
let z1 = (hit[2] + sq_i + 1).min(ze);
for z in z0..z1 {
match getcube(slab_buf, column_offsets, vsid, x, y, z) {
Cube::Air => {}
Cube::UnexposedSolid => {
cx = cx.wrapping_add(x.wrapping_sub(hit[0]));
cy = cy.wrapping_add(y.wrapping_sub(hit[1]));
cz = cz.wrapping_add(z.wrapping_sub(hit[2]));
cw = cw.wrapping_add(1);
}
Cube::Color(_) => {
cx = cx.wrapping_add(x.wrapping_sub(hit[0]));
cy = cy.wrapping_add(y.wrapping_sub(hit[1]));
cz = cz.wrapping_add(z.wrapping_sub(hit[2]));
cw = cw.wrapping_add(1);
numvoxs = numvoxs.wrapping_add(1);
}
}
}
}
}
}
if numvoxs == 0 {
return None;
}
let f_inv = (1.0f64 / f64::from(cw)) as f32;
let p = [
hit[0] as f32 + cx as f32 * f_inv,
hit[1] as f32 + cy as f32 * f_inv,
hit[2] as f32 + cz as f32 * f_inv,
];
let xsiz = (xe - xs + 1) as u32;
let ysiz = (ye - ys + 1) as u32;
let zsiz = (ze - zs + 1) as u32;
let xpiv = p[0] - xs as f32;
let ypiv = p[1] - ys as f32;
let zpiv = p[2] - zs as f32;
let mut voxels: Vec<Voxel> = Vec::with_capacity(numvoxs as usize);
let mut xlen: Vec<u32> = Vec::with_capacity(xsiz as usize);
let mut ylen_flat: Vec<u16> = Vec::with_capacity((xsiz as usize) * (ysiz as usize));
let mut o_x_voxs: u32 = 0;
let mut o_y_voxs: u32 = 0;
let mut sq: usize = 0;
for x in xs..=xe {
let dx = (x - hit[0]).unsigned_abs() as usize;
let ff = buf[h] - buf[dx];
for y in ys..=ye {
let dy = (y - hit[1]).unsigned_abs() as usize;
let f = ff - buf[dy];
if f > 0.0 {
while buf[sq] < f {
sq += 1;
}
while sq > 0 && buf[sq] >= f {
sq -= 1;
}
let sq_i = sq as i32;
let z0 = (hit[2] - sq_i).max(zs);
let z1 = (hit[2] + sq_i + 1).min(ze);
for z in z0..z1 {
if let Cube::Color(c) = getcube(slab_buf, column_offsets, vsid, x, y, z) {
voxels.push(Voxel {
col: lightvox(c),
z: (z - zs) as u16,
vis: 63,
dir: 0,
});
}
}
}
let cur = voxels.len() as u32;
ylen_flat.push((cur - o_y_voxs) as u16);
o_y_voxs = cur;
}
let cur = voxels.len() as u32;
xlen.push(cur - o_x_voxs);
o_x_voxs = cur;
}
let ylen: Vec<Vec<u16>> = ylen_flat
.chunks_exact(ysiz as usize)
.map(<[u16]>::to_vec)
.collect();
let kv6 = Kv6 {
xsiz,
ysiz,
zsiz,
xpiv,
ypiv,
zpiv,
voxels,
xlen,
ylen,
palette: None,
};
Some(MeltsphereOutput {
kv6,
p,
cw: cw as u32,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lightvox_neutral_brightness_passes_rgb_through() {
assert_eq!(lightvox(0x80ff_4030), 0x00ff_4030);
assert_eq!(lightvox(0x80ff_ffff), 0x00ff_ffff);
assert_eq!(lightvox(0x8000_0000), 0x0000_0000);
}
#[test]
fn lightvox_zero_alpha_blackens() {
assert_eq!(lightvox(0x00ff_ffff), 0);
assert_eq!(lightvox(0x0080_4020), 0);
}
#[test]
fn lightvox_clamps_at_255() {
assert_eq!(lightvox(0xffff_ffff), 0x00ff_ffff);
assert_eq!(lightvox(0xc080_8080), 0x00c0_c0c0);
assert_eq!(lightvox(0xc0ff_4030), 0x00ff_6048);
}
#[test]
fn lightvox_half_brightness() {
assert_eq!(lightvox(0x4080_8080), 0x0040_4040);
}
#[test]
fn factr_known_primes_have_zero_factor() {
let pt = PowerTables::new();
for &p in &[
2u32, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 251,
] {
assert_eq!(
pt.factr[p as usize][0], 0,
"prime {p} should have factr[{p}][0] == 0"
);
}
}
#[test]
fn factr_composites_decompose_to_their_factors() {
let pt = PowerTables::new();
let cases = [
(4u32, 2u32, 2u32),
(6, 3, 2),
(8, 4, 2),
(9, 3, 3),
(10, 5, 2),
(12, 6, 2),
(15, 3, 5),
(21, 3, 7),
(25, 5, 5),
(27, 3, 9),
(35, 5, 7),
(49, 7, 7),
(121, 11, 11),
(169, 13, 13),
(255, 3, 85),
];
for (z, a, b) in cases {
assert_eq!(
pt.factr[z as usize],
[a, b],
"factr[{z}] should be [{a}, {b}]"
);
}
}
#[test]
fn factr_invariant_holds_for_all_composites() {
let pt = PowerTables::new();
for z in 2..SETSPHMAXRAD as u32 {
let a = pt.factr[z as usize][0];
if a != 0 {
let b = pt.factr[z as usize][1];
assert_eq!(a * b, z, "factr[{z}] = [{a}, {b}], product {}", a * b);
}
}
}
#[test]
fn logint_matches_natural_log() {
let pt = PowerTables::new();
assert_eq!(pt.logint[1].to_bits(), 0.0f64.to_bits());
assert_eq!(pt.logint[10].to_bits(), (10.0f64).ln().to_bits());
assert_eq!(pt.logint[100].to_bits(), (100.0f64).ln().to_bits());
assert_eq!(pt.logint[255].to_bits(), (255.0f64).ln().to_bits());
}
#[test]
fn tempfloatbuf_curpow_two_approximates_squares() {
let pt = PowerTables::new();
let buf = pt.build_tempfloatbuf(64, 2.0);
for i in 0..=64u32 {
let want = (i * i) as f32;
let got = buf[i as usize];
let rel = ((got - want) / want.max(1.0)).abs();
assert!(rel < 1e-5, "tempfloatbuf[{i}] = {got}, want {want}");
}
}
#[test]
fn tempfloatbuf_zero_and_one_are_exact() {
let pt = PowerTables::new();
let buf = pt.build_tempfloatbuf(8, 2.0);
assert_eq!(buf[0].to_bits(), 0.0f32.to_bits());
assert_eq!(buf[1].to_bits(), 1.0f32.to_bits());
}
#[test]
fn tempfloatbuf_sentinel_is_max_finite_float() {
let pt = PowerTables::new();
let buf = pt.build_tempfloatbuf(8, 2.0);
assert_eq!(buf[9].to_bits(), 0x7f7f_ffff);
}
#[test]
fn tempfloatbuf_clamps_huge_hitrad() {
let pt = PowerTables::new();
let buf = pt.build_tempfloatbuf(10_000, 2.0);
assert_eq!(buf[SETSPHMAXRAD - 1].to_bits(), 0x7f7f_ffff);
}
#[test]
fn tempfloatbuf_curpow_three_matches_cubes() {
let pt = PowerTables::new();
let buf = pt.build_tempfloatbuf(20, 3.0);
for i in 0..=20u32 {
let want = (i as f32).powi(3);
let got = buf[i as usize];
let rel = ((got - want) / want.max(1.0)).abs();
assert!(rel < 1e-4, "tempfloatbuf[{i}] = {got}, want {want}");
}
}
fn synth_world(paints: &[(i32, i32, i32, u32)]) -> (Vec<u8>, Vec<u32>) {
const VSID: u32 = 16;
let empty_col: [u8; 8] = [0, 255, 255, 0, 0xff, 0xff, 0xff, 0x80];
let n_cols = (VSID * VSID) as usize;
let mut data: Vec<u8> = Vec::new();
let mut offsets: Vec<u32> = Vec::with_capacity(n_cols + 1);
let mut overrides: std::collections::HashMap<usize, (i32, u32)> =
std::collections::HashMap::new();
for &(x, y, z, c) in paints {
let idx = y as usize * VSID as usize + x as usize;
overrides.insert(idx, (z, c));
}
for i in 0..n_cols {
offsets.push(u32::try_from(data.len()).unwrap());
if let Some(&(z, c)) = overrides.get(&i) {
let z_u8 = z as u8;
data.extend_from_slice(&[2, z_u8, z_u8, 0]);
data.extend_from_slice(&c.to_le_bytes());
data.extend_from_slice(&[0, 255, 255, z_u8 + 1]);
data.extend_from_slice(&[0xff, 0xff, 0xff, 0x80]);
} else {
data.extend_from_slice(&empty_col);
}
}
offsets.push(u32::try_from(data.len()).unwrap());
(data, offsets)
}
#[test]
fn meltsphere_empty_region_returns_none() {
let (buf, off) = synth_world(&[]);
let pt = PowerTables::new();
let r = meltsphere(&buf, &off, 16, [8, 8, 4], 2, 2.0, &pt);
assert!(r.is_none(), "expected None, got {r:?}");
}
#[test]
fn meltsphere_single_voxel_at_center_extracts_one() {
let (buf, off) = synth_world(&[(8, 8, 4, 0x8011_2233)]);
let pt = PowerTables::new();
let out = meltsphere(&buf, &off, 16, [8, 8, 4], 2, 2.0, &pt)
.expect("sphere should hit the painted voxel");
assert_eq!(out.kv6.voxels.len(), 1);
let v = out.kv6.voxels[0];
assert_eq!(v.col, 0x0011_2233);
assert_eq!(v.vis, 63);
assert_eq!(v.dir, 0);
assert_eq!(v.z, 2);
assert_eq!(out.kv6.xsiz, 5);
assert_eq!(out.kv6.ysiz, 5);
assert_eq!(out.kv6.zsiz, 5);
let xsum: u32 = out.kv6.xlen.iter().sum();
assert_eq!(xsum, 1);
assert_eq!(out.kv6.ylen.len(), 5);
for row in &out.kv6.ylen {
assert_eq!(row.len(), 5);
}
}
#[test]
fn meltsphere_returns_none_when_aabb_off_map() {
let (buf, off) = synth_world(&[]);
let pt = PowerTables::new();
let r = meltsphere(&buf, &off, 16, [-100, 8, 4], 2, 2.0, &pt);
assert!(r.is_none());
}
#[test]
fn meltsphere_centroid_is_on_painted_voxel() {
let (buf, off) = synth_world(&[(8, 8, 4, 0x8011_2233)]);
let pt = PowerTables::new();
let out = meltsphere(&buf, &off, 16, [8, 8, 4], 2, 2.0, &pt).unwrap();
assert_eq!(out.p[0].to_bits(), 8.0f32.to_bits());
assert_eq!(out.p[1].to_bits(), 8.0f32.to_bits());
assert_eq!(out.p[2].to_bits(), 4.0f32.to_bits());
assert_eq!(out.cw, 1);
}
#[test]
fn meltsphere_skips_unexposed_solid_but_counts_for_centroid() {
let (buf, off) = synth_world(&[(8, 8, 4, 0x8000_ff00), (9, 8, 4, 0x8000_00ff)]);
let pt = PowerTables::new();
let out = meltsphere(&buf, &off, 16, [8, 8, 4], 2, 2.0, &pt).unwrap();
assert_eq!(out.kv6.voxels.len(), 2);
assert_eq!(out.cw, 2);
let expected_px = 8.0f32 + 1.0 * (1.0 / 2.0f64) as f32;
assert_eq!(out.p[0].to_bits(), expected_px.to_bits());
}
#[test]
fn meltsphere_xlen_ylen_partition_voxels() {
let paints = [
(8, 8, 4, 0x8011_2233),
(9, 8, 4, 0x8044_5566),
(8, 9, 4, 0x8077_8899),
];
let (buf, off) = synth_world(&paints);
let pt = PowerTables::new();
let out = meltsphere(&buf, &off, 16, [8, 8, 4], 2, 2.0, &pt).unwrap();
assert_eq!(out.kv6.voxels.len(), 3);
let xsum: u32 = out.kv6.xlen.iter().sum();
assert_eq!(xsum, 3);
for (x_i, ylen_row) in out.kv6.ylen.iter().enumerate() {
let row_sum: u32 = ylen_row.iter().map(|&v| u32::from(v)).sum();
assert_eq!(
row_sum, out.kv6.xlen[x_i],
"ylen row {x_i} sum {row_sum} != xlen[{x_i}] {}",
out.kv6.xlen[x_i]
);
}
}
}