use serde_json::Value;
use crate::engine_state::rotate_euler_xyz_f64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FeatureDimKind {
Linear,
Angular,
}
#[derive(Clone, Debug)]
pub struct FeatureDimAnnotation {
pub field_key: String,
pub point_a: [f64; 3],
pub point_b: [f64; 3],
pub value: f64,
pub label: String,
pub kind: FeatureDimKind,
pub center: [f64; 3],
pub axis: [f64; 3],
pub ref_dir: [f64; 3],
}
impl FeatureDimAnnotation {
fn linear(field_key: &str, a: [f64; 3], b: [f64; 3], value: f64, label: &str) -> Self {
Self {
field_key: field_key.to_string(),
point_a: a,
point_b: b,
value,
label: label.to_string(),
kind: FeatureDimKind::Linear,
center: [0.0; 3],
axis: [0.0; 3],
ref_dir: [0.0; 3],
}
}
fn angular(
field_key: &str,
center: [f64; 3],
axis: [f64; 3],
ref_dir: [f64; 3],
value: f64,
label: &str,
) -> Self {
let axis = normalize_or(axis, [0.0, 1.0, 0.0]);
let d = dot3(ref_dir, axis);
let planar = [
ref_dir[0] - axis[0] * d,
ref_dir[1] - axis[1] * d,
ref_dir[2] - axis[2] * d,
];
let ref_dir = if norm3(planar) <= 1e-9 {
arbitrary_perpendicular(axis)
} else {
normalize_or(planar, arbitrary_perpendicular(axis))
};
Self {
field_key: field_key.to_string(),
point_a: center,
point_b: center,
value,
label: label.to_string(),
kind: FeatureDimKind::Angular,
center,
axis,
ref_dir,
}
}
pub fn midpoint(&self) -> [f64; 3] {
[
(self.point_a[0] + self.point_b[0]) * 0.5,
(self.point_a[1] + self.point_b[1]) * 0.5,
(self.point_a[2] + self.point_b[2]) * 0.5,
]
}
}
#[derive(Clone, Debug, Default)]
pub struct ResolvedRefs {
pub profile_center: Option<[f64; 3]>,
pub profile_normal: Option<[f64; 3]>,
pub axis_point: Option<[f64; 3]>,
pub axis_dir: Option<[f64; 3]>,
}
pub fn build_annotations(feature_type: &str, input_params: &Value) -> Vec<FeatureDimAnnotation> {
build_annotations_with_refs(feature_type, input_params, &ResolvedRefs::default())
}
pub fn build_annotations_with_refs(
feature_type: &str,
input_params: &Value,
resolved: &ResolvedRefs,
) -> Vec<FeatureDimAnnotation> {
let transform = input_params.get("transform");
match feature_type {
"P.CU" => build_cube(input_params, transform),
"P.CY" => build_cylinder(input_params, transform),
"P.CO" => build_cone(input_params, transform),
"P.S" | "P.SP" => build_sphere(input_params, transform),
"P.PY" => build_pyramid(input_params, transform),
"P.T" => build_torus(input_params, transform),
"E" => build_extrude(input_params, resolved),
"R" => build_revolve(input_params, resolved),
_ => Vec::new(),
}
}
fn build_cube(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let sx = resolve_number(params, "sizeX");
let sy = resolve_number(params, "sizeY");
let sz = resolve_number(params, "sizeZ");
let p0 = transform_point(transform, [0.0, 0.0, 0.0]);
let px = transform_point(transform, [sx, 0.0, 0.0]);
let py = transform_point(transform, [0.0, sy, 0.0]);
let pz = transform_point(transform, [0.0, 0.0, sz]);
vec![
FeatureDimAnnotation::linear("sizeX", p0, px, sx, "X"),
FeatureDimAnnotation::linear("sizeY", p0, py, sy, "Y"),
FeatureDimAnnotation::linear("sizeZ", p0, pz, sz, "Z"),
]
}
fn build_cylinder(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let radius = resolve_number(params, "radius");
let height = resolve_number(params, "height");
let base = transform_point(transform, [0.0, 0.0, 0.0]);
let top = transform_point(transform, [0.0, height, 0.0]);
let radial = transform_point(transform, [radius, 0.0, 0.0]);
vec![
FeatureDimAnnotation::linear("radius", base, radial, radius, "R"),
FeatureDimAnnotation::linear("height", base, top, height, "H"),
]
}
fn build_cone(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let radius_top = resolve_number(params, "radiusTop");
let radius_bottom = resolve_number(params, "radiusBottom");
let height = resolve_number(params, "height");
let base_center = transform_point(transform, [0.0, 0.0, 0.0]);
let top_center = transform_point(transform, [0.0, height, 0.0]);
let base_radius = transform_point(transform, [radius_bottom, 0.0, 0.0]);
let top_radius = transform_point(transform, [radius_top, height, 0.0]);
vec![
FeatureDimAnnotation::linear("radiusBottom", base_center, base_radius, radius_bottom, "Rb"),
FeatureDimAnnotation::linear("radiusTop", top_center, top_radius, radius_top, "Rt"),
FeatureDimAnnotation::linear("height", base_center, top_center, height, "H"),
]
}
fn build_sphere(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let radius = resolve_number(params, "radius");
let center = transform_point(transform, [0.0, 0.0, 0.0]);
let radial = transform_point(transform, [radius, 0.0, 0.0]);
vec![FeatureDimAnnotation::linear("radius", center, radial, radius, "R")]
}
fn build_pyramid(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let side = resolve_number(params, "baseSideLength");
let height = resolve_number(params, "height");
let half_side = side * 0.5;
let base_y = -height * 0.5;
let apex_y = height * 0.5;
let base_start = transform_point(transform, [-half_side, base_y, -half_side]);
let base_end = transform_point(transform, [half_side, base_y, -half_side]);
let base_center = transform_point(transform, [0.0, base_y, 0.0]);
let apex = transform_point(transform, [0.0, apex_y, 0.0]);
vec![
FeatureDimAnnotation::linear("baseSideLength", base_start, base_end, side, "Side"),
FeatureDimAnnotation::linear("height", base_center, apex, height, "H"),
]
}
fn build_torus(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
let major = resolve_number(params, "majorRadius");
let tube = resolve_number(params, "tubeRadius");
let arc = clamp_deg(resolve_number(params, "arc"));
let center = transform_point(transform, [0.0, 0.0, 0.0]);
let major_point = transform_point(transform, [major, 0.0, 0.0]);
let tube_point = transform_point(transform, [major + tube, 0.0, 0.0]);
let axis = normalize_or(sub3(transform_point(transform, [0.0, 1.0, 0.0]), center), [0.0, 1.0, 0.0]);
let start_dir = sub3(major_point, center);
vec![
FeatureDimAnnotation::linear("majorRadius", center, major_point, major, "R"),
FeatureDimAnnotation::linear("tubeRadius", major_point, tube_point, tube, "r"),
FeatureDimAnnotation::angular("arc", center, axis, start_dir, arc, "Arc"),
]
}
fn build_extrude(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
let (Some(center), Some(normal)) = (resolved.profile_center, resolved.profile_normal) else {
return Vec::new();
};
let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
let distance = resolve_number(params, "distance");
let back = resolve_number(params, "distanceBack");
let forward = [
center[0] + normal[0] * distance,
center[1] + normal[1] * distance,
center[2] + normal[2] * distance,
];
let backward = [
center[0] - normal[0] * back,
center[1] - normal[1] * back,
center[2] - normal[2] * back,
];
vec![
FeatureDimAnnotation::linear("distance", center, forward, distance, "D"),
FeatureDimAnnotation::linear("distanceBack", center, backward, back, "Db"),
]
}
fn build_revolve(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
let (Some(axis_point), Some(axis_dir)) = (resolved.axis_point, resolved.axis_dir) else {
return Vec::new();
};
let Some(profile_center) = resolved.profile_center else {
return Vec::new();
};
let axis = orient_revolve_axis(axis_dir, axis_point, profile_center, resolved.profile_normal);
let vertex = closest_point_on_line(profile_center, axis_point, axis);
let start_dir = sub3(profile_center, vertex);
let angle = clamp_deg(resolve_number(params, "angle"));
vec![FeatureDimAnnotation::angular("angle", vertex, axis, start_dir, angle, "A")]
}
pub(crate) fn orient_revolve_axis(
axis_dir: [f64; 3],
axis_point: [f64; 3],
profile_center: [f64; 3],
profile_normal: Option<[f64; 3]>,
) -> [f64; 3] {
let axis = normalize_or(axis_dir, [0.0, 1.0, 0.0]);
let Some(normal) = profile_normal else {
return axis;
};
if norm3(normal) <= 1e-12 {
return axis;
}
let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
let mut radial = sub3(profile_center, axis_point);
let d = dot3(radial, axis);
radial = [radial[0] - axis[0] * d, radial[1] - axis[1] * d, radial[2] - axis[2] * d];
if norm3(radial) <= 1e-12 {
return axis;
}
let c = cross3(axis, radial);
if dot3(c, normal) < 0.0 {
[-axis[0], -axis[1], -axis[2]]
} else {
axis
}
}
pub(crate) fn closest_point_on_line(
point: [f64; 3],
line_point: [f64; 3],
line_dir: [f64; 3],
) -> [f64; 3] {
let dir = normalize_or(line_dir, [0.0, 1.0, 0.0]);
let t = dot3(sub3(point, line_point), dir);
[
line_point[0] + dir[0] * t,
line_point[1] + dir[1] * t,
line_point[2] + dir[2] * t,
]
}
fn clamp_deg(v: f64) -> f64 {
v.clamp(-360.0, 360.0)
}
pub(crate) fn transform_point(transform: Option<&Value>, local: [f64; 3]) -> [f64; 3] {
let position = read_vec3(transform, "position", [0.0, 0.0, 0.0]);
let rotation_deg = read_vec3(transform, "rotationEuler", [0.0, 0.0, 0.0]);
let scale = read_vec3(transform, "scale", [1.0, 1.0, 1.0]);
let scaled = [local[0] * scale[0], local[1] * scale[1], local[2] * scale[2]];
let euler = [
rotation_deg[0].to_radians(),
rotation_deg[1].to_radians(),
rotation_deg[2].to_radians(),
];
let rotated = rotate_euler_xyz_f64(scaled, euler);
[
rotated[0] + position[0],
rotated[1] + position[1],
rotated[2] + position[2],
]
}
fn read_vec3(transform: Option<&Value>, key: &str, default: [f64; 3]) -> [f64; 3] {
let array = transform.and_then(|t| t.get(key)).and_then(Value::as_array);
let mut out = default;
if let Some(array) = array {
for (index, slot) in out.iter_mut().enumerate() {
if let Some(number) = array.get(index).and_then(Value::as_f64) {
*slot = number;
}
}
}
out
}
fn resolve_number(params: &Value, key: &str) -> f64 {
match params.get(key) {
Some(Value::Number(n)) => n.as_f64().filter(|v| v.is_finite()).unwrap_or(0.0),
Some(Value::String(s)) => s.trim().parse::<f64>().ok().filter(|v| v.is_finite()).unwrap_or(0.0),
_ => 0.0,
}
}
const SHAFT_RGB: [f32; 3] = [0.80, 0.81, 0.82];
const ORANGE_RGB: [f32; 3] = [0.961, 0.651, 0.137];
const RED_RGB: [f32; 3] = [0.902, 0.157, 0.157];
const GREEN_RGB: [f32; 3] = [0.204, 0.808, 0.267];
const SHAFT_RAD_PX: f64 = 2.2;
const CONE_LEN_PX: f64 = 16.0;
const CONE_RAD_PX: f64 = 6.0;
pub(crate) const ORIGIN_SPHERE_RAD_PX: f64 = 7.0;
pub const ANGLE_ARC_RAD_PX: f64 = 120.0;
const ANGLE_RAY_RAD_PX: f64 = 1.6;
const ARC_DEG_PER_SEG: f64 = 4.0;
const DASH_LEN_PX: f64 = 6.0;
const DASH_GAP_PX: f64 = 5.0;
const TUBE_SEGMENTS: usize = 8;
const CONE_SEGMENTS: usize = 16;
const SPHERE_RINGS: usize = 6;
const SPHERE_SECTORS: usize = 10;
pub fn leaders_buffers(
annotations: &[FeatureDimAnnotation],
world_per_pixel: f64,
) -> (Vec<f32>, Vec<f32>) {
let mut tb = TriBuf::default();
let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
let cone_len = CONE_LEN_PX * world_per_pixel;
let cone_rad = CONE_RAD_PX * world_per_pixel;
let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
let mut origins: Vec<[f64; 3]> = Vec::new();
let mut add_origin = |tb: &mut TriBuf, a: [f64; 3]| {
if !origins.iter().any(|o| norm3(sub3(*o, a)) < 1e-6) {
push_sphere(tb, a, sphere_rad, ORANGE_RGB);
origins.push(a);
}
};
for ann in annotations {
match ann.kind {
FeatureDimKind::Angular => {
add_origin(&mut tb, ann.center);
push_angle_gizmo(&mut tb, ann, world_per_pixel);
}
FeatureDimKind::Linear => {
let a = ann.point_a;
let b = ann.point_b;
let axis = sub3(b, a);
let len = norm3(axis);
add_origin(&mut tb, a);
if len < 1e-9 {
continue;
}
let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
let cl = cone_len.min(len * 0.9);
let shaft_end = [b[0] - dir[0] * cl, b[1] - dir[1] * cl, b[2] - dir[2] * cl];
push_tube(&mut tb, a, shaft_end, shaft_rad, SHAFT_RGB);
push_cone(&mut tb, shaft_end, b, cone_rad, ORANGE_RGB);
}
}
}
(tb.positions, tb.colors)
}
#[derive(Default)]
struct TriBuf {
positions: Vec<f32>,
colors: Vec<f32>,
}
impl TriBuf {
fn tri(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], rgb: [f32; 3]) {
for p in [a, b, c] {
self.positions
.extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
self.colors.extend_from_slice(&rgb);
}
}
}
fn push_tube(tb: &mut TriBuf, a: [f64; 3], b: [f64; 3], radius: f64, rgb: [f32; 3]) {
let axis = sub3(b, a);
let len = norm3(axis);
if len < 1e-9 || radius <= 0.0 {
return;
}
let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
let (u, v) = axis_basis(dir);
let ring = |center: [f64; 3], k: usize| -> [f64; 3] {
let ang = (k as f64 / TUBE_SEGMENTS as f64) * std::f64::consts::TAU;
let (c, s) = (ang.cos() * radius, ang.sin() * radius);
[
center[0] + u[0] * c + v[0] * s,
center[1] + u[1] * c + v[1] * s,
center[2] + u[2] * c + v[2] * s,
]
};
for k in 0..TUBE_SEGMENTS {
let a0 = ring(a, k);
let a1 = ring(a, k + 1);
let b0 = ring(b, k);
let b1 = ring(b, k + 1);
tb.tri(a0, b0, b1, rgb);
tb.tri(a0, b1, a1, rgb);
}
}
fn push_cone(tb: &mut TriBuf, base: [f64; 3], tip: [f64; 3], radius: f64, rgb: [f32; 3]) {
let axis = sub3(tip, base);
let len = norm3(axis);
if len < 1e-9 || radius <= 0.0 {
return;
}
let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
let (u, v) = axis_basis(dir);
let ring = |k: usize| -> [f64; 3] {
let ang = (k as f64 / CONE_SEGMENTS as f64) * std::f64::consts::TAU;
let (c, s) = (ang.cos() * radius, ang.sin() * radius);
[
base[0] + u[0] * c + v[0] * s,
base[1] + u[1] * c + v[1] * s,
base[2] + u[2] * c + v[2] * s,
]
};
let mut prev = ring(0);
for k in 1..=CONE_SEGMENTS {
let cur = ring(k);
tb.tri(tip, prev, cur, rgb); tb.tri(base, cur, prev, rgb); prev = cur;
}
}
fn push_sphere(tb: &mut TriBuf, center: [f64; 3], radius: f64, rgb: [f32; 3]) {
if radius <= 0.0 {
return;
}
let point = |ring: usize, sector: usize| -> [f64; 3] {
let lat = std::f64::consts::PI * (ring as f64 / SPHERE_RINGS as f64)
- std::f64::consts::FRAC_PI_2;
let lon = std::f64::consts::TAU * (sector as f64 / SPHERE_SECTORS as f64);
[
center[0] + lat.cos() * lon.cos() * radius,
center[1] + lat.cos() * lon.sin() * radius,
center[2] + lat.sin() * radius,
]
};
for r in 0..SPHERE_RINGS {
for sct in 0..SPHERE_SECTORS {
let p00 = point(r, sct);
let p01 = point(r, sct + 1);
let p10 = point(r + 1, sct);
let p11 = point(r + 1, sct + 1);
tb.tri(p00, p10, p11, rgb);
tb.tri(p00, p11, p01, rgb);
}
}
}
fn push_angle_gizmo(tb: &mut TriBuf, ann: &FeatureDimAnnotation, world_per_pixel: f64) {
let center = ann.center;
let axis = ann.axis;
let start = ann.ref_dir;
let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
let ray_rad = ANGLE_RAY_RAD_PX * world_per_pixel;
let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
let cone_len = CONE_LEN_PX * world_per_pixel;
let cone_rad = CONE_RAD_PX * world_per_pixel;
let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
if radius <= 1e-9 {
return;
}
let value = ann.value.clamp(-359.9, 359.9);
let value_rad = value.to_radians();
let arc_point = |t: f64| -> [f64; 3] {
let dir = rotate_about_axis(start, axis, t);
[
center[0] + dir[0] * radius,
center[1] + dir[1] * radius,
center[2] + dir[2] * radius,
]
};
let seg_count = ((value.abs() / ARC_DEG_PER_SEG).ceil() as usize).max(2);
let mut prev = arc_point(0.0);
for k in 1..=seg_count {
let t = value_rad * (k as f64 / seg_count as f64);
let cur = arc_point(t);
push_tube(tb, prev, cur, shaft_rad, SHAFT_RGB);
prev = cur;
}
let dir_end = rotate_about_axis(start, axis, value_rad);
let end_pt = [
center[0] + dir_end[0] * radius,
center[1] + dir_end[1] * radius,
center[2] + dir_end[2] * radius,
];
push_sphere(tb, end_pt, sphere_rad, ORANGE_RGB);
let sweep_sign = if value < 0.0 { -1.0 } else { 1.0 };
let tangent = normalize_or(cross3(axis, dir_end), dir_end);
let tangent = [tangent[0] * sweep_sign, tangent[1] * sweep_sign, tangent[2] * sweep_sign];
let cone_tip = [
end_pt[0] + tangent[0] * cone_len,
end_pt[1] + tangent[1] * cone_len,
end_pt[2] + tangent[2] * cone_len,
];
push_cone(tb, end_pt, cone_tip, cone_rad, ORANGE_RGB);
let ref_end = [
center[0] + start[0] * radius,
center[1] + start[1] * radius,
center[2] + start[2] * radius,
];
push_dashed(tb, center, ref_end, ray_rad, RED_RGB, world_per_pixel);
let axis_len = radius * 0.7;
let axis_a = [
center[0] - axis[0] * axis_len,
center[1] - axis[1] * axis_len,
center[2] - axis[2] * axis_len,
];
let axis_b = [
center[0] + axis[0] * axis_len,
center[1] + axis[1] * axis_len,
center[2] + axis[2] * axis_len,
];
push_tube(tb, axis_a, axis_b, ray_rad, GREEN_RGB);
}
fn push_dashed(
tb: &mut TriBuf,
a: [f64; 3],
b: [f64; 3],
radius: f64,
rgb: [f32; 3],
world_per_pixel: f64,
) {
let axis = sub3(b, a);
let len = norm3(axis);
if len < 1e-9 {
return;
}
let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
let dash = (DASH_LEN_PX * world_per_pixel).max(1e-6);
let gap = (DASH_GAP_PX * world_per_pixel).max(1e-6);
let mut s = 0.0;
while s < len {
let e = (s + dash).min(len);
let p0 = [a[0] + dir[0] * s, a[1] + dir[1] * s, a[2] + dir[2] * s];
let p1 = [a[0] + dir[0] * e, a[1] + dir[1] * e, a[2] + dir[2] * e];
push_tube(tb, p0, p1, radius, rgb);
s = e + gap;
}
}
pub fn angular_chip_anchor(ann: &FeatureDimAnnotation, world_per_pixel: f64) -> [f64; 3] {
let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
let value = ann.value.clamp(-359.9, 359.9);
let bisector = rotate_about_axis(ann.ref_dir, ann.axis, (value * 0.5).to_radians());
[
ann.center[0] + bisector[0] * radius,
ann.center[1] + bisector[1] * radius,
ann.center[2] + bisector[2] * radius,
]
}
pub(crate) const ARROW_HANDLE_HIT_RAD_PX: f64 = CONE_RAD_PX + 12.0;
pub(crate) fn arrow_handle_point(
ann: &FeatureDimAnnotation,
world_per_pixel: f64,
) -> [f64; 3] {
match ann.kind {
FeatureDimKind::Linear => ann.point_b,
FeatureDimKind::Angular => {
let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
let value = ann.value.clamp(-359.9, 359.9);
let dir = rotate_about_axis(ann.ref_dir, ann.axis, value.to_radians());
[
ann.center[0] + dir[0] * radius,
ann.center[1] + dir[1] * radius,
ann.center[2] + dir[2] * radius,
]
}
}
}
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn normalize_or(v: [f64; 3], fallback: [f64; 3]) -> [f64; 3] {
let n = norm3(v);
if n < 1e-12 {
fallback
} else {
[v[0] / n, v[1] / n, v[2] / n]
}
}
fn arbitrary_perpendicular(direction: [f64; 3]) -> [f64; 3] {
if norm3(direction) <= 1e-12 {
return [0.0, 0.0, 1.0];
}
let seed = if dot3(direction, [0.0, 0.0, 1.0]).abs() < 0.9 {
[0.0, 0.0, 1.0]
} else {
[0.0, 1.0, 0.0]
};
let mut perp = cross3(direction, seed);
if norm3(perp) <= 1e-12 {
perp = cross3(direction, [1.0, 0.0, 0.0]);
}
if norm3(perp) <= 1e-12 {
[1.0, 0.0, 0.0]
} else {
normalize_or(perp, [1.0, 0.0, 0.0])
}
}
pub fn rotate_about_axis(v: [f64; 3], axis: [f64; 3], angle: f64) -> [f64; 3] {
let axis = normalize_or(axis, [0.0, 1.0, 0.0]);
let (s, c) = angle.sin_cos();
let d = dot3(axis, v);
let cr = cross3(axis, v);
[
v[0] * c + cr[0] * s + axis[0] * d * (1.0 - c),
v[1] * c + cr[1] * s + axis[1] * d * (1.0 - c),
v[2] * c + cr[2] * s + axis[2] * d * (1.0 - c),
]
}
fn norm3(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn axis_basis(dir: [f64; 3]) -> ([f64; 3], [f64; 3]) {
let seed = if dir[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let mut u = cross3(dir, seed);
let un = norm3(u);
if un < 1e-9 {
u = [0.0, 1.0, 0.0];
} else {
u = [u[0] / un, u[1] / un, u[2] / un];
}
let v = cross3(dir, u);
let vn = norm3(v).max(1e-9);
(u, [v[0] / vn, v[1] / vn, v[2] / vn])
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ident_transform() -> Value {
json!({
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0],
})
}
fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
norm3(sub3(a, b))
}
#[test]
fn cube_identity_gives_three_axis_dims() {
let params = json!({
"sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.CU", ¶ms);
assert_eq!(anns.len(), 3);
let keys: Vec<&str> = anns.iter().map(|a| a.field_key.as_str()).collect();
assert_eq!(keys, ["sizeX", "sizeY", "sizeZ"]);
assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
assert!((dist(anns[1].point_a, anns[1].point_b) - 20.0).abs() < 1e-9);
assert!((dist(anns[2].point_a, anns[2].point_b) - 30.0).abs() < 1e-9);
assert!((anns[0].point_a[0]).abs() < 1e-9);
assert!((anns[0].point_b[0] - 10.0).abs() < 1e-9);
assert!((anns[0].value - 10.0).abs() < 1e-9);
}
#[test]
fn cube_translation_moves_the_dims() {
let params = json!({
"sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
"transform": {
"position": [5.0, -3.0, 2.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0],
},
});
let anns = build_annotations("P.CU", ¶ms);
assert!((anns[0].point_a[0] - 5.0).abs() < 1e-9);
assert!((anns[0].point_a[1] + 3.0).abs() < 1e-9);
assert!((anns[0].point_a[2] - 2.0).abs() < 1e-9);
assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
}
#[test]
fn cube_rotation_90_about_z_maps_x_axis_to_y() {
let params = json!({
"sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 90.0],
"scale": [1.0, 1.0, 1.0],
},
});
let anns = build_annotations("P.CU", ¶ms);
let px = anns[0].point_b;
assert!(px[0].abs() < 1e-6, "{px:?}");
assert!((px[1] - 10.0).abs() < 1e-6, "{px:?}");
assert!(px[2].abs() < 1e-6, "{px:?}");
assert!((dist(anns[1].point_a, anns[1].point_b) - 20.0).abs() < 1e-6);
}
#[test]
fn cube_scale_scales_world_length() {
let params = json!({
"sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [2.0, 1.0, 1.0],
},
});
let anns = build_annotations("P.CU", ¶ms);
assert!((dist(anns[0].point_a, anns[0].point_b) - 20.0).abs() < 1e-9);
assert!((anns[0].value - 10.0).abs() < 1e-9);
}
#[test]
fn cylinder_gives_radius_and_height() {
let params = json!({
"radius": 4.0, "height": 12.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.CY", ¶ms);
assert_eq!(anns.len(), 2);
assert_eq!(anns[0].field_key, "radius");
assert_eq!(anns[1].field_key, "height");
assert!((dist(anns[0].point_a, anns[0].point_b) - 4.0).abs() < 1e-9);
assert!((dist(anns[1].point_a, anns[1].point_b) - 12.0).abs() < 1e-9);
assert!((anns[0].point_b[0] - 4.0).abs() < 1e-9);
assert!((anns[1].point_b[1] - 12.0).abs() < 1e-9);
}
#[test]
fn cone_gives_three_dims() {
let params = json!({
"radiusBottom": 5.0, "radiusTop": 2.0, "height": 8.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.CO", ¶ms);
assert_eq!(anns.len(), 3);
let keys: Vec<&str> = anns.iter().map(|a| a.field_key.as_str()).collect();
assert_eq!(keys, ["radiusBottom", "radiusTop", "height"]);
assert!((dist(anns[0].point_a, anns[0].point_b) - 5.0).abs() < 1e-9);
assert!((dist(anns[1].point_a, anns[1].point_b) - 2.0).abs() < 1e-9);
assert!((dist(anns[2].point_a, anns[2].point_b) - 8.0).abs() < 1e-9);
assert!((anns[1].point_a[1] - 8.0).abs() < 1e-9);
}
#[test]
fn sphere_gives_one_radius_dim() {
let params = json!({ "radius": 7.5, "transform": ident_transform() });
let anns = build_annotations("P.S", ¶ms);
assert_eq!(anns.len(), 1);
assert_eq!(anns[0].field_key, "radius");
assert!((dist(anns[0].point_a, anns[0].point_b) - 7.5).abs() < 1e-9);
}
#[test]
fn pyramid_gives_side_and_height_centered() {
let params = json!({
"baseSideLength": 6.0, "height": 10.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.PY", ¶ms);
assert_eq!(anns.len(), 2);
assert_eq!(anns[0].field_key, "baseSideLength");
assert_eq!(anns[1].field_key, "height");
assert!((dist(anns[0].point_a, anns[0].point_b) - 6.0).abs() < 1e-9);
assert!((dist(anns[1].point_a, anns[1].point_b) - 10.0).abs() < 1e-9);
assert!((anns[1].point_a[1] + 5.0).abs() < 1e-9);
assert!((anns[1].point_b[1] - 5.0).abs() < 1e-9);
}
#[test]
fn unknown_type_gives_no_dims() {
let params = json!({ "distance": 5.0 });
assert!(build_annotations("EXTRUDE", ¶ms).is_empty());
assert!(build_annotations("BOOLEAN", ¶ms).is_empty());
}
#[test]
fn numeric_string_params_resolve() {
let params = json!({
"sizeX": "10", "sizeY": "20", "sizeZ": "30",
"transform": ident_transform(),
});
let anns = build_annotations("P.CU", ¶ms);
assert!((anns[0].value - 10.0).abs() < 1e-9);
assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
}
#[test]
fn leaders_buffers_emit_shaft_cone_and_origin_sphere_tris() {
let ann = FeatureDimAnnotation::linear("sizeX", [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 10.0, "X");
let (pos, col) = leaders_buffers(std::slice::from_ref(&ann), 0.1);
assert!(!pos.is_empty(), "expected triangle geometry");
assert_eq!(pos.len(), col.len(), "one rgb color per xyz position");
assert_eq!(pos.len() % 9, 0, "whole triangles (3 verts * 3 floats)");
let has = |rgb: [f32; 3]| {
col.chunks_exact(3)
.any(|c| (c[0] - rgb[0]).abs() < 1e-3 && (c[1] - rgb[1]).abs() < 1e-3 && (c[2] - rgb[2]).abs() < 1e-3)
};
assert!(has(SHAFT_RGB), "expected silver shaft tris");
assert!(has(ORANGE_RGB), "expected orange cone/sphere tris");
}
#[test]
fn cube_dims_share_one_origin_sphere() {
let anns = build_annotations(
"P.CU",
&json!({ "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0, "transform": ident_transform() }),
);
assert_eq!(anns.len(), 3);
let origins: std::collections::BTreeSet<_> = anns
.iter()
.map(|a| (a.point_a[0] as i64, a.point_a[1] as i64, a.point_a[2] as i64))
.collect();
assert_eq!(origins.len(), 1, "cube dims share one origin corner");
let (pos, _) = leaders_buffers(&anns, 0.1);
assert!(!pos.is_empty());
}
fn color_present(col: &[f32], rgb: [f32; 3]) -> bool {
col.chunks_exact(3).any(|c| {
(c[0] - rgb[0]).abs() < 1e-3
&& (c[1] - rgb[1]).abs() < 1e-3
&& (c[2] - rgb[2]).abs() < 1e-3
})
}
#[test]
fn torus_emits_two_linear_and_one_angular() {
let params = json!({
"majorRadius": 5.0, "tubeRadius": 1.0, "arc": 90.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.T", ¶ms);
assert_eq!(anns.len(), 3);
assert_eq!(anns[0].field_key, "majorRadius");
assert_eq!(anns[0].kind, FeatureDimKind::Linear);
assert!((dist(anns[0].point_a, anns[0].point_b) - 5.0).abs() < 1e-9);
assert_eq!(anns[1].field_key, "tubeRadius");
assert_eq!(anns[1].kind, FeatureDimKind::Linear);
assert!((dist(anns[1].point_a, anns[1].point_b) - 1.0).abs() < 1e-9);
assert!((anns[1].point_a[0] - 5.0).abs() < 1e-9);
let arc = &anns[2];
assert_eq!(arc.field_key, "arc");
assert_eq!(arc.kind, FeatureDimKind::Angular);
assert!((arc.value - 90.0).abs() < 1e-9);
assert!((arc.axis[1] - 1.0).abs() < 1e-6, "axis ≈ +Y: {:?}", arc.axis);
assert!((arc.ref_dir[0] - 1.0).abs() < 1e-6, "ref ≈ +X: {:?}", arc.ref_dir);
}
#[test]
fn torus_arc_clamped_to_360() {
let params = json!({
"majorRadius": 5.0, "tubeRadius": 1.0, "arc": 500.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.T", ¶ms);
assert!((anns[2].value - 360.0).abs() < 1e-9, "arc clamps to 360");
}
#[test]
fn extrude_emits_linear_distance_along_normal() {
let refs = ResolvedRefs {
profile_center: Some([2.0, 0.0, 0.0]),
profile_normal: Some([0.0, 0.0, 1.0]),
..Default::default()
};
let params = json!({ "distance": 10.0, "distanceBack": 3.0 });
let anns = build_annotations_with_refs("E", ¶ms, &refs);
assert_eq!(anns.len(), 2);
assert_eq!(anns[0].field_key, "distance");
assert_eq!(anns[0].kind, FeatureDimKind::Linear);
assert_eq!(anns[0].point_a, [2.0, 0.0, 0.0]);
assert!((anns[0].point_b[2] - 10.0).abs() < 1e-9);
assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
assert_eq!(anns[1].field_key, "distanceBack");
assert!((anns[1].point_b[2] + 3.0).abs() < 1e-9);
}
#[test]
fn revolve_emits_one_angular_about_axis() {
let refs = ResolvedRefs {
profile_center: Some([5.0, 0.0, 0.0]),
profile_normal: Some([0.0, 1.0, 0.0]),
axis_point: Some([0.0, 0.0, 0.0]),
axis_dir: Some([0.0, 0.0, 1.0]),
};
let params = json!({ "angle": 234.0 });
let anns = build_annotations_with_refs("R", ¶ms, &refs);
assert_eq!(anns.len(), 1);
let a = &anns[0];
assert_eq!(a.field_key, "angle");
assert_eq!(a.kind, FeatureDimKind::Angular);
assert!((a.value - 234.0).abs() < 1e-9);
assert!((a.axis[2] - 1.0).abs() < 1e-6, "axis ≈ +Z: {:?}", a.axis);
assert!((a.ref_dir[0] - 1.0).abs() < 1e-6, "ref ≈ +X: {:?}", a.ref_dir);
assert!(norm3(a.center) < 1e-9, "vertex on axis: {:?}", a.center);
}
#[test]
fn revolve_axis_orients_toward_profile_front() {
let base = ResolvedRefs {
profile_center: Some([5.0, 0.0, 0.0]),
profile_normal: Some([0.0, 1.0, 0.0]),
axis_point: Some([0.0, 0.0, 0.0]),
axis_dir: Some([0.0, 0.0, 1.0]),
};
let flipped = ResolvedRefs {
profile_normal: Some([0.0, -1.0, 0.0]),
..base.clone()
};
let params = json!({ "angle": 90.0 });
let a = build_annotations_with_refs("R", ¶ms, &base);
let b = build_annotations_with_refs("R", ¶ms, &flipped);
assert!((a[0].axis[2] - 1.0).abs() < 1e-6);
assert!((b[0].axis[2] + 1.0).abs() < 1e-6, "flipped normal → negated axis");
}
#[test]
fn extrude_and_revolve_empty_without_resolved_refs() {
let refs = ResolvedRefs::default();
assert!(build_annotations_with_refs("E", &json!({ "distance": 5.0 }), &refs).is_empty());
assert!(build_annotations_with_refs("R", &json!({ "angle": 90.0 }), &refs).is_empty());
}
#[test]
fn angle_gizmo_emits_arc_cone_sphere_ref_and_axis_tris() {
let params = json!({
"majorRadius": 5.0, "tubeRadius": 1.0, "arc": 234.0,
"transform": ident_transform(),
});
let anns = build_annotations("P.T", ¶ms);
let (pos, col) = leaders_buffers(&anns, 0.1);
assert!(!pos.is_empty());
assert_eq!(pos.len(), col.len());
assert_eq!(pos.len() % 9, 0);
assert!(color_present(&col, SHAFT_RGB), "grey arc");
assert!(color_present(&col, ORANGE_RGB), "orange cone/handle");
assert!(color_present(&col, RED_RGB), "red dashed reference");
assert!(color_present(&col, GREEN_RGB), "green axis");
}
#[test]
fn angular_chip_anchor_sits_on_the_arc_mid_sweep() {
let ann = FeatureDimAnnotation::angular(
"angle",
[0.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 0.0],
180.0,
"A",
);
let wpp = 0.01;
let anchor = angular_chip_anchor(&ann, wpp);
let radius = ANGLE_ARC_RAD_PX * wpp;
assert!((anchor[1] - radius).abs() < 1e-6, "mid-sweep ≈ +Y*radius: {anchor:?}");
assert!(anchor[0].abs() < 1e-6 && anchor[2].abs() < 1e-6);
}
}