use crate::{Gizmo, GizmoCamera, HandleId, Overlay, Vec3};
pub const AXIS_X_COLOR: [f32; 4] = [1.0, 0.302, 0.302, 1.0];
pub const AXIS_Y_COLOR: [f32; 4] = [0.302, 1.0, 0.302, 1.0];
pub const AXIS_Z_COLOR: [f32; 4] = [0.302, 0.490, 1.0, 1.0];
pub const PLANE_COLOR: [f32; 4] = [0.180, 1.0, 0.180, 1.0];
pub const DEFAULT_PLANE_SCREEN_PX: f32 = 140.0;
pub const DEFAULT_FRAME_PX: f32 = 70.0;
pub const PICK_PX: f32 = 6.0;
pub fn datum_plane(
origin: Vec3,
x_axis: Vec3,
y_axis: Vec3,
size: f32,
color: [f32; 4],
) -> Overlay {
datum_plane_half(origin, x_axis, y_axis, size * 0.5, color)
}
pub fn datum_plane_screen(
origin: Vec3,
x_axis: Vec3,
y_axis: Vec3,
screen_px: f32,
color: [f32; 4],
camera: &GizmoCamera,
) -> Overlay {
let half = camera.world_per_pixel(origin) * screen_px * 0.5;
datum_plane_half(origin, x_axis, y_axis, half, color)
}
fn datum_plane_half(
origin: Vec3,
x_axis: Vec3,
y_axis: Vec3,
half: f32,
color: [f32; 4],
) -> Overlay {
let mut ov = Overlay::new();
let hx = x_axis.scale(half);
let hy = y_axis.scale(half);
let c00 = origin.sub(hx).sub(hy);
let c10 = origin.add(hx).sub(hy);
let c11 = origin.add(hx).add(hy);
let c01 = origin.sub(hx).add(hy);
let fill = with_alpha(scale_rgb(color, 0.55), (color[3] * 0.28).min(0.28));
ov.tri(c00, c10, c11, fill);
ov.tri(c00, c11, c01, fill);
let border = [color[0], color[1], color[2], 1.0];
ov.line(c00, c10, border);
ov.line(c10, c11, border);
ov.line(c11, c01, border);
ov.line(c01, c00, border);
let m = half * 0.28;
ov.line(c00, c00.add(x_axis.scale(m)), border);
ov.line(c00, c00.add(y_axis.scale(m)), border);
ov
}
pub fn datum_axis(point: Vec3, direction: Vec3, length: f32, color: [f32; 4]) -> Overlay {
let mut ov = Overlay::new();
let d = direction.normalized();
let end = point.add(d.scale(length));
ov.line(point, end, color); let head = length * 0.14;
push_cone(&mut ov, end, d, head, head * 0.45, color, 12);
ov
}
pub fn datum_frame(
origin: Vec3,
x: Vec3,
y: Vec3,
z: Vec3,
screen_len_px: f32,
camera: &GizmoCamera,
) -> Overlay {
let mut ov = Overlay::new();
let len = camera.world_per_pixel(origin) * screen_len_px;
let axes = [
(x.normalized(), AXIS_X_COLOR),
(y.normalized(), AXIS_Y_COLOR),
(z.normalized(), AXIS_Z_COLOR),
];
for (dir, col) in axes {
ov.line(origin, origin.add(dir.scale(len)), col);
}
let head = len * 0.16;
for (dir, col) in axes {
push_cone(&mut ov, origin.add(dir.scale(len)), dir, head, head * 0.5, col, 10);
}
let marker = (len * 0.08).max(camera.world_per_pixel(origin) * 3.0);
push_octahedron(&mut ov, origin, marker, [0.88, 0.88, 0.92, 1.0]);
ov
}
pub fn world_axes(camera: &GizmoCamera, length_px: f32) -> Overlay {
datum_frame(Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::Z, length_px, camera)
}
#[derive(Debug, Clone, Copy)]
pub struct DatumPlane {
pub origin: Vec3,
pub x_axis: Vec3,
pub y_axis: Vec3,
pub size: Option<f32>,
pub color: [f32; 4],
pub handle: HandleId,
}
impl DatumPlane {
pub fn normal(&self) -> Vec3 {
self.x_axis.cross(self.y_axis).normalized()
}
fn half(&self, camera: &GizmoCamera) -> f32 {
match self.size {
Some(s) => s * 0.5,
None => camera.world_per_pixel(self.origin) * DEFAULT_PLANE_SCREEN_PX * 0.5,
}
}
pub fn hit_point(&self, camera: &GizmoCamera, screen: [f32; 2]) -> Option<Vec3> {
let ray = camera.ray_from_screen(screen[0], screen[1]);
let t = ray.intersect_plane(self.origin, self.normal())?;
if t < 0.0 {
return None;
}
let p = ray.at(t);
let rel = p.sub(self.origin);
let u = rel.dot(self.x_axis.normalized());
let v = rel.dot(self.y_axis.normalized());
let half = self.half(camera);
(u.abs() <= half && v.abs() <= half).then_some(p)
}
}
impl Gizmo for DatumPlane {
fn geometry(
&self,
camera: &GizmoCamera,
hovered: Option<HandleId>,
active: Option<HandleId>,
) -> Overlay {
let hot = hovered == Some(self.handle) || active == Some(self.handle);
let color = if hot { brighten(self.color) } else { self.color };
match self.size {
Some(s) => datum_plane(self.origin, self.x_axis, self.y_axis, s, color),
None => datum_plane_screen(
self.origin,
self.x_axis,
self.y_axis,
DEFAULT_PLANE_SCREEN_PX,
color,
camera,
),
}
}
fn hit(&self, camera: &GizmoCamera, screen: [f32; 2]) -> Option<HandleId> {
self.hit_point(camera, screen).map(|_| self.handle)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DatumAxis {
pub point: Vec3,
pub direction: Vec3,
pub length: f32,
pub color: [f32; 4],
pub handle: HandleId,
}
impl DatumAxis {
fn end(&self) -> Vec3 {
self.point.add(self.direction.normalized().scale(self.length))
}
}
impl Gizmo for DatumAxis {
fn geometry(
&self,
_camera: &GizmoCamera,
hovered: Option<HandleId>,
active: Option<HandleId>,
) -> Overlay {
let hot = hovered == Some(self.handle) || active == Some(self.handle);
let color = if hot { brighten(self.color) } else { self.color };
datum_axis(self.point, self.direction, self.length, color)
}
fn hit(&self, camera: &GizmoCamera, screen: [f32; 2]) -> Option<HandleId> {
let ray = camera.ray_from_screen(screen[0], screen[1]);
let end = self.end();
let world_dist = ray.distance_to_segment(self.point, end);
let mid = self.point.lerp(end, 0.5);
let px = world_dist / camera.world_per_pixel(mid);
if px <= PICK_PX {
Some(self.handle)
} else {
None
}
}
}
fn push_cone(
ov: &mut Overlay,
apex: Vec3,
dir: Vec3,
len: f32,
radius: f32,
color: [f32; 4],
segments: usize,
) {
let base = apex.sub(dir.scale(len));
let p1 = dir.any_perp();
let p2 = dir.cross(p1).normalized();
let seg = segments.max(3);
for i in 0..seg {
let a0 = (i as f32) / (seg as f32) * std::f32::consts::TAU;
let a1 = ((i + 1) as f32) / (seg as f32) * std::f32::consts::TAU;
let r0 = base.add(p1.scale(a0.cos() * radius)).add(p2.scale(a0.sin() * radius));
let r1 = base.add(p1.scale(a1.cos() * radius)).add(p2.scale(a1.sin() * radius));
ov.tri(apex, r0, r1, color); ov.tri(base, r1, r0, color); }
}
fn push_octahedron(ov: &mut Overlay, center: Vec3, half: f32, color: [f32; 4]) {
let px = center.add(Vec3::X.scale(half));
let nx = center.sub(Vec3::X.scale(half));
let py = center.add(Vec3::Y.scale(half));
let ny = center.sub(Vec3::Y.scale(half));
let pz = center.add(Vec3::Z.scale(half));
let nz = center.sub(Vec3::Z.scale(half));
let faces = [
(px, py, pz),
(py, nx, pz),
(nx, ny, pz),
(ny, px, pz),
(py, px, nz),
(nx, py, nz),
(ny, nx, nz),
(px, ny, nz),
];
for (a, b, c) in faces {
ov.tri(a, b, c, color);
}
}
fn scale_rgb(c: [f32; 4], s: f32) -> [f32; 4] {
[c[0] * s, c[1] * s, c[2] * s, c[3]]
}
fn with_alpha(c: [f32; 4], a: f32) -> [f32; 4] {
[c[0], c[1], c[2], a]
}
fn brighten(c: [f32; 4]) -> [f32; 4] {
[
(c[0] * 1.35 + 0.1).min(1.0),
(c[1] * 1.35 + 0.1).min(1.0),
(c[2] * 1.35 + 0.1).min(1.0),
c[3],
]
}
#[cfg(test)]
mod tests {
use super::*;
fn ortho_vp(eye: Vec3, target: Vec3, w: f32, h: f32, half: f32) -> [[f32; 4]; 4] {
let fwd = target.sub(eye).normalized();
let up = if fwd.z.abs() > 0.9 { Vec3::Y } else { Vec3::Z };
let right = fwd.cross(up).normalized();
let u = right.cross(fwd).normalized();
let view = [
[right.x, u.x, -fwd.x, 0.0],
[right.y, u.y, -fwd.y, 0.0],
[right.z, u.z, -fwd.z, 0.0],
[-right.dot(eye), -u.dot(eye), fwd.dot(eye), 1.0],
];
let aspect = w / h;
let (l, r, b, t) = (-half * aspect, half * aspect, -half, half);
let (near, far) = (0.01f32, 100.0f32);
let ortho = [
[2.0 / (r - l), 0.0, 0.0, 0.0],
[0.0, 2.0 / (t - b), 0.0, 0.0],
[0.0, 0.0, -1.0 / (far - near), 0.0],
[-(r + l) / (r - l), -(t + b) / (t - b), -near / (far - near), 1.0],
];
mat_mul(&ortho, &view)
}
fn mat_mul(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
let mut out = [[0.0f32; 4]; 4];
for col in 0..4 {
for row in 0..4 {
let mut s = 0.0;
for k in 0..4 {
s += a[k][row] * b[col][k];
}
out[col][row] = s;
}
}
out
}
fn iso_cam(half: f32) -> GizmoCamera {
let eye = Vec3::new(10.0, 10.0, 10.0);
let target = Vec3::ZERO;
let fwd = target.sub(eye).normalized();
GizmoCamera {
view_proj: ortho_vp(eye, target, 200.0, 200.0, half),
eye,
forward: fwd,
up: Vec3::Z,
viewport: [200.0, 200.0],
orthographic: true,
}
}
fn topdown_cam(half: f32) -> GizmoCamera {
let eye = Vec3::new(0.0, 0.0, 10.0);
let target = Vec3::ZERO;
let fwd = target.sub(eye).normalized();
GizmoCamera {
view_proj: ortho_vp(eye, target, 200.0, 200.0, half),
eye,
forward: fwd,
up: Vec3::Y,
viewport: [200.0, 200.0],
orthographic: true,
}
}
fn seg_screen_len(ov: &Overlay, seg: usize, cam: &GizmoCamera) -> f32 {
let a = cam.world_to_screen(Vec3::from(ov.lines[seg * 2].pos)).unwrap();
let b = cam.world_to_screen(Vec3::from(ov.lines[seg * 2 + 1].pos)).unwrap();
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
}
#[test]
fn plane_emits_two_tris_and_border_in_plane() {
let origin = Vec3::new(1.0, 2.0, 3.0);
let (x, y) = (Vec3::X, Vec3::Y);
let ov = datum_plane(origin, x, y, 4.0, PLANE_COLOR);
assert_eq!(ov.tris.len(), 6, "expected 2 tris (6 verts)");
assert!(ov.lines.len() >= 8, "expected >= 4 border segments");
let half = 2.0f32;
let c00 = origin.sub(x.scale(half)).sub(y.scale(half));
let c10 = origin.add(x.scale(half)).sub(y.scale(half));
let c11 = origin.add(x.scale(half)).add(y.scale(half));
let c01 = origin.sub(x.scale(half)).add(y.scale(half));
let expect = [(c00, c10), (c10, c11), (c11, c01), (c01, c00)];
for (i, (a, b)) in expect.iter().enumerate() {
let pa = Vec3::from(ov.lines[i * 2].pos);
let pb = Vec3::from(ov.lines[i * 2 + 1].pos);
assert!(pa.sub(*a).length() < 1e-5, "border {i} start");
assert!(pb.sub(*b).length() < 1e-5, "border {i} end");
}
let n = x.cross(y).normalized();
for v in &ov.tris {
let d = Vec3::from(v.pos).sub(origin).dot(n);
assert!(d.abs() < 1e-5, "tri vert off-plane by {d}");
}
for v in &ov.lines {
let d = Vec3::from(v.pos).sub(origin).dot(n);
assert!(d.abs() < 1e-5, "line vert off-plane by {d}");
}
}
#[test]
fn axis_line_endpoints_correct() {
let point = Vec3::new(0.0, 0.0, 1.0);
let dir = Vec3::new(0.0, 0.0, 1.0);
let ov = datum_axis(point, dir, 5.0, AXIS_Z_COLOR);
assert_eq!(ov.lines.len(), 2, "one shaft segment");
let a = Vec3::from(ov.lines[0].pos);
let b = Vec3::from(ov.lines[1].pos);
assert!(a.sub(point).length() < 1e-6, "start at point");
assert!(b.sub(Vec3::new(0.0, 0.0, 6.0)).length() < 1e-6, "end at point+dir*len");
assert!(ov.tris.len() >= 9, "arrowhead cone tris");
}
#[test]
fn frame_axes_equal_and_screen_constant() {
let cam1 = iso_cam(6.0); let cam2 = iso_cam(3.0); let px = 40.0f32;
let f1 = datum_frame(Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::Z, px, &cam1);
let f2 = datum_frame(Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::Z, px, &cam2);
let l1 = [
seg_screen_len(&f1, 0, &cam1),
seg_screen_len(&f1, 1, &cam1),
seg_screen_len(&f1, 2, &cam1),
];
let l2 = [
seg_screen_len(&f2, 0, &cam2),
seg_screen_len(&f2, 1, &cam2),
seg_screen_len(&f2, 2, &cam2),
];
assert!((l1[0] - l1[1]).abs() < 1.0, "X vs Y len: {:?}", l1);
assert!((l1[1] - l1[2]).abs() < 1.0, "Y vs Z len: {:?}", l1);
for i in 0..3 {
assert!(
(l1[i] - l2[i]).abs() < 1.0,
"axis {i} screen length not constant across zoom: {} vs {}",
l1[i],
l2[i]
);
}
for i in 0..3 {
assert!(l1[i] > 0.5 * px && l1[i] < px + 1.0, "axis {i} len {}", l1[i]);
}
}
#[test]
fn plane_hit_inside_and_outside() {
let cam = topdown_cam(6.0);
let plane = DatumPlane {
origin: Vec3::ZERO,
x_axis: Vec3::X,
y_axis: Vec3::Y,
size: Some(5.0),
color: PLANE_COLOR,
handle: 7,
};
assert_eq!(plane.hit(&cam, [100.0, 100.0]), Some(7));
assert_eq!(plane.hit(&cam, [196.0, 100.0]), None);
}
#[test]
fn axis_hit_near_and_far() {
let cam = topdown_cam(6.0);
let axis = DatumAxis {
point: Vec3::new(-2.0, 0.0, 0.0),
direction: Vec3::X,
length: 4.0,
color: AXIS_X_COLOR,
handle: 9,
};
assert_eq!(axis.hit(&cam, [100.0, 100.0]), Some(9));
assert_eq!(axis.hit(&cam, [100.0, 40.0]), None);
}
#[test]
fn world_axes_is_a_triad() {
let cam = iso_cam(6.0);
let ov = world_axes(&cam, DEFAULT_FRAME_PX);
assert!(ov.lines.len() >= 6, "three shaft segments");
assert_eq!(ov.lines[0].color, AXIS_X_COLOR);
assert_eq!(ov.lines[2].color, AXIS_Y_COLOR);
assert_eq!(ov.lines[4].color, AXIS_Z_COLOR);
}
}