use crate::{GizmoCamera, Overlay, Vec3};
pub const ARROW_LEN_PX: f32 = 14.0;
pub const ARROW_HALF_WIDTH_PX: f32 = 5.0;
pub const ARROW_CONE_SEGMENTS: usize = 12;
pub const DEFAULT_ANGLE_RADIUS_PX: f32 = 120.0;
pub const MIN_ANGLE_RADIUS_PX: f32 = 40.0;
pub const ANGLE_EXT_PX: f32 = 18.0;
pub const LABEL_GAP_PX: f32 = 10.0;
pub const DIMENSION_COLOR: [f32; 4] = [1.0, 0.72, 0.30, 1.0];
#[derive(Debug, Clone)]
pub struct DimensionAnnotation {
pub overlay: Overlay,
pub label_anchor: Vec3,
}
fn rotate_about_axis(v: Vec3, axis: Vec3, angle: f32) -> Vec3 {
let (s, c) = angle.sin_cos();
v.scale(c)
.add(axis.cross(v).scale(s))
.add(axis.scale(axis.dot(v) * (1.0 - c)))
}
fn arrowhead(
ov: &mut Overlay,
apex: Vec3,
dir: Vec3,
len: f32,
radius: f32,
color: [f32; 4],
) {
let axis = dir.normalized();
if axis.length() < 1e-9 || len <= 0.0 || radius <= 0.0 {
return;
}
let base = apex.sub(axis.scale(len));
let u = axis.any_perp();
let v = axis.cross(u).normalized();
let ring = |k: usize| -> Vec3 {
let ang = (k as f32 / ARROW_CONE_SEGMENTS as f32) * std::f32::consts::TAU;
base.add(u.scale(ang.cos() * radius))
.add(v.scale(ang.sin() * radius))
};
let mut prev = ring(0);
for k in 1..=ARROW_CONE_SEGMENTS {
let cur = ring(k);
ov.tri(apex, prev, cur, color); ov.tri(base, cur, prev, color); prev = cur;
}
}
pub fn linear_dimension(
a: Vec3,
b: Vec3,
offset_dir: Vec3,
offset_dist: f32,
camera: &GizmoCamera,
) -> DimensionAnnotation {
linear_dimension_colored(a, b, offset_dir, offset_dist, camera, DIMENSION_COLOR)
}
pub fn linear_dimension_colored(
a: Vec3,
b: Vec3,
offset_dir: Vec3,
offset_dist: f32,
camera: &GizmoCamera,
color: [f32; 4],
) -> DimensionAnnotation {
let mut ov = Overlay::new();
let dir_raw = b.sub(a);
let len_ab = dir_raw.length();
let dir = if len_ab < 1e-9 {
Vec3::X
} else {
dir_raw.scale(1.0 / len_ab)
};
let mut od = offset_dir.sub(dir.scale(offset_dir.dot(dir)));
if od.length() < 1e-9 {
od = dir.any_perp();
}
od = od.normalized();
let da = a.add(od.scale(offset_dist));
let db = b.add(od.scale(offset_dist));
ov.line(a, da, color);
ov.line(b, db, color);
ov.line(da, db, color);
let wpp_a = camera.world_per_pixel(da);
let wpp_b = camera.world_per_pixel(db);
arrowhead(
&mut ov,
da,
dir.scale(-1.0),
ARROW_LEN_PX * wpp_a,
ARROW_HALF_WIDTH_PX * wpp_a,
color,
);
arrowhead(
&mut ov,
db,
dir,
ARROW_LEN_PX * wpp_b,
ARROW_HALF_WIDTH_PX * wpp_b,
color,
);
let label_anchor = da.lerp(db, 0.5);
DimensionAnnotation {
overlay: ov,
label_anchor,
}
}
pub fn angular_arc_points(vertex: Vec3, dir_a: Vec3, dir_b: Vec3, radius: f32) -> Vec<Vec3> {
let a = dir_a.normalized();
let b = dir_b.normalized();
let mut normal = a.cross(b);
if normal.length() < 1e-9 {
return Vec::new();
}
normal = normal.normalized();
let sweep = a.dot(b).clamp(-1.0, 1.0).acos();
let steps = (((sweep / (5.0_f32).to_radians()).ceil()) as usize).max(8);
let mut out = Vec::with_capacity(steps + 1);
for i in 0..=steps {
let t = i as f32 / steps as f32;
let d = rotate_about_axis(a, normal, sweep * t);
out.push(vertex.add(d.scale(radius)));
}
out
}
pub fn angular_dimension(
vertex: Vec3,
dir_a: Vec3,
dir_b: Vec3,
radius: f32,
camera: &GizmoCamera,
) -> DimensionAnnotation {
angular_dimension_colored(vertex, dir_a, dir_b, radius, camera, DIMENSION_COLOR)
}
pub fn angular_dimension_colored(
vertex: Vec3,
dir_a: Vec3,
dir_b: Vec3,
radius: f32,
camera: &GizmoCamera,
color: [f32; 4],
) -> DimensionAnnotation {
let mut ov = Overlay::new();
let a = dir_a.normalized();
let b = dir_b.normalized();
let wpp = camera.world_per_pixel(vertex);
let mut r = if radius > 1e-6 {
radius
} else {
DEFAULT_ANGLE_RADIUS_PX * wpp
};
r = r.max(MIN_ANGLE_RADIUS_PX * wpp);
let mut normal = a.cross(b);
if normal.length() < 1e-9 {
return DimensionAnnotation {
overlay: ov,
label_anchor: vertex.add(a.scale(r)),
};
}
normal = normal.normalized();
let sweep = a.dot(b).clamp(-1.0, 1.0).acos();
let pts = angular_arc_points(vertex, a, b, r);
for w in pts.windows(2) {
ov.line(w[0], w[1], color);
}
let ext = r + ANGLE_EXT_PX * wpp;
ov.line(vertex, vertex.add(a.scale(ext)), color);
ov.line(vertex, vertex.add(b.scale(ext)), color);
if pts.len() >= 2 {
let n = pts.len();
let start_dir = pts[0].sub(pts[1]).normalized();
arrowhead(
&mut ov,
pts[0],
start_dir,
ARROW_LEN_PX * wpp,
ARROW_HALF_WIDTH_PX * wpp,
color,
);
let end_dir = pts[n - 1].sub(pts[n - 2]).normalized();
arrowhead(
&mut ov,
pts[n - 1],
end_dir,
ARROW_LEN_PX * wpp,
ARROW_HALF_WIDTH_PX * wpp,
color,
);
}
let mid_dir = rotate_about_axis(a, normal, sweep * 0.5);
let label_anchor = vertex.add(mid_dir.scale(r));
DimensionAnnotation {
overlay: ov,
label_anchor,
}
}
pub fn radial_dimension(
center: Vec3,
point_on_circle: Vec3,
camera: &GizmoCamera,
) -> DimensionAnnotation {
radial_dimension_colored(center, point_on_circle, camera, DIMENSION_COLOR)
}
pub fn radial_dimension_colored(
center: Vec3,
point_on_circle: Vec3,
camera: &GizmoCamera,
color: [f32; 4],
) -> DimensionAnnotation {
let mut ov = Overlay::new();
let radial = point_on_circle.sub(center);
let dist = radial.length();
let dir = if dist < 1e-9 {
Vec3::X
} else {
radial.scale(1.0 / dist)
};
let wpp = camera.world_per_pixel(point_on_circle);
ov.line(center, point_on_circle, color);
arrowhead(
&mut ov,
point_on_circle,
dir,
ARROW_LEN_PX * wpp,
ARROW_HALF_WIDTH_PX * wpp,
color,
);
let gap = LABEL_GAP_PX * wpp;
let anchor = point_on_circle.add(dir.scale(gap.max(0.0)));
ov.line(point_on_circle, anchor, color);
DimensionAnnotation {
overlay: ov,
label_anchor: anchor,
}
}