isokit: engine core, theme and Rhai scene runtime with prefab library
This commit is contained in:
commit
95c80ec6b1
32 files changed
+5896
No files matched your search
@@ -0,0 +1,181 @@
|
||||
//! 2:1 isometric projection with four 90 degree view rotations.
|
||||
//!
|
||||
//! View coordinates (vx, vy): the viewer sits at +vx +vy, so faces facing
|
||||
//! +vy show on the left (mid tone), faces facing +vx on the right (dark).
|
||||
|
||||
use crate::fmt::r1;
|
||||
|
||||
/// Screen units per metre along a ground axis.
|
||||
pub const C: f64 = 4.0;
|
||||
/// Screen units per metre of height.
|
||||
pub const CZ: f64 = 4.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Proj {
|
||||
/// 0: from the south-east, 1: south-west, 2: north-west, 3: north-east.
|
||||
pub rot: u8,
|
||||
}
|
||||
|
||||
impl Proj {
|
||||
pub fn new(rot: i64) -> Self {
|
||||
Proj { rot: rot.rem_euclid(4) as u8 }
|
||||
}
|
||||
|
||||
/// World plan point to view coordinates.
|
||||
#[inline]
|
||||
pub fn r(&self, x: f64, y: f64) -> [f64; 2] {
|
||||
match self.rot {
|
||||
0 => [x, -y],
|
||||
1 => [-y, -x],
|
||||
2 => [-x, y],
|
||||
_ => [y, x],
|
||||
}
|
||||
}
|
||||
|
||||
/// View coordinates back to the world plan.
|
||||
#[inline]
|
||||
pub fn r_inv(&self, vx: f64, vy: f64) -> [f64; 2] {
|
||||
match self.rot {
|
||||
0 => [vx, -vy],
|
||||
1 => [-vy, -vx],
|
||||
2 => [-vx, vy],
|
||||
_ => [vy, vx],
|
||||
}
|
||||
}
|
||||
|
||||
/// World point to screen.
|
||||
#[inline]
|
||||
pub fn s(&self, x: f64, y: f64, z: f64) -> [f64; 2] {
|
||||
let v = self.r(x, y);
|
||||
[(v[0] - v[1]) * C, (v[0] + v[1]) * C / 2.0 - z * CZ]
|
||||
}
|
||||
|
||||
/// Screen point to the world plan at height z.
|
||||
pub fn unproject(&self, sx: f64, sy: f64, z: f64) -> [f64; 2] {
|
||||
let a = sx / C; // vx - vy
|
||||
let b = (sy + z * CZ) * 2.0 / C; // vx + vy
|
||||
self.r_inv((a + b) / 2.0, (b - a) / 2.0)
|
||||
}
|
||||
|
||||
/// The view vector of a face normal if the face is turned towards the
|
||||
/// viewer, else None.
|
||||
#[inline]
|
||||
pub fn vis(&self, nx: f64, ny: f64) -> Option<[f64; 2]> {
|
||||
let v = self.r(nx, ny);
|
||||
if v[0] + v[1] > 1e-6 {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen displacement of a world displacement (for CSS motion).
|
||||
pub fn dv(&self, dx: f64, dy: f64, dz: f64) -> [f64; 2] {
|
||||
let a = self.s(dx, dy, dz);
|
||||
let b = self.s(0.0, 0.0, 0.0);
|
||||
[a[0] - b[0], a[1] - b[1]]
|
||||
}
|
||||
|
||||
/// Distance towards the viewer of a plan point (larger = nearer).
|
||||
#[inline]
|
||||
pub fn depth(&self, x: f64, y: f64) -> f64 {
|
||||
let v = self.r(x, y);
|
||||
v[0] + v[1]
|
||||
}
|
||||
|
||||
/// True when world x maps onto view y in this rotation: a mover should
|
||||
/// then run along x (the axis the depth sort compares second).
|
||||
pub fn along_x(&self) -> bool {
|
||||
self.r(1.0, 0.0)[0] == 0.0
|
||||
}
|
||||
|
||||
/// Append an SVG path `M..L..Z` through the projected points.
|
||||
pub fn d(&self, out: &mut String, pts: &[[f64; 3]], open: bool) {
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
let s = self.s(p[0], p[1], p[2]);
|
||||
out.push(if i == 0 { 'M' } else { 'L' });
|
||||
r1(out, s[0]);
|
||||
out.push(' ');
|
||||
r1(out, s[1]);
|
||||
}
|
||||
if !open && !pts.is_empty() {
|
||||
out.push('Z');
|
||||
}
|
||||
}
|
||||
|
||||
/// Angle of north on screen in degrees (for the compass needle).
|
||||
pub fn north_deg(&self) -> f64 {
|
||||
let n = self.r(0.0, 1.0);
|
||||
((n[0] + n[1]) / 2.0).atan2(n[0] - n[1]).to_degrees() + 90.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Face class for a visible side face from its view-space normal.
|
||||
pub fn shade(v: [f64; 2]) -> &'static str {
|
||||
let a = v[1].atan2(v[0]).to_degrees();
|
||||
if a > 67.5 {
|
||||
"iso-l"
|
||||
} else if a < 22.5 {
|
||||
"iso-r"
|
||||
} else {
|
||||
"iso-c"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rotation_zero_matches_js() {
|
||||
let p = Proj::new(0);
|
||||
// JS: ROT[0](x, y) = [x, -y]; s = [(vx - vy) * 4, (vx + vy) * 2 - z * 4]
|
||||
assert_eq!(p.s(1.0, 0.0, 0.0), [4.0, 2.0]);
|
||||
assert_eq!(p.s(0.0, 1.0, 0.0), [4.0, -2.0]);
|
||||
assert_eq!(p.s(0.0, 0.0, 1.0), [0.0, -4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotations_wrap() {
|
||||
assert_eq!(Proj::new(5).rot, 1);
|
||||
assert_eq!(Proj::new(-1).rot, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unproject_round_trips() {
|
||||
for rot in 0..4 {
|
||||
let p = Proj::new(rot);
|
||||
let s = p.s(12.5, -3.25, 2.0);
|
||||
let w = p.unproject(s[0], s[1], 2.0);
|
||||
assert!((w[0] - 12.5).abs() < 1e-9 && (w[1] + 3.25).abs() < 1e-9, "rot {rot}: {w:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exactly_two_of_four_walls_face_the_viewer() {
|
||||
for rot in 0..4 {
|
||||
let p = Proj::new(rot);
|
||||
let n = [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)]
|
||||
.iter()
|
||||
.filter(|(x, y)| p.vis(*x, *y).is_some())
|
||||
.count();
|
||||
assert_eq!(n, 2, "rot {rot}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faces_get_left_and_right_shades() {
|
||||
let p = Proj::new(0);
|
||||
// rotation 0 looks from the south-east: the south face (normal -y) is on the left
|
||||
assert_eq!(shade(p.vis(0.0, -1.0).unwrap()), "iso-l");
|
||||
assert_eq!(shade(p.vis(1.0, 0.0).unwrap()), "iso-r");
|
||||
assert_eq!(shade(p.vis(1.0, -1.0).unwrap()), "iso-c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_grows_towards_viewer() {
|
||||
let p = Proj::new(0);
|
||||
assert!(p.depth(10.0, 0.0) > p.depth(0.0, 0.0)); // east is nearer from the south-east
|
||||
assert!(p.depth(0.0, -10.0) > p.depth(0.0, 0.0)); // south is nearer
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user