#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Quad3 {
pub origin: [f32; 3],
pub u: [f32; 3],
pub v: [f32; 3],
pub color: [u8; 4],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Line3 {
pub a: [f32; 3],
pub b: [f32; 3],
pub width_px: f32,
pub color: [u8; 4],
}
#[derive(Clone, Debug, PartialEq)]
pub struct Text3 {
pub origin: [f32; 3],
pub right: [f32; 3],
pub up: [f32; 3],
pub text: String,
pub color: [u8; 4],
}
#[derive(Clone, Debug, PartialEq)]
pub struct Scene3D {
pub quads: Vec<Quad3>,
pub lines: Vec<Line3>,
pub texts: Vec<Text3>,
pub sheets: Vec<Quad3>,
pub sheet_texts: Vec<Text3>,
pub clear: [f32; 4],
pub min_text_px: f32,
}
impl Default for Scene3D {
fn default() -> Self {
Self { quads: Vec::new(), lines: Vec::new(), texts: Vec::new(), sheets: Vec::new(), sheet_texts: Vec::new(), clear: [0.02, 0.022, 0.03, 1.0], min_text_px: 6.0 }
}
}
pub const MONO_ADVANCE: f32 = 1233.0 / 2048.0;
pub const LINE_ADVANCE: f32 = 1.25;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Camera3 {
pub eye: [f32; 3],
pub target: [f32; 3],
pub up: [f32; 3],
pub fovy: f32,
pub near: f32,
pub far: f32,
}
impl Default for Camera3 {
fn default() -> Self {
Self { eye: [0.0, 0.0, 10.0], target: [0.0; 3], up: [0.0, 1.0, 0.0], fovy: 0.8, near: 0.05, far: 10_000.0 }
}
}
impl Camera3 {
#[must_use]
pub fn view_proj(&self, aspect: f32) -> [[f32; 4]; 4] {
self.matrix(aspect).to_cols_array_2d()
}
fn matrix(&self, aspect: f32) -> glam::Mat4 {
let v = |a: [f32; 3]| glam::Vec3::from_array(a);
let view = glam::camera::rh::view::look_at_mat4(v(self.eye), v(self.target), v(self.up));
let near = self.near.max(1e-6);
let proj = glam::camera::rh::proj::directx::perspective(self.fovy.clamp(1e-3, 3.1), aspect.max(1e-6), near, self.far.max(near * 1.001));
proj * view
}
#[must_use]
pub fn project(&self, p: [f32; 3], w: f32, h: f32) -> Option<([f32; 2], f32)> {
let c = self.matrix(w / h.max(1e-6)) * glam::Vec4::new(p[0], p[1], p[2], 1.0);
if c.w <= 1e-6 {
return None;
}
let n = c.truncate() / c.w;
Some(([(n.x * 0.5 + 0.5) * w, (0.5 - n.y * 0.5) * h], n.z))
}
#[must_use]
pub fn em_px(&self, at: [f32; 3], up: [f32; 3], w: f32, h: f32) -> f32 {
let top = [at[0] + up[0], at[1] + up[1], at[2] + up[2]];
match (self.project(at, w, h), self.project(top, w, h)) {
(Some((a, _)), Some((b, _))) => ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt(),
_ => 0.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlyphAt {
pub ch: char,
pub pen: [f32; 3],
}
#[must_use]
pub fn layout_text(t: &Text3) -> Vec<GlyphAt> {
let mut out = Vec::with_capacity(t.text.len());
let (mut col, mut row) = (0usize, 0usize);
for ch in t.text.chars() {
match ch {
'\n' => {
col = 0;
row += 1;
}
'\t' => col = (col / 4 + 1) * 4,
c if c.is_whitespace() || c.is_control() => col += 1,
c => {
let (dx, dy) = (col as f32 * MONO_ADVANCE, -(row as f32) * LINE_ADVANCE);
let pen = std::array::from_fn(|k| t.origin[k] + t.right[k] * dx + t.up[k] * dy);
out.push(GlyphAt { ch: c, pen });
col += 1;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_target_projects_to_the_screen_centre() {
let cam = Camera3 { eye: [3.0, 4.0, 12.0], target: [1.0, -2.0, 0.5], ..Camera3::default() };
let (xy, depth) = cam.project(cam.target, 800.0, 600.0).expect("in front");
assert!((xy[0] - 400.0).abs() < 1e-2 && (xy[1] - 300.0).abs() < 1e-2, "{xy:?}");
assert!(depth > 0.0 && depth < 1.0, "{depth}");
let above = cam.project([1.0, -1.0, 0.5], 800.0, 600.0).unwrap().0;
assert!(above[1] < 300.0, "{above:?}");
assert!(cam.project([3.0 * 2.0 - 1.0, 4.0 * 2.0 + 2.0, 12.0 * 2.0 - 0.5], 800.0, 600.0).is_none());
let near = cam.project([2.0, 1.0, 6.25], 800.0, 600.0).unwrap().1;
assert!(near < depth, "{near} {depth}");
}
#[test]
fn text_advances_monospaced_and_wraps_on_newline() {
let t = Text3 { origin: [1.0, 2.0, 3.0], right: [2.0, 0.0, 0.0], up: [0.0, 2.0, 0.0], text: "fn a\nb".into(), color: [255; 4] };
let g = layout_text(&t);
let chars: String = g.iter().map(|g| g.ch).collect();
assert_eq!(chars, "fnab", "spaces and newlines are not glyphs");
let adv = 2.0 * MONO_ADVANCE;
assert!((g[1].pen[0] - g[0].pen[0] - adv).abs() < 1e-5, "{g:?}");
assert!((g[2].pen[0] - (1.0 + 3.0 * adv)).abs() < 1e-5, "the space advanced: {g:?}");
assert_eq!(g[3].pen, [1.0, 2.0 - 2.0 * LINE_ADVANCE, 3.0], "a new line starts under the first");
}
#[test]
fn an_em_measures_its_projected_height() {
let cam = Camera3 { eye: [0.0, 0.0, 10.0], ..Camera3::default() };
let near = cam.em_px([0.0; 3], [0.0, 1.0, 0.0], 800.0, 600.0);
let far = Camera3 { eye: [0.0, 0.0, 30.0], ..cam }.em_px([0.0; 3], [0.0, 1.0, 0.0], 800.0, 600.0);
assert!(near > 10.0 && (near / far - 3.0).abs() < 0.2, "{near} {far}");
}
}