1use serde_json::Value;
19
20use crate::engine_state::rotate_euler_xyz_f64;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum FeatureDimKind {
26 Linear,
28 Angular,
32}
33
34#[derive(Clone, Debug)]
47pub struct FeatureDimAnnotation {
48 pub field_key: String,
50 pub point_a: [f64; 3],
53 pub point_b: [f64; 3],
56 pub value: f64,
59 pub label: String,
61 pub kind: FeatureDimKind,
63 pub center: [f64; 3],
66 pub axis: [f64; 3],
69 pub ref_dir: [f64; 3],
72}
73
74impl FeatureDimAnnotation {
75 fn linear(field_key: &str, a: [f64; 3], b: [f64; 3], value: f64, label: &str) -> Self {
76 Self {
77 field_key: field_key.to_string(),
78 point_a: a,
79 point_b: b,
80 value,
81 label: label.to_string(),
82 kind: FeatureDimKind::Linear,
83 center: [0.0; 3],
84 axis: [0.0; 3],
85 ref_dir: [0.0; 3],
86 }
87 }
88
89 fn angular(
95 field_key: &str,
96 center: [f64; 3],
97 axis: [f64; 3],
98 ref_dir: [f64; 3],
99 value: f64,
100 label: &str,
101 ) -> Self {
102 let axis = normalize_or(axis, [0.0, 1.0, 0.0]);
103 let d = dot3(ref_dir, axis);
105 let planar = [
106 ref_dir[0] - axis[0] * d,
107 ref_dir[1] - axis[1] * d,
108 ref_dir[2] - axis[2] * d,
109 ];
110 let ref_dir = if norm3(planar) <= 1e-9 {
111 arbitrary_perpendicular(axis)
112 } else {
113 normalize_or(planar, arbitrary_perpendicular(axis))
114 };
115 Self {
116 field_key: field_key.to_string(),
117 point_a: center,
118 point_b: center,
119 value,
120 label: label.to_string(),
121 kind: FeatureDimKind::Angular,
122 center,
123 axis,
124 ref_dir,
125 }
126 }
127
128 pub fn midpoint(&self) -> [f64; 3] {
132 [
133 (self.point_a[0] + self.point_b[0]) * 0.5,
134 (self.point_a[1] + self.point_b[1]) * 0.5,
135 (self.point_a[2] + self.point_b[2]) * 0.5,
136 ]
137 }
138}
139
140#[derive(Clone, Debug, Default)]
147pub struct ResolvedRefs {
148 pub profile_center: Option<[f64; 3]>,
152 pub profile_normal: Option<[f64; 3]>,
155 pub axis_point: Option<[f64; 3]>,
157 pub axis_dir: Option<[f64; 3]>,
160}
161
162pub fn build_annotations(feature_type: &str, input_params: &Value) -> Vec<FeatureDimAnnotation> {
171 build_annotations_with_refs(feature_type, input_params, &ResolvedRefs::default())
172}
173
174pub fn build_annotations_with_refs(
180 feature_type: &str,
181 input_params: &Value,
182 resolved: &ResolvedRefs,
183) -> Vec<FeatureDimAnnotation> {
184 let transform = input_params.get("transform");
185 match feature_type {
186 "P.CU" => build_cube(input_params, transform),
187 "P.CY" => build_cylinder(input_params, transform),
188 "P.CO" => build_cone(input_params, transform),
189 "P.S" | "P.SP" => build_sphere(input_params, transform),
191 "P.PY" => build_pyramid(input_params, transform),
192 "P.T" => build_torus(input_params, transform),
193 "E" => build_extrude(input_params, resolved),
194 "R" => build_revolve(input_params, resolved),
195 _ => Vec::new(),
196 }
197}
198
199fn build_cube(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
203 let sx = resolve_number(params, "sizeX");
204 let sy = resolve_number(params, "sizeY");
205 let sz = resolve_number(params, "sizeZ");
206 let p0 = transform_point(transform, [0.0, 0.0, 0.0]);
207 let px = transform_point(transform, [sx, 0.0, 0.0]);
208 let py = transform_point(transform, [0.0, sy, 0.0]);
209 let pz = transform_point(transform, [0.0, 0.0, sz]);
210 vec![
211 FeatureDimAnnotation::linear("sizeX", p0, px, sx, "X"),
212 FeatureDimAnnotation::linear("sizeY", p0, py, sy, "Y"),
213 FeatureDimAnnotation::linear("sizeZ", p0, pz, sz, "Z"),
214 ]
215}
216
217fn build_cylinder(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
220 let radius = resolve_number(params, "radius");
221 let height = resolve_number(params, "height");
222 let base = transform_point(transform, [0.0, 0.0, 0.0]);
223 let top = transform_point(transform, [0.0, height, 0.0]);
224 let radial = transform_point(transform, [radius, 0.0, 0.0]);
225 vec![
226 FeatureDimAnnotation::linear("radius", base, radial, radius, "R"),
227 FeatureDimAnnotation::linear("height", base, top, height, "H"),
228 ]
229}
230
231fn build_cone(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
234 let radius_top = resolve_number(params, "radiusTop");
235 let radius_bottom = resolve_number(params, "radiusBottom");
236 let height = resolve_number(params, "height");
237 let base_center = transform_point(transform, [0.0, 0.0, 0.0]);
238 let top_center = transform_point(transform, [0.0, height, 0.0]);
239 let base_radius = transform_point(transform, [radius_bottom, 0.0, 0.0]);
240 let top_radius = transform_point(transform, [radius_top, height, 0.0]);
241 vec![
242 FeatureDimAnnotation::linear("radiusBottom", base_center, base_radius, radius_bottom, "Rb"),
243 FeatureDimAnnotation::linear("radiusTop", top_center, top_radius, radius_top, "Rt"),
244 FeatureDimAnnotation::linear("height", base_center, top_center, height, "H"),
245 ]
246}
247
248fn build_sphere(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
250 let radius = resolve_number(params, "radius");
251 let center = transform_point(transform, [0.0, 0.0, 0.0]);
252 let radial = transform_point(transform, [radius, 0.0, 0.0]);
253 vec![FeatureDimAnnotation::linear("radius", center, radial, radius, "R")]
254}
255
256fn build_pyramid(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
260 let side = resolve_number(params, "baseSideLength");
261 let height = resolve_number(params, "height");
262 let half_side = side * 0.5;
263 let base_y = -height * 0.5;
264 let apex_y = height * 0.5;
265 let base_start = transform_point(transform, [-half_side, base_y, -half_side]);
266 let base_end = transform_point(transform, [half_side, base_y, -half_side]);
267 let base_center = transform_point(transform, [0.0, base_y, 0.0]);
268 let apex = transform_point(transform, [0.0, apex_y, 0.0]);
269 vec![
270 FeatureDimAnnotation::linear("baseSideLength", base_start, base_end, side, "Side"),
271 FeatureDimAnnotation::linear("height", base_center, apex, height, "H"),
272 ]
273}
274
275fn build_torus(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
282 let major = resolve_number(params, "majorRadius");
283 let tube = resolve_number(params, "tubeRadius");
284 let arc = clamp_deg(resolve_number(params, "arc"));
285
286 let center = transform_point(transform, [0.0, 0.0, 0.0]);
287 let major_point = transform_point(transform, [major, 0.0, 0.0]);
288 let tube_point = transform_point(transform, [major + tube, 0.0, 0.0]);
289 let axis = normalize_or(sub3(transform_point(transform, [0.0, 1.0, 0.0]), center), [0.0, 1.0, 0.0]);
293 let start_dir = sub3(major_point, center);
294
295 vec![
296 FeatureDimAnnotation::linear("majorRadius", center, major_point, major, "R"),
297 FeatureDimAnnotation::linear("tubeRadius", major_point, tube_point, tube, "r"),
298 FeatureDimAnnotation::angular("arc", center, axis, start_dir, arc, "Arc"),
299 ]
300}
301
302fn build_extrude(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
308 let (Some(center), Some(normal)) = (resolved.profile_center, resolved.profile_normal) else {
309 return Vec::new();
310 };
311 let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
312 let distance = resolve_number(params, "distance");
313 let back = resolve_number(params, "distanceBack");
314 let forward = [
315 center[0] + normal[0] * distance,
316 center[1] + normal[1] * distance,
317 center[2] + normal[2] * distance,
318 ];
319 let backward = [
320 center[0] - normal[0] * back,
321 center[1] - normal[1] * back,
322 center[2] - normal[2] * back,
323 ];
324 vec![
325 FeatureDimAnnotation::linear("distance", center, forward, distance, "D"),
326 FeatureDimAnnotation::linear("distanceBack", center, backward, back, "Db"),
327 ]
328}
329
330fn build_revolve(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
337 let (Some(axis_point), Some(axis_dir)) = (resolved.axis_point, resolved.axis_dir) else {
338 return Vec::new();
339 };
340 let Some(profile_center) = resolved.profile_center else {
341 return Vec::new();
342 };
343 let axis = orient_revolve_axis(axis_dir, axis_point, profile_center, resolved.profile_normal);
344 let vertex = closest_point_on_line(profile_center, axis_point, axis);
345 let start_dir = sub3(profile_center, vertex);
348 let angle = clamp_deg(resolve_number(params, "angle"));
349 vec![FeatureDimAnnotation::angular("angle", vertex, axis, start_dir, angle, "A")]
350}
351
352pub(crate) fn orient_revolve_axis(
356 axis_dir: [f64; 3],
357 axis_point: [f64; 3],
358 profile_center: [f64; 3],
359 profile_normal: Option<[f64; 3]>,
360) -> [f64; 3] {
361 let axis = normalize_or(axis_dir, [0.0, 1.0, 0.0]);
362 let Some(normal) = profile_normal else {
363 return axis;
364 };
365 if norm3(normal) <= 1e-12 {
366 return axis;
367 }
368 let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
369 let mut radial = sub3(profile_center, axis_point);
371 let d = dot3(radial, axis);
372 radial = [radial[0] - axis[0] * d, radial[1] - axis[1] * d, radial[2] - axis[2] * d];
373 if norm3(radial) <= 1e-12 {
374 return axis;
375 }
376 let c = cross3(axis, radial);
377 if dot3(c, normal) < 0.0 {
378 [-axis[0], -axis[1], -axis[2]]
379 } else {
380 axis
381 }
382}
383
384pub(crate) fn closest_point_on_line(
386 point: [f64; 3],
387 line_point: [f64; 3],
388 line_dir: [f64; 3],
389) -> [f64; 3] {
390 let dir = normalize_or(line_dir, [0.0, 1.0, 0.0]);
391 let t = dot3(sub3(point, line_point), dir);
392 [
393 line_point[0] + dir[0] * t,
394 line_point[1] + dir[1] * t,
395 line_point[2] + dir[2] * t,
396 ]
397}
398
399fn clamp_deg(v: f64) -> f64 {
401 v.clamp(-360.0, 360.0)
402}
403
404pub(crate) fn transform_point(transform: Option<&Value>, local: [f64; 3]) -> [f64; 3] {
409 let position = read_vec3(transform, "position", [0.0, 0.0, 0.0]);
410 let rotation_deg = read_vec3(transform, "rotationEuler", [0.0, 0.0, 0.0]);
411 let scale = read_vec3(transform, "scale", [1.0, 1.0, 1.0]);
412 let scaled = [local[0] * scale[0], local[1] * scale[1], local[2] * scale[2]];
413 let euler = [
414 rotation_deg[0].to_radians(),
415 rotation_deg[1].to_radians(),
416 rotation_deg[2].to_radians(),
417 ];
418 let rotated = rotate_euler_xyz_f64(scaled, euler);
419 [
420 rotated[0] + position[0],
421 rotated[1] + position[1],
422 rotated[2] + position[2],
423 ]
424}
425
426fn read_vec3(transform: Option<&Value>, key: &str, default: [f64; 3]) -> [f64; 3] {
429 let array = transform.and_then(|t| t.get(key)).and_then(Value::as_array);
430 let mut out = default;
431 if let Some(array) = array {
432 for (index, slot) in out.iter_mut().enumerate() {
433 if let Some(number) = array.get(index).and_then(Value::as_f64) {
434 *slot = number;
435 }
436 }
437 }
438 out
439}
440
441fn resolve_number(params: &Value, key: &str) -> f64 {
446 match params.get(key) {
447 Some(Value::Number(n)) => n.as_f64().filter(|v| v.is_finite()).unwrap_or(0.0),
448 Some(Value::String(s)) => s.trim().parse::<f64>().ok().filter(|v| v.is_finite()).unwrap_or(0.0),
449 _ => 0.0,
450 }
451}
452
453const SHAFT_RGB: [f32; 3] = [0.80, 0.81, 0.82];
466const ORANGE_RGB: [f32; 3] = [0.961, 0.651, 0.137];
468const RED_RGB: [f32; 3] = [0.902, 0.157, 0.157];
470const GREEN_RGB: [f32; 3] = [0.204, 0.808, 0.267];
472
473const SHAFT_RAD_PX: f64 = 2.2;
475const CONE_LEN_PX: f64 = 16.0;
477const CONE_RAD_PX: f64 = 6.0;
479pub(crate) const ORIGIN_SPHERE_RAD_PX: f64 = 7.0;
483
484pub const ANGLE_ARC_RAD_PX: f64 = 120.0;
488const ANGLE_RAY_RAD_PX: f64 = 1.6;
490const ARC_DEG_PER_SEG: f64 = 4.0;
492const DASH_LEN_PX: f64 = 6.0;
494const DASH_GAP_PX: f64 = 5.0;
495
496const TUBE_SEGMENTS: usize = 8;
497const CONE_SEGMENTS: usize = 16;
498const SPHERE_RINGS: usize = 6;
499const SPHERE_SECTORS: usize = 10;
500
501pub fn leaders_buffers(
508 annotations: &[FeatureDimAnnotation],
509 world_per_pixel: f64,
510) -> (Vec<f32>, Vec<f32>) {
511 let mut tb = TriBuf::default();
512 let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
513 let cone_len = CONE_LEN_PX * world_per_pixel;
514 let cone_rad = CONE_RAD_PX * world_per_pixel;
515 let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
516
517 let mut origins: Vec<[f64; 3]> = Vec::new();
519 let mut add_origin = |tb: &mut TriBuf, a: [f64; 3]| {
520 if !origins.iter().any(|o| norm3(sub3(*o, a)) < 1e-6) {
521 push_sphere(tb, a, sphere_rad, ORANGE_RGB);
522 origins.push(a);
523 }
524 };
525
526 for ann in annotations {
527 match ann.kind {
528 FeatureDimKind::Angular => {
529 add_origin(&mut tb, ann.center);
536 push_angle_gizmo(&mut tb, ann, world_per_pixel);
537 }
538 FeatureDimKind::Linear => {
539 let a = ann.point_a;
540 let b = ann.point_b;
541 let axis = sub3(b, a);
542 let len = norm3(axis);
543 add_origin(&mut tb, a);
544 if len < 1e-9 {
545 continue;
546 }
547 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
548 let cl = cone_len.min(len * 0.9);
550 let shaft_end = [b[0] - dir[0] * cl, b[1] - dir[1] * cl, b[2] - dir[2] * cl];
551 push_tube(&mut tb, a, shaft_end, shaft_rad, SHAFT_RGB);
552 push_cone(&mut tb, shaft_end, b, cone_rad, ORANGE_RGB);
553 }
554 }
555 }
556 (tb.positions, tb.colors)
557}
558
559#[derive(Default)]
561struct TriBuf {
562 positions: Vec<f32>,
563 colors: Vec<f32>,
564}
565
566impl TriBuf {
567 fn tri(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], rgb: [f32; 3]) {
568 for p in [a, b, c] {
569 self.positions
570 .extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
571 self.colors.extend_from_slice(&rgb);
572 }
573 }
574}
575
576fn push_tube(tb: &mut TriBuf, a: [f64; 3], b: [f64; 3], radius: f64, rgb: [f32; 3]) {
578 let axis = sub3(b, a);
579 let len = norm3(axis);
580 if len < 1e-9 || radius <= 0.0 {
581 return;
582 }
583 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
584 let (u, v) = axis_basis(dir);
585 let ring = |center: [f64; 3], k: usize| -> [f64; 3] {
586 let ang = (k as f64 / TUBE_SEGMENTS as f64) * std::f64::consts::TAU;
587 let (c, s) = (ang.cos() * radius, ang.sin() * radius);
588 [
589 center[0] + u[0] * c + v[0] * s,
590 center[1] + u[1] * c + v[1] * s,
591 center[2] + u[2] * c + v[2] * s,
592 ]
593 };
594 for k in 0..TUBE_SEGMENTS {
595 let a0 = ring(a, k);
596 let a1 = ring(a, k + 1);
597 let b0 = ring(b, k);
598 let b1 = ring(b, k + 1);
599 tb.tri(a0, b0, b1, rgb);
600 tb.tri(a0, b1, a1, rgb);
601 }
602}
603
604fn push_cone(tb: &mut TriBuf, base: [f64; 3], tip: [f64; 3], radius: f64, rgb: [f32; 3]) {
607 let axis = sub3(tip, base);
608 let len = norm3(axis);
609 if len < 1e-9 || radius <= 0.0 {
610 return;
611 }
612 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
613 let (u, v) = axis_basis(dir);
614 let ring = |k: usize| -> [f64; 3] {
615 let ang = (k as f64 / CONE_SEGMENTS as f64) * std::f64::consts::TAU;
616 let (c, s) = (ang.cos() * radius, ang.sin() * radius);
617 [
618 base[0] + u[0] * c + v[0] * s,
619 base[1] + u[1] * c + v[1] * s,
620 base[2] + u[2] * c + v[2] * s,
621 ]
622 };
623 let mut prev = ring(0);
624 for k in 1..=CONE_SEGMENTS {
625 let cur = ring(k);
626 tb.tri(tip, prev, cur, rgb); tb.tri(base, cur, prev, rgb); prev = cur;
629 }
630}
631
632fn push_sphere(tb: &mut TriBuf, center: [f64; 3], radius: f64, rgb: [f32; 3]) {
634 if radius <= 0.0 {
635 return;
636 }
637 let point = |ring: usize, sector: usize| -> [f64; 3] {
638 let lat = std::f64::consts::PI * (ring as f64 / SPHERE_RINGS as f64)
639 - std::f64::consts::FRAC_PI_2;
640 let lon = std::f64::consts::TAU * (sector as f64 / SPHERE_SECTORS as f64);
641 [
642 center[0] + lat.cos() * lon.cos() * radius,
643 center[1] + lat.cos() * lon.sin() * radius,
644 center[2] + lat.sin() * radius,
645 ]
646 };
647 for r in 0..SPHERE_RINGS {
648 for sct in 0..SPHERE_SECTORS {
649 let p00 = point(r, sct);
650 let p01 = point(r, sct + 1);
651 let p10 = point(r + 1, sct);
652 let p11 = point(r + 1, sct + 1);
653 tb.tri(p00, p10, p11, rgb);
654 tb.tri(p00, p11, p01, rgb);
655 }
656 }
657}
658
659fn push_angle_gizmo(tb: &mut TriBuf, ann: &FeatureDimAnnotation, world_per_pixel: f64) {
666 let center = ann.center;
667 let axis = ann.axis;
668 let start = ann.ref_dir;
669 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
670 let ray_rad = ANGLE_RAY_RAD_PX * world_per_pixel;
671 let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
672 let cone_len = CONE_LEN_PX * world_per_pixel;
673 let cone_rad = CONE_RAD_PX * world_per_pixel;
674 let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
675 if radius <= 1e-9 {
676 return;
677 }
678 let value = ann.value.clamp(-359.9, 359.9);
682 let value_rad = value.to_radians();
683
684 let arc_point = |t: f64| -> [f64; 3] {
688 let dir = rotate_about_axis(start, axis, t);
689 [
690 center[0] + dir[0] * radius,
691 center[1] + dir[1] * radius,
692 center[2] + dir[2] * radius,
693 ]
694 };
695 let seg_count = ((value.abs() / ARC_DEG_PER_SEG).ceil() as usize).max(2);
696 let mut prev = arc_point(0.0);
697 for k in 1..=seg_count {
698 let t = value_rad * (k as f64 / seg_count as f64);
699 let cur = arc_point(t);
700 push_tube(tb, prev, cur, shaft_rad, SHAFT_RGB);
701 prev = cur;
702 }
703
704 let dir_end = rotate_about_axis(start, axis, value_rad);
707 let end_pt = [
708 center[0] + dir_end[0] * radius,
709 center[1] + dir_end[1] * radius,
710 center[2] + dir_end[2] * radius,
711 ];
712 push_sphere(tb, end_pt, sphere_rad, ORANGE_RGB);
713 let sweep_sign = if value < 0.0 { -1.0 } else { 1.0 };
715 let tangent = normalize_or(cross3(axis, dir_end), dir_end);
716 let tangent = [tangent[0] * sweep_sign, tangent[1] * sweep_sign, tangent[2] * sweep_sign];
717 let cone_tip = [
718 end_pt[0] + tangent[0] * cone_len,
719 end_pt[1] + tangent[1] * cone_len,
720 end_pt[2] + tangent[2] * cone_len,
721 ];
722 push_cone(tb, end_pt, cone_tip, cone_rad, ORANGE_RGB);
723
724 let ref_end = [
727 center[0] + start[0] * radius,
728 center[1] + start[1] * radius,
729 center[2] + start[2] * radius,
730 ];
731 push_dashed(tb, center, ref_end, ray_rad, RED_RGB, world_per_pixel);
732
733 let axis_len = radius * 0.7;
735 let axis_a = [
736 center[0] - axis[0] * axis_len,
737 center[1] - axis[1] * axis_len,
738 center[2] - axis[2] * axis_len,
739 ];
740 let axis_b = [
741 center[0] + axis[0] * axis_len,
742 center[1] + axis[1] * axis_len,
743 center[2] + axis[2] * axis_len,
744 ];
745 push_tube(tb, axis_a, axis_b, ray_rad, GREEN_RGB);
746}
747
748fn push_dashed(
751 tb: &mut TriBuf,
752 a: [f64; 3],
753 b: [f64; 3],
754 radius: f64,
755 rgb: [f32; 3],
756 world_per_pixel: f64,
757) {
758 let axis = sub3(b, a);
759 let len = norm3(axis);
760 if len < 1e-9 {
761 return;
762 }
763 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
764 let dash = (DASH_LEN_PX * world_per_pixel).max(1e-6);
765 let gap = (DASH_GAP_PX * world_per_pixel).max(1e-6);
766 let mut s = 0.0;
767 while s < len {
768 let e = (s + dash).min(len);
769 let p0 = [a[0] + dir[0] * s, a[1] + dir[1] * s, a[2] + dir[2] * s];
770 let p1 = [a[0] + dir[0] * e, a[1] + dir[1] * e, a[2] + dir[2] * e];
771 push_tube(tb, p0, p1, radius, rgb);
772 s = e + gap;
773 }
774}
775
776pub fn angular_chip_anchor(ann: &FeatureDimAnnotation, world_per_pixel: f64) -> [f64; 3] {
781 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
782 let value = ann.value.clamp(-359.9, 359.9);
783 let bisector = rotate_about_axis(ann.ref_dir, ann.axis, (value * 0.5).to_radians());
784 [
785 ann.center[0] + bisector[0] * radius,
786 ann.center[1] + bisector[1] * radius,
787 ann.center[2] + bisector[2] * radius,
788 ]
789}
790
791pub(crate) const ARROW_HANDLE_HIT_RAD_PX: f64 = CONE_RAD_PX + 12.0;
798
799pub(crate) fn arrow_handle_point(
806 ann: &FeatureDimAnnotation,
807 world_per_pixel: f64,
808) -> [f64; 3] {
809 match ann.kind {
810 FeatureDimKind::Linear => ann.point_b,
811 FeatureDimKind::Angular => {
812 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
813 let value = ann.value.clamp(-359.9, 359.9);
814 let dir = rotate_about_axis(ann.ref_dir, ann.axis, value.to_radians());
815 [
816 ann.center[0] + dir[0] * radius,
817 ann.center[1] + dir[1] * radius,
818 ann.center[2] + dir[2] * radius,
819 ]
820 }
821 }
822}
823
824fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
827 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
828}
829
830fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
831 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
832}
833
834fn normalize_or(v: [f64; 3], fallback: [f64; 3]) -> [f64; 3] {
836 let n = norm3(v);
837 if n < 1e-12 {
838 fallback
839 } else {
840 [v[0] / n, v[1] / n, v[2] / n]
841 }
842}
843
844fn arbitrary_perpendicular(direction: [f64; 3]) -> [f64; 3] {
846 if norm3(direction) <= 1e-12 {
847 return [0.0, 0.0, 1.0];
848 }
849 let seed = if dot3(direction, [0.0, 0.0, 1.0]).abs() < 0.9 {
850 [0.0, 0.0, 1.0]
851 } else {
852 [0.0, 1.0, 0.0]
853 };
854 let mut perp = cross3(direction, seed);
855 if norm3(perp) <= 1e-12 {
856 perp = cross3(direction, [1.0, 0.0, 0.0]);
857 }
858 if norm3(perp) <= 1e-12 {
859 [1.0, 0.0, 0.0]
860 } else {
861 normalize_or(perp, [1.0, 0.0, 0.0])
862 }
863}
864
865pub fn rotate_about_axis(v: [f64; 3], axis: [f64; 3], angle: f64) -> [f64; 3] {
868 let axis = normalize_or(axis, [0.0, 1.0, 0.0]);
869 let (s, c) = angle.sin_cos();
870 let d = dot3(axis, v);
871 let cr = cross3(axis, v);
872 [
873 v[0] * c + cr[0] * s + axis[0] * d * (1.0 - c),
874 v[1] * c + cr[1] * s + axis[1] * d * (1.0 - c),
875 v[2] * c + cr[2] * s + axis[2] * d * (1.0 - c),
876 ]
877}
878
879fn norm3(v: [f64; 3]) -> f64 {
880 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
881}
882
883fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
884 [
885 a[1] * b[2] - a[2] * b[1],
886 a[2] * b[0] - a[0] * b[2],
887 a[0] * b[1] - a[1] * b[0],
888 ]
889}
890
891fn axis_basis(dir: [f64; 3]) -> ([f64; 3], [f64; 3]) {
893 let seed = if dir[0].abs() < 0.9 {
894 [1.0, 0.0, 0.0]
895 } else {
896 [0.0, 1.0, 0.0]
897 };
898 let mut u = cross3(dir, seed);
899 let un = norm3(u);
900 if un < 1e-9 {
901 u = [0.0, 1.0, 0.0];
902 } else {
903 u = [u[0] / un, u[1] / un, u[2] / un];
904 }
905 let v = cross3(dir, u);
906 let vn = norm3(v).max(1e-9);
907 (u, [v[0] / vn, v[1] / vn, v[2] / vn])
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use serde_json::json;
914
915 fn ident_transform() -> Value {
916 json!({
917 "position": [0.0, 0.0, 0.0],
918 "rotationEuler": [0.0, 0.0, 0.0],
919 "scale": [1.0, 1.0, 1.0],
920 })
921 }
922
923 fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
924 norm3(sub3(a, b))
925 }
926
927 #[test]
928 fn cube_identity_gives_three_axis_dims() {
929 let params = json!({
930 "sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
931 "transform": ident_transform(),
932 });
933 let anns = build_annotations("P.CU", ¶ms);
934 assert_eq!(anns.len(), 3);
935 let keys: Vec<&str> = anns.iter().map(|a| a.field_key.as_str()).collect();
936 assert_eq!(keys, ["sizeX", "sizeY", "sizeZ"]);
937 assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
938 assert!((dist(anns[1].point_a, anns[1].point_b) - 20.0).abs() < 1e-9);
939 assert!((dist(anns[2].point_a, anns[2].point_b) - 30.0).abs() < 1e-9);
940 assert!((anns[0].point_a[0]).abs() < 1e-9);
942 assert!((anns[0].point_b[0] - 10.0).abs() < 1e-9);
943 assert!((anns[0].value - 10.0).abs() < 1e-9);
944 }
945
946 #[test]
947 fn cube_translation_moves_the_dims() {
948 let params = json!({
949 "sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
950 "transform": {
951 "position": [5.0, -3.0, 2.0],
952 "rotationEuler": [0.0, 0.0, 0.0],
953 "scale": [1.0, 1.0, 1.0],
954 },
955 });
956 let anns = build_annotations("P.CU", ¶ms);
957 assert!((anns[0].point_a[0] - 5.0).abs() < 1e-9);
959 assert!((anns[0].point_a[1] + 3.0).abs() < 1e-9);
960 assert!((anns[0].point_a[2] - 2.0).abs() < 1e-9);
961 assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
962 }
963
964 #[test]
965 fn cube_rotation_90_about_z_maps_x_axis_to_y() {
966 let params = json!({
967 "sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
968 "transform": {
969 "position": [0.0, 0.0, 0.0],
970 "rotationEuler": [0.0, 0.0, 90.0],
971 "scale": [1.0, 1.0, 1.0],
972 },
973 });
974 let anns = build_annotations("P.CU", ¶ms);
975 let px = anns[0].point_b;
977 assert!(px[0].abs() < 1e-6, "{px:?}");
978 assert!((px[1] - 10.0).abs() < 1e-6, "{px:?}");
979 assert!(px[2].abs() < 1e-6, "{px:?}");
980 assert!((dist(anns[1].point_a, anns[1].point_b) - 20.0).abs() < 1e-6);
982 }
983
984 #[test]
985 fn cube_scale_scales_world_length() {
986 let params = json!({
987 "sizeX": 10.0, "sizeY": 20.0, "sizeZ": 30.0,
988 "transform": {
989 "position": [0.0, 0.0, 0.0],
990 "rotationEuler": [0.0, 0.0, 0.0],
991 "scale": [2.0, 1.0, 1.0],
992 },
993 });
994 let anns = build_annotations("P.CU", ¶ms);
995 assert!((dist(anns[0].point_a, anns[0].point_b) - 20.0).abs() < 1e-9);
997 assert!((anns[0].value - 10.0).abs() < 1e-9);
998 }
999
1000 #[test]
1001 fn cylinder_gives_radius_and_height() {
1002 let params = json!({
1003 "radius": 4.0, "height": 12.0,
1004 "transform": ident_transform(),
1005 });
1006 let anns = build_annotations("P.CY", ¶ms);
1007 assert_eq!(anns.len(), 2);
1008 assert_eq!(anns[0].field_key, "radius");
1009 assert_eq!(anns[1].field_key, "height");
1010 assert!((dist(anns[0].point_a, anns[0].point_b) - 4.0).abs() < 1e-9);
1011 assert!((dist(anns[1].point_a, anns[1].point_b) - 12.0).abs() < 1e-9);
1012 assert!((anns[0].point_b[0] - 4.0).abs() < 1e-9);
1014 assert!((anns[1].point_b[1] - 12.0).abs() < 1e-9);
1015 }
1016
1017 #[test]
1018 fn cone_gives_three_dims() {
1019 let params = json!({
1020 "radiusBottom": 5.0, "radiusTop": 2.0, "height": 8.0,
1021 "transform": ident_transform(),
1022 });
1023 let anns = build_annotations("P.CO", ¶ms);
1024 assert_eq!(anns.len(), 3);
1025 let keys: Vec<&str> = anns.iter().map(|a| a.field_key.as_str()).collect();
1026 assert_eq!(keys, ["radiusBottom", "radiusTop", "height"]);
1027 assert!((dist(anns[0].point_a, anns[0].point_b) - 5.0).abs() < 1e-9);
1028 assert!((dist(anns[1].point_a, anns[1].point_b) - 2.0).abs() < 1e-9);
1029 assert!((dist(anns[2].point_a, anns[2].point_b) - 8.0).abs() < 1e-9);
1030 assert!((anns[1].point_a[1] - 8.0).abs() < 1e-9);
1032 }
1033
1034 #[test]
1035 fn sphere_gives_one_radius_dim() {
1036 let params = json!({ "radius": 7.5, "transform": ident_transform() });
1037 let anns = build_annotations("P.S", ¶ms);
1038 assert_eq!(anns.len(), 1);
1039 assert_eq!(anns[0].field_key, "radius");
1040 assert!((dist(anns[0].point_a, anns[0].point_b) - 7.5).abs() < 1e-9);
1041 }
1042
1043 #[test]
1044 fn pyramid_gives_side_and_height_centered() {
1045 let params = json!({
1046 "baseSideLength": 6.0, "height": 10.0,
1047 "transform": ident_transform(),
1048 });
1049 let anns = build_annotations("P.PY", ¶ms);
1050 assert_eq!(anns.len(), 2);
1051 assert_eq!(anns[0].field_key, "baseSideLength");
1052 assert_eq!(anns[1].field_key, "height");
1053 assert!((dist(anns[0].point_a, anns[0].point_b) - 6.0).abs() < 1e-9);
1054 assert!((dist(anns[1].point_a, anns[1].point_b) - 10.0).abs() < 1e-9);
1055 assert!((anns[1].point_a[1] + 5.0).abs() < 1e-9);
1057 assert!((anns[1].point_b[1] - 5.0).abs() < 1e-9);
1058 }
1059
1060 #[test]
1061 fn unknown_type_gives_no_dims() {
1062 let params = json!({ "distance": 5.0 });
1063 assert!(build_annotations("EXTRUDE", ¶ms).is_empty());
1064 assert!(build_annotations("BOOLEAN", ¶ms).is_empty());
1065 }
1066
1067 #[test]
1068 fn numeric_string_params_resolve() {
1069 let params = json!({
1070 "sizeX": "10", "sizeY": "20", "sizeZ": "30",
1071 "transform": ident_transform(),
1072 });
1073 let anns = build_annotations("P.CU", ¶ms);
1074 assert!((anns[0].value - 10.0).abs() < 1e-9);
1075 assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
1076 }
1077
1078 #[test]
1079 fn leaders_buffers_emit_shaft_cone_and_origin_sphere_tris() {
1080 let ann = FeatureDimAnnotation::linear("sizeX", [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 10.0, "X");
1081 let (pos, col) = leaders_buffers(std::slice::from_ref(&ann), 0.1);
1082 assert!(!pos.is_empty(), "expected triangle geometry");
1085 assert_eq!(pos.len(), col.len(), "one rgb color per xyz position");
1086 assert_eq!(pos.len() % 9, 0, "whole triangles (3 verts * 3 floats)");
1087 let has = |rgb: [f32; 3]| {
1089 col.chunks_exact(3)
1090 .any(|c| (c[0] - rgb[0]).abs() < 1e-3 && (c[1] - rgb[1]).abs() < 1e-3 && (c[2] - rgb[2]).abs() < 1e-3)
1091 };
1092 assert!(has(SHAFT_RGB), "expected silver shaft tris");
1093 assert!(has(ORANGE_RGB), "expected orange cone/sphere tris");
1094 }
1095
1096 #[test]
1097 fn cube_dims_share_one_origin_sphere() {
1098 let anns = build_annotations(
1102 "P.CU",
1103 &json!({ "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0, "transform": ident_transform() }),
1104 );
1105 assert_eq!(anns.len(), 3);
1106 let origins: std::collections::BTreeSet<_> = anns
1107 .iter()
1108 .map(|a| (a.point_a[0] as i64, a.point_a[1] as i64, a.point_a[2] as i64))
1109 .collect();
1110 assert_eq!(origins.len(), 1, "cube dims share one origin corner");
1111 let (pos, _) = leaders_buffers(&anns, 0.1);
1112 assert!(!pos.is_empty());
1113 }
1114
1115 fn color_present(col: &[f32], rgb: [f32; 3]) -> bool {
1118 col.chunks_exact(3).any(|c| {
1119 (c[0] - rgb[0]).abs() < 1e-3
1120 && (c[1] - rgb[1]).abs() < 1e-3
1121 && (c[2] - rgb[2]).abs() < 1e-3
1122 })
1123 }
1124
1125 #[test]
1126 fn torus_emits_two_linear_and_one_angular() {
1127 let params = json!({
1128 "majorRadius": 5.0, "tubeRadius": 1.0, "arc": 90.0,
1129 "transform": ident_transform(),
1130 });
1131 let anns = build_annotations("P.T", ¶ms);
1132 assert_eq!(anns.len(), 3);
1133 assert_eq!(anns[0].field_key, "majorRadius");
1135 assert_eq!(anns[0].kind, FeatureDimKind::Linear);
1136 assert!((dist(anns[0].point_a, anns[0].point_b) - 5.0).abs() < 1e-9);
1137 assert_eq!(anns[1].field_key, "tubeRadius");
1138 assert_eq!(anns[1].kind, FeatureDimKind::Linear);
1139 assert!((dist(anns[1].point_a, anns[1].point_b) - 1.0).abs() < 1e-9);
1140 assert!((anns[1].point_a[0] - 5.0).abs() < 1e-9);
1142 let arc = &anns[2];
1144 assert_eq!(arc.field_key, "arc");
1145 assert_eq!(arc.kind, FeatureDimKind::Angular);
1146 assert!((arc.value - 90.0).abs() < 1e-9);
1147 assert!((arc.axis[1] - 1.0).abs() < 1e-6, "axis ≈ +Y: {:?}", arc.axis);
1148 assert!((arc.ref_dir[0] - 1.0).abs() < 1e-6, "ref ≈ +X: {:?}", arc.ref_dir);
1149 }
1150
1151 #[test]
1152 fn torus_arc_clamped_to_360() {
1153 let params = json!({
1154 "majorRadius": 5.0, "tubeRadius": 1.0, "arc": 500.0,
1155 "transform": ident_transform(),
1156 });
1157 let anns = build_annotations("P.T", ¶ms);
1158 assert!((anns[2].value - 360.0).abs() < 1e-9, "arc clamps to 360");
1159 }
1160
1161 #[test]
1162 fn extrude_emits_linear_distance_along_normal() {
1163 let refs = ResolvedRefs {
1164 profile_center: Some([2.0, 0.0, 0.0]),
1165 profile_normal: Some([0.0, 0.0, 1.0]),
1166 ..Default::default()
1167 };
1168 let params = json!({ "distance": 10.0, "distanceBack": 3.0 });
1169 let anns = build_annotations_with_refs("E", ¶ms, &refs);
1170 assert_eq!(anns.len(), 2);
1171 assert_eq!(anns[0].field_key, "distance");
1172 assert_eq!(anns[0].kind, FeatureDimKind::Linear);
1173 assert_eq!(anns[0].point_a, [2.0, 0.0, 0.0]);
1175 assert!((anns[0].point_b[2] - 10.0).abs() < 1e-9);
1176 assert!((dist(anns[0].point_a, anns[0].point_b) - 10.0).abs() < 1e-9);
1177 assert_eq!(anns[1].field_key, "distanceBack");
1179 assert!((anns[1].point_b[2] + 3.0).abs() < 1e-9);
1180 }
1181
1182 #[test]
1183 fn revolve_emits_one_angular_about_axis() {
1184 let refs = ResolvedRefs {
1187 profile_center: Some([5.0, 0.0, 0.0]),
1188 profile_normal: Some([0.0, 1.0, 0.0]),
1189 axis_point: Some([0.0, 0.0, 0.0]),
1190 axis_dir: Some([0.0, 0.0, 1.0]),
1191 };
1192 let params = json!({ "angle": 234.0 });
1193 let anns = build_annotations_with_refs("R", ¶ms, &refs);
1194 assert_eq!(anns.len(), 1);
1195 let a = &anns[0];
1196 assert_eq!(a.field_key, "angle");
1197 assert_eq!(a.kind, FeatureDimKind::Angular);
1198 assert!((a.value - 234.0).abs() < 1e-9);
1199 assert!((a.axis[2] - 1.0).abs() < 1e-6, "axis ≈ +Z: {:?}", a.axis);
1200 assert!((a.ref_dir[0] - 1.0).abs() < 1e-6, "ref ≈ +X: {:?}", a.ref_dir);
1201 assert!(norm3(a.center) < 1e-9, "vertex on axis: {:?}", a.center);
1203 }
1204
1205 #[test]
1206 fn revolve_axis_orients_toward_profile_front() {
1207 let base = ResolvedRefs {
1210 profile_center: Some([5.0, 0.0, 0.0]),
1211 profile_normal: Some([0.0, 1.0, 0.0]),
1212 axis_point: Some([0.0, 0.0, 0.0]),
1213 axis_dir: Some([0.0, 0.0, 1.0]),
1214 };
1215 let flipped = ResolvedRefs {
1216 profile_normal: Some([0.0, -1.0, 0.0]),
1217 ..base.clone()
1218 };
1219 let params = json!({ "angle": 90.0 });
1220 let a = build_annotations_with_refs("R", ¶ms, &base);
1221 let b = build_annotations_with_refs("R", ¶ms, &flipped);
1222 assert!((a[0].axis[2] - 1.0).abs() < 1e-6);
1223 assert!((b[0].axis[2] + 1.0).abs() < 1e-6, "flipped normal → negated axis");
1224 }
1225
1226 #[test]
1227 fn extrude_and_revolve_empty_without_resolved_refs() {
1228 let refs = ResolvedRefs::default();
1229 assert!(build_annotations_with_refs("E", &json!({ "distance": 5.0 }), &refs).is_empty());
1230 assert!(build_annotations_with_refs("R", &json!({ "angle": 90.0 }), &refs).is_empty());
1231 }
1232
1233 #[test]
1234 fn angle_gizmo_emits_arc_cone_sphere_ref_and_axis_tris() {
1235 let params = json!({
1236 "majorRadius": 5.0, "tubeRadius": 1.0, "arc": 234.0,
1237 "transform": ident_transform(),
1238 });
1239 let anns = build_annotations("P.T", ¶ms);
1240 let (pos, col) = leaders_buffers(&anns, 0.1);
1241 assert!(!pos.is_empty());
1242 assert_eq!(pos.len(), col.len());
1243 assert_eq!(pos.len() % 9, 0);
1244 assert!(color_present(&col, SHAFT_RGB), "grey arc");
1247 assert!(color_present(&col, ORANGE_RGB), "orange cone/handle");
1248 assert!(color_present(&col, RED_RGB), "red dashed reference");
1249 assert!(color_present(&col, GREEN_RGB), "green axis");
1250 }
1251
1252 #[test]
1253 fn angular_chip_anchor_sits_on_the_arc_mid_sweep() {
1254 let ann = FeatureDimAnnotation::angular(
1257 "angle",
1258 [0.0, 0.0, 0.0],
1259 [0.0, 0.0, 1.0],
1260 [1.0, 0.0, 0.0],
1261 180.0,
1262 "A",
1263 );
1264 let wpp = 0.01;
1265 let anchor = angular_chip_anchor(&ann, wpp);
1266 let radius = ANGLE_ARC_RAD_PX * wpp;
1267 assert!((anchor[1] - radius).abs() < 1e-6, "mid-sweep ≈ +Y*radius: {anchor:?}");
1268 assert!(anchor[0].abs() < 1e-6 && anchor[2].abs() < 1e-6);
1269 }
1270}