1use crate::geometry3d::{cross3, dot3, len3 as norm3, sub3};
8
9use serde_json::Value;
10
11use crate::engine_state::rotate_euler_xyz_f64;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum FeatureDimKind {
17 Linear,
19 Angular,
23}
24
25#[derive(Clone, Debug)]
38pub struct FeatureDimAnnotation {
39 pub field_key: String,
41 pub point_a: [f64; 3],
44 pub point_b: [f64; 3],
47 pub value: f64,
50 pub label: String,
52 pub kind: FeatureDimKind,
54 pub center: [f64; 3],
57 pub axis: [f64; 3],
63 pub ref_dir: [f64; 3],
66}
67
68impl FeatureDimAnnotation {
69 pub(crate) fn linear(field_key: &str, a: [f64; 3], b: [f64; 3], value: f64, label: &str) -> Self {
74 Self {
75 field_key: field_key.to_string(),
76 point_a: a,
77 point_b: b,
78 value,
79 label: label.to_string(),
80 kind: FeatureDimKind::Linear,
81 center: [0.0; 3],
82 axis: [0.0; 3],
83 ref_dir: [0.0; 3],
84 }
85 }
86
87 pub(crate) fn angular(
96 field_key: &str,
97 center: [f64; 3],
98 axis: [f64; 3],
99 ref_dir: [f64; 3],
100 value: f64,
101 label: &str,
102 ) -> Self {
103 let axis = normalize_or(axis, [0.0, 1.0, 0.0]);
104 let d = dot3(ref_dir, axis);
106 let planar = [
107 ref_dir[0] - axis[0] * d,
108 ref_dir[1] - axis[1] * d,
109 ref_dir[2] - axis[2] * d,
110 ];
111 let ref_dir = if norm3(planar) <= 1e-9 {
112 arbitrary_perpendicular(axis)
113 } else {
114 normalize_or(planar, arbitrary_perpendicular(axis))
115 };
116 Self {
117 field_key: field_key.to_string(),
118 point_a: center,
119 point_b: center,
120 value,
121 label: label.to_string(),
122 kind: FeatureDimKind::Angular,
123 center,
124 axis,
125 ref_dir,
126 }
127 }
128
129 pub fn midpoint(&self) -> [f64; 3] {
133 [
134 (self.point_a[0] + self.point_b[0]) * 0.5,
135 (self.point_a[1] + self.point_b[1]) * 0.5,
136 (self.point_a[2] + self.point_b[2]) * 0.5,
137 ]
138 }
139}
140
141#[derive(Clone, Debug, Default)]
149pub struct ResolvedRefs {
150 pub profile_center: Option<[f64; 3]>,
154 pub profile_normal: Option<[f64; 3]>,
158 pub axis_point: Option<[f64; 3]>,
160 pub axis_dir: Option<[f64; 3]>,
163 pub plane_origin: Option<[f64; 3]>,
166 pub plane_normal: Option<[f64; 3]>,
169 pub plane_dim_length: Option<f64>,
174}
175
176pub fn build_annotations(feature_type: &str, input_params: &Value) -> Vec<FeatureDimAnnotation> {
185 build_annotations_with_refs(feature_type, input_params, &ResolvedRefs::default())
186}
187
188pub fn build_annotations_with_refs(
195 feature_type: &str,
196 input_params: &Value,
197 resolved: &ResolvedRefs,
198) -> Vec<FeatureDimAnnotation> {
199 let transform = input_params.get("transform");
200 match feature_type {
201 "P.CU" => build_cube(input_params, transform),
202 "P.CY" => build_cylinder(input_params, transform),
203 "P.CO" => build_cone(input_params, transform),
204 "P.S" | "P.SP" => build_sphere(input_params, transform),
206 "P.PY" => build_pyramid(input_params, transform),
207 "P.T" => build_torus(input_params, transform),
208 "E" => build_extrude(input_params, resolved),
209 "R" => build_revolve(input_params, resolved),
210 "P" => build_plane(input_params, resolved),
211 _ => Vec::new(),
212 }
213}
214
215fn build_cube(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
219 let sx = resolve_number(params, "sizeX");
220 let sy = resolve_number(params, "sizeY");
221 let sz = resolve_number(params, "sizeZ");
222 let p0 = transform_point(transform, [0.0, 0.0, 0.0]);
223 let px = transform_point(transform, [sx, 0.0, 0.0]);
224 let py = transform_point(transform, [0.0, sy, 0.0]);
225 let pz = transform_point(transform, [0.0, 0.0, sz]);
226 vec![
227 FeatureDimAnnotation::linear("sizeX", p0, px, sx, "X"),
228 FeatureDimAnnotation::linear("sizeY", p0, py, sy, "Y"),
229 FeatureDimAnnotation::linear("sizeZ", p0, pz, sz, "Z"),
230 ]
231}
232
233fn build_cylinder(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
236 let radius = resolve_number(params, "radius");
237 let height = resolve_number(params, "height");
238 let base = transform_point(transform, [0.0, 0.0, 0.0]);
239 let top = transform_point(transform, [0.0, height, 0.0]);
240 let radial = transform_point(transform, [radius, 0.0, 0.0]);
241 vec![
242 FeatureDimAnnotation::linear("radius", base, radial, radius, "R"),
243 FeatureDimAnnotation::linear("height", base, top, height, "H"),
244 ]
245}
246
247fn build_cone(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
250 let radius_top = resolve_number(params, "radiusTop");
251 let radius_bottom = resolve_number(params, "radiusBottom");
252 let height = resolve_number(params, "height");
253 let base_center = transform_point(transform, [0.0, 0.0, 0.0]);
254 let top_center = transform_point(transform, [0.0, height, 0.0]);
255 let base_radius = transform_point(transform, [radius_bottom, 0.0, 0.0]);
256 let top_radius = transform_point(transform, [radius_top, height, 0.0]);
257 vec![
258 FeatureDimAnnotation::linear("radiusBottom", base_center, base_radius, radius_bottom, "Rb"),
259 FeatureDimAnnotation::linear("radiusTop", top_center, top_radius, radius_top, "Rt"),
260 FeatureDimAnnotation::linear("height", base_center, top_center, height, "H"),
261 ]
262}
263
264fn build_sphere(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
266 let radius = resolve_number(params, "radius");
267 let center = transform_point(transform, [0.0, 0.0, 0.0]);
268 let radial = transform_point(transform, [radius, 0.0, 0.0]);
269 vec![FeatureDimAnnotation::linear("radius", center, radial, radius, "R")]
270}
271
272fn build_pyramid(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
276 let side = resolve_number(params, "baseSideLength");
277 let height = resolve_number(params, "height");
278 let half_side = side * 0.5;
279 let base_y = -height * 0.5;
280 let apex_y = height * 0.5;
281 let base_start = transform_point(transform, [-half_side, base_y, -half_side]);
282 let base_end = transform_point(transform, [half_side, base_y, -half_side]);
283 let base_center = transform_point(transform, [0.0, base_y, 0.0]);
284 let apex = transform_point(transform, [0.0, apex_y, 0.0]);
285 vec![
286 FeatureDimAnnotation::linear("baseSideLength", base_start, base_end, side, "Side"),
287 FeatureDimAnnotation::linear("height", base_center, apex, height, "H"),
288 ]
289}
290
291fn build_torus(params: &Value, transform: Option<&Value>) -> Vec<FeatureDimAnnotation> {
298 let major = resolve_number(params, "majorRadius");
299 let tube = resolve_number(params, "tubeRadius");
300 let arc = clamp_deg(resolve_number(params, "arc"));
301
302 let center = transform_point(transform, [0.0, 0.0, 0.0]);
303 let major_point = transform_point(transform, [major, 0.0, 0.0]);
304 let tube_point = transform_point(transform, [major + tube, 0.0, 0.0]);
305 let axis = normalize_or(sub3(transform_point(transform, [0.0, 1.0, 0.0]), center), [0.0, 1.0, 0.0]);
309 let start_dir = sub3(major_point, center);
310
311 vec![
312 FeatureDimAnnotation::linear("majorRadius", center, major_point, major, "R"),
313 FeatureDimAnnotation::linear("tubeRadius", major_point, tube_point, tube, "r"),
314 FeatureDimAnnotation::angular("arc", center, axis, start_dir, arc, "Arc"),
315 ]
316}
317
318fn build_extrude(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
324 let (Some(center), Some(normal)) = (resolved.profile_center, resolved.profile_normal) else {
325 return Vec::new();
326 };
327 let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
328 let distance = resolve_number(params, "distance");
329 let back = resolve_number(params, "distanceBack");
330 let leg = |value: f64| if value.abs() > 1e-6 { value } else { resolved.plane_dim_length.unwrap_or(0.0) };
335 let (forward_len, back_len) = (leg(distance), leg(back));
336 let forward = [
337 center[0] + normal[0] * forward_len,
338 center[1] + normal[1] * forward_len,
339 center[2] + normal[2] * forward_len,
340 ];
341 let backward = [
342 center[0] - normal[0] * back_len,
343 center[1] - normal[1] * back_len,
344 center[2] - normal[2] * back_len,
345 ];
346 vec![
347 FeatureDimAnnotation::linear("distance", center, forward, distance, "D"),
348 FeatureDimAnnotation::linear("distanceBack", center, backward, back, "Db"),
349 ]
350}
351
352fn build_plane(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
361 let (Some(origin), Some(normal)) = (resolved.plane_origin, resolved.plane_normal) else {
362 return Vec::new();
363 };
364 let offset = resolve_number(params, "offset_distance");
365 let base = [
367 origin[0] - normal[0] * offset,
368 origin[1] - normal[1] * offset,
369 origin[2] - normal[2] * offset,
370 ];
371 let extent = if offset.abs() > 1e-6 {
374 offset
375 } else {
376 resolved.plane_dim_length.unwrap_or(1.0)
377 };
378 let handle = [
379 base[0] + normal[0] * extent,
380 base[1] + normal[1] * extent,
381 base[2] + normal[2] * extent,
382 ];
383 vec![FeatureDimAnnotation::linear(
384 "offset_distance",
385 base,
386 handle,
387 offset,
388 "Offset",
389 )]
390}
391
392fn build_revolve(params: &Value, resolved: &ResolvedRefs) -> Vec<FeatureDimAnnotation> {
399 let (Some(axis_point), Some(axis_dir)) = (resolved.axis_point, resolved.axis_dir) else {
400 return Vec::new();
401 };
402 let Some(profile_center) = resolved.profile_center else {
403 return Vec::new();
404 };
405 let axis = orient_revolve_axis(axis_dir, axis_point, profile_center, resolved.profile_normal);
406 let vertex = closest_point_on_line(profile_center, axis_point, axis);
407 let start_dir = sub3(profile_center, vertex);
410 let angle = clamp_deg(resolve_number(params, "angle"));
411 vec![FeatureDimAnnotation::angular("angle", vertex, axis, start_dir, angle, "A")]
412}
413
414pub(crate) fn orient_revolve_axis(
418 axis_dir: [f64; 3],
419 axis_point: [f64; 3],
420 profile_center: [f64; 3],
421 profile_normal: Option<[f64; 3]>,
422) -> [f64; 3] {
423 let axis = normalize_or(axis_dir, [0.0, 1.0, 0.0]);
424 let Some(normal) = profile_normal else {
425 return axis;
426 };
427 if norm3(normal) <= 1e-12 {
428 return axis;
429 }
430 let normal = normalize_or(normal, [0.0, 0.0, 1.0]);
431 let mut radial = sub3(profile_center, axis_point);
433 let d = dot3(radial, axis);
434 radial = [radial[0] - axis[0] * d, radial[1] - axis[1] * d, radial[2] - axis[2] * d];
435 if norm3(radial) <= 1e-12 {
436 return axis;
437 }
438 let c = cross3(axis, radial);
439 if dot3(c, normal) < 0.0 {
440 [-axis[0], -axis[1], -axis[2]]
441 } else {
442 axis
443 }
444}
445
446pub(crate) fn closest_point_on_line(
448 point: [f64; 3],
449 line_point: [f64; 3],
450 line_dir: [f64; 3],
451) -> [f64; 3] {
452 let dir = normalize_or(line_dir, [0.0, 1.0, 0.0]);
453 let t = dot3(sub3(point, line_point), dir);
454 [
455 line_point[0] + dir[0] * t,
456 line_point[1] + dir[1] * t,
457 line_point[2] + dir[2] * t,
458 ]
459}
460
461fn clamp_deg(v: f64) -> f64 {
463 v.clamp(-360.0, 360.0)
464}
465
466pub(crate) fn transform_point(transform: Option<&Value>, local: [f64; 3]) -> [f64; 3] {
471 let position = read_vec3(transform, "position", [0.0, 0.0, 0.0]);
472 let rotation_deg = read_vec3(transform, "rotationEuler", [0.0, 0.0, 0.0]);
473 let scale = read_vec3(transform, "scale", [1.0, 1.0, 1.0]);
474 let scaled = [local[0] * scale[0], local[1] * scale[1], local[2] * scale[2]];
475 let euler = [
476 rotation_deg[0].to_radians(),
477 rotation_deg[1].to_radians(),
478 rotation_deg[2].to_radians(),
479 ];
480 let rotated = rotate_euler_xyz_f64(scaled, euler);
481 [
482 rotated[0] + position[0],
483 rotated[1] + position[1],
484 rotated[2] + position[2],
485 ]
486}
487
488fn read_vec3(transform: Option<&Value>, key: &str, default: [f64; 3]) -> [f64; 3] {
491 crate::json_support::vec3_or(transform.and_then(|t| t.get(key)), default)
492}
493
494fn resolve_number(params: &Value, key: &str) -> f64 {
499 match params.get(key) {
500 Some(Value::Number(n)) => n.as_f64().filter(|v| v.is_finite()).unwrap_or(0.0),
501 Some(Value::String(s)) => s.trim().parse::<f64>().ok().filter(|v| v.is_finite()).unwrap_or(0.0),
502 _ => 0.0,
503 }
504}
505
506const SHAFT_RGB: [f32; 3] = [0.80, 0.81, 0.82];
519const ORANGE_RGB: [f32; 3] = [0.961, 0.651, 0.137];
521const RED_RGB: [f32; 3] = [0.902, 0.157, 0.157];
523const GREEN_RGB: [f32; 3] = [0.204, 0.808, 0.267];
525
526const SHAFT_RAD_PX: f64 = 2.2;
528const CONE_LEN_PX: f64 = 16.0;
530const CONE_RAD_PX: f64 = 6.0;
532pub(crate) const ORIGIN_SPHERE_RAD_PX: f64 = 7.0;
536
537pub const ANGLE_ARC_RAD_PX: f64 = 120.0;
541const ANGLE_RAY_RAD_PX: f64 = 1.6;
543const ARC_DEG_PER_SEG: f64 = 4.0;
545const DASH_LEN_PX: f64 = 6.0;
547const DASH_GAP_PX: f64 = 5.0;
548
549const TUBE_SEGMENTS: usize = 8;
550const CONE_SEGMENTS: usize = 16;
551const SPHERE_RINGS: usize = 6;
552const SPHERE_SECTORS: usize = 10;
553
554pub fn leaders_buffers(
561 annotations: &[FeatureDimAnnotation],
562 world_per_pixel: f64,
563) -> (Vec<f32>, Vec<f32>) {
564 let mut tb = TriBuf::default();
565 let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
566 let cone_len = CONE_LEN_PX * world_per_pixel;
567 let cone_rad = CONE_RAD_PX * world_per_pixel;
568 let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
569
570 let mut origins: Vec<[f64; 3]> = Vec::new();
572 let mut add_origin = |tb: &mut TriBuf, a: [f64; 3]| {
573 if !origins.iter().any(|o| norm3(sub3(*o, a)) < 1e-6) {
574 push_sphere(tb, a, sphere_rad, ORANGE_RGB);
575 origins.push(a);
576 }
577 };
578
579 for ann in annotations {
580 match ann.kind {
581 FeatureDimKind::Angular => {
582 add_origin(&mut tb, ann.center);
589 push_angle_gizmo(&mut tb, ann, world_per_pixel);
590 }
591 FeatureDimKind::Linear => {
592 let a = ann.point_a;
593 let b = ann.point_b;
594 let axis = sub3(b, a);
595 let len = norm3(axis);
596 add_origin(&mut tb, a);
597 if len < 1e-9 {
598 continue;
599 }
600 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
601 let cl = cone_len.min(len * 0.9);
603 let shaft_end = [b[0] - dir[0] * cl, b[1] - dir[1] * cl, b[2] - dir[2] * cl];
604 push_tube(&mut tb, a, shaft_end, shaft_rad, SHAFT_RGB);
605 push_cone(&mut tb, shaft_end, b, cone_rad, ORANGE_RGB);
606 }
607 }
608 }
609 (tb.positions, tb.colors)
610}
611
612pub fn append_plain_leader(
619 positions: &mut Vec<f32>,
620 colors: &mut Vec<f32>,
621 a: [f64; 3],
622 b: [f64; 3],
623 world_per_pixel: f64,
624) {
625 let mut tb = TriBuf {
626 positions: std::mem::take(positions),
627 colors: std::mem::take(colors),
628 };
629 push_tube(&mut tb, a, b, ANGLE_RAY_RAD_PX * world_per_pixel, SHAFT_RGB);
630 *positions = tb.positions;
631 *colors = tb.colors;
632}
633
634pub fn freedom_glyph_buffers(
640 center: [f64; 3],
641 slides: &[[f64; 3]],
642 axes: &[([f64; 3], [f64; 3])],
643 world_per_pixel: f64,
644) -> (Vec<f32>, Vec<f32>) {
645 let mut tb = TriBuf::default();
646 let rod = SHAFT_RAD_PX * world_per_pixel;
647 let cone_len = CONE_LEN_PX * world_per_pixel;
648 let cone_rad = CONE_RAD_PX * world_per_pixel;
649 let half = GLYPH_SLIDE_HALF_PX * world_per_pixel;
650 for slide in slides {
651 let d = normalize_or(*slide, [1.0, 0.0, 0.0]);
652 let tip = |k: f64| [center[0] + d[0] * k, center[1] + d[1] * k, center[2] + d[2] * k];
653 push_tube(&mut tb, tip(-(half - cone_len)), tip(half - cone_len), rod, SLIDE_RGB);
654 push_cone(&mut tb, tip(half - cone_len), tip(half), cone_rad, SLIDE_RGB);
655 push_cone(&mut tb, tip(-(half - cone_len)), tip(-half), cone_rad, SLIDE_RGB);
656 }
657 for (point, direction) in axes {
658 let d = normalize_or(*direction, [0.0, 0.0, 1.0]);
659 let foot = closest_point_on_line(center, *point, d);
661 let along = |k: f64| [foot[0] + d[0] * k, foot[1] + d[1] * k, foot[2] + d[2] * k];
662 let reach = GLYPH_AXIS_HALF_PX * world_per_pixel;
663 push_tube(&mut tb, along(-reach), along(reach), ANGLE_RAY_RAD_PX * world_per_pixel, TURN_RGB);
664 let (u, v) = axis_basis(d);
666 let radius = GLYPH_CURL_RAD_PX * world_per_pixel;
667 let at = |angle: f64| {
668 let (c, s) = (angle.cos() * radius, angle.sin() * radius);
669 [foot[0] + u[0] * c + v[0] * s, foot[1] + u[1] * c + v[1] * s, foot[2] + u[2] * c + v[2] * s]
670 };
671 let sweep = 1.5 * std::f64::consts::PI;
672 let steps = 18;
673 for i in 0..steps {
674 let a0 = sweep * i as f64 / steps as f64;
675 let a1 = sweep * (i + 1) as f64 / steps as f64;
676 push_tube(&mut tb, at(a0), at(a1), rod, TURN_RGB);
677 }
678 let end = at(sweep);
679 let tangent = normalize_or(sub3(at(sweep), at(sweep - 0.05)), u);
680 let head = [end[0] + tangent[0] * cone_len, end[1] + tangent[1] * cone_len, end[2] + tangent[2] * cone_len];
681 push_cone(&mut tb, end, head, cone_rad, TURN_RGB);
682 }
683 (tb.positions, tb.colors)
684}
685
686const SLIDE_RGB: [f32; 3] = [0.255, 0.624, 0.949];
688const TURN_RGB: [f32; 3] = [0.804, 0.380, 0.886];
689const GLYPH_SLIDE_HALF_PX: f64 = 48.0;
690const GLYPH_AXIS_HALF_PX: f64 = 60.0;
691const GLYPH_CURL_RAD_PX: f64 = 20.0;
692
693#[derive(Default)]
695struct TriBuf {
696 positions: Vec<f32>,
697 colors: Vec<f32>,
698}
699
700impl TriBuf {
701 fn tri(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], rgb: [f32; 3]) {
702 for p in [a, b, c] {
703 self.positions
704 .extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
705 self.colors.extend_from_slice(&rgb);
706 }
707 }
708}
709
710fn push_tube(tb: &mut TriBuf, a: [f64; 3], b: [f64; 3], radius: f64, rgb: [f32; 3]) {
712 let axis = sub3(b, a);
713 let len = norm3(axis);
714 if len < 1e-9 || radius <= 0.0 {
715 return;
716 }
717 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
718 let (u, v) = axis_basis(dir);
719 let ring = |center: [f64; 3], k: usize| -> [f64; 3] {
720 let ang = (k as f64 / TUBE_SEGMENTS as f64) * std::f64::consts::TAU;
721 let (c, s) = (ang.cos() * radius, ang.sin() * radius);
722 [
723 center[0] + u[0] * c + v[0] * s,
724 center[1] + u[1] * c + v[1] * s,
725 center[2] + u[2] * c + v[2] * s,
726 ]
727 };
728 for k in 0..TUBE_SEGMENTS {
729 let a0 = ring(a, k);
730 let a1 = ring(a, k + 1);
731 let b0 = ring(b, k);
732 let b1 = ring(b, k + 1);
733 tb.tri(a0, b0, b1, rgb);
734 tb.tri(a0, b1, a1, rgb);
735 }
736}
737
738fn push_cone(tb: &mut TriBuf, base: [f64; 3], tip: [f64; 3], radius: f64, rgb: [f32; 3]) {
741 let axis = sub3(tip, base);
742 let len = norm3(axis);
743 if len < 1e-9 || radius <= 0.0 {
744 return;
745 }
746 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
747 let (u, v) = axis_basis(dir);
748 let ring = |k: usize| -> [f64; 3] {
749 let ang = (k as f64 / CONE_SEGMENTS as f64) * std::f64::consts::TAU;
750 let (c, s) = (ang.cos() * radius, ang.sin() * radius);
751 [
752 base[0] + u[0] * c + v[0] * s,
753 base[1] + u[1] * c + v[1] * s,
754 base[2] + u[2] * c + v[2] * s,
755 ]
756 };
757 let mut prev = ring(0);
758 for k in 1..=CONE_SEGMENTS {
759 let cur = ring(k);
760 tb.tri(tip, prev, cur, rgb); tb.tri(base, cur, prev, rgb); prev = cur;
763 }
764}
765
766fn push_sphere(tb: &mut TriBuf, center: [f64; 3], radius: f64, rgb: [f32; 3]) {
768 if radius <= 0.0 {
769 return;
770 }
771 let point = |ring: usize, sector: usize| -> [f64; 3] {
772 let lat = std::f64::consts::PI * (ring as f64 / SPHERE_RINGS as f64)
773 - std::f64::consts::FRAC_PI_2;
774 let lon = std::f64::consts::TAU * (sector as f64 / SPHERE_SECTORS as f64);
775 [
776 center[0] + lat.cos() * lon.cos() * radius,
777 center[1] + lat.cos() * lon.sin() * radius,
778 center[2] + lat.sin() * radius,
779 ]
780 };
781 for r in 0..SPHERE_RINGS {
782 for sct in 0..SPHERE_SECTORS {
783 let p00 = point(r, sct);
784 let p01 = point(r, sct + 1);
785 let p10 = point(r + 1, sct);
786 let p11 = point(r + 1, sct + 1);
787 tb.tri(p00, p10, p11, rgb);
788 tb.tri(p00, p11, p01, rgb);
789 }
790 }
791}
792
793fn push_angle_gizmo(tb: &mut TriBuf, ann: &FeatureDimAnnotation, world_per_pixel: f64) {
800 let center = ann.center;
801 let axis = ann.axis;
802 let start = ann.ref_dir;
803 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
804 let ray_rad = ANGLE_RAY_RAD_PX * world_per_pixel;
805 let shaft_rad = SHAFT_RAD_PX * world_per_pixel;
806 let cone_len = CONE_LEN_PX * world_per_pixel;
807 let cone_rad = CONE_RAD_PX * world_per_pixel;
808 let sphere_rad = ORIGIN_SPHERE_RAD_PX * world_per_pixel;
809 if radius <= 1e-9 {
810 return;
811 }
812 let value = ann.value.clamp(-359.9, 359.9);
816 let value_rad = value.to_radians();
817
818 let arc_point = |t: f64| -> [f64; 3] {
822 let dir = rotate_about_axis(start, axis, t);
823 [
824 center[0] + dir[0] * radius,
825 center[1] + dir[1] * radius,
826 center[2] + dir[2] * radius,
827 ]
828 };
829 let seg_count = ((value.abs() / ARC_DEG_PER_SEG).ceil() as usize).max(2);
830 let mut prev = arc_point(0.0);
831 for k in 1..=seg_count {
832 let t = value_rad * (k as f64 / seg_count as f64);
833 let cur = arc_point(t);
834 push_tube(tb, prev, cur, shaft_rad, SHAFT_RGB);
835 prev = cur;
836 }
837
838 let dir_end = rotate_about_axis(start, axis, value_rad);
841 let end_pt = [
842 center[0] + dir_end[0] * radius,
843 center[1] + dir_end[1] * radius,
844 center[2] + dir_end[2] * radius,
845 ];
846 push_sphere(tb, end_pt, sphere_rad, ORANGE_RGB);
847 let sweep_sign = if value < 0.0 { -1.0 } else { 1.0 };
849 let tangent = normalize_or(cross3(axis, dir_end), dir_end);
850 let tangent = [tangent[0] * sweep_sign, tangent[1] * sweep_sign, tangent[2] * sweep_sign];
851 let cone_tip = [
852 end_pt[0] + tangent[0] * cone_len,
853 end_pt[1] + tangent[1] * cone_len,
854 end_pt[2] + tangent[2] * cone_len,
855 ];
856 push_cone(tb, end_pt, cone_tip, cone_rad, ORANGE_RGB);
857
858 let ref_end = [
861 center[0] + start[0] * radius,
862 center[1] + start[1] * radius,
863 center[2] + start[2] * radius,
864 ];
865 push_dashed(tb, center, ref_end, ray_rad, RED_RGB, world_per_pixel);
866
867 let axis_len = radius * 0.7;
869 let axis_a = [
870 center[0] - axis[0] * axis_len,
871 center[1] - axis[1] * axis_len,
872 center[2] - axis[2] * axis_len,
873 ];
874 let axis_b = [
875 center[0] + axis[0] * axis_len,
876 center[1] + axis[1] * axis_len,
877 center[2] + axis[2] * axis_len,
878 ];
879 push_tube(tb, axis_a, axis_b, ray_rad, GREEN_RGB);
880}
881
882fn push_dashed(
885 tb: &mut TriBuf,
886 a: [f64; 3],
887 b: [f64; 3],
888 radius: f64,
889 rgb: [f32; 3],
890 world_per_pixel: f64,
891) {
892 let axis = sub3(b, a);
893 let len = norm3(axis);
894 if len < 1e-9 {
895 return;
896 }
897 let dir = [axis[0] / len, axis[1] / len, axis[2] / len];
898 let dash = (DASH_LEN_PX * world_per_pixel).max(1e-6);
899 let gap = (DASH_GAP_PX * world_per_pixel).max(1e-6);
900 let mut s = 0.0;
901 while s < len {
902 let e = (s + dash).min(len);
903 let p0 = [a[0] + dir[0] * s, a[1] + dir[1] * s, a[2] + dir[2] * s];
904 let p1 = [a[0] + dir[0] * e, a[1] + dir[1] * e, a[2] + dir[2] * e];
905 push_tube(tb, p0, p1, radius, rgb);
906 s = e + gap;
907 }
908}
909
910pub fn angular_chip_anchor(ann: &FeatureDimAnnotation, world_per_pixel: f64) -> [f64; 3] {
915 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
916 let value = ann.value.clamp(-359.9, 359.9);
917 let bisector = rotate_about_axis(ann.ref_dir, ann.axis, (value * 0.5).to_radians());
918 [
919 ann.center[0] + bisector[0] * radius,
920 ann.center[1] + bisector[1] * radius,
921 ann.center[2] + bisector[2] * radius,
922 ]
923}
924
925pub(crate) const ARROW_HANDLE_HIT_RAD_PX: f64 = CONE_RAD_PX + 12.0;
932
933pub(crate) fn arrow_handle_point(
940 ann: &FeatureDimAnnotation,
941 world_per_pixel: f64,
942) -> [f64; 3] {
943 match ann.kind {
944 FeatureDimKind::Linear => ann.point_b,
945 FeatureDimKind::Angular => {
946 let radius = ANGLE_ARC_RAD_PX * world_per_pixel;
947 let value = ann.value.clamp(-359.9, 359.9);
948 let dir = rotate_about_axis(ann.ref_dir, ann.axis, value.to_radians());
949 [
950 ann.center[0] + dir[0] * radius,
951 ann.center[1] + dir[1] * radius,
952 ann.center[2] + dir[2] * radius,
953 ]
954 }
955 }
956}
957
958fn normalize_or(v: [f64; 3], fallback: [f64; 3]) -> [f64; 3] {
960 let n = norm3(v);
961 if n < 1e-12 {
962 fallback
963 } else {
964 [v[0] / n, v[1] / n, v[2] / n]
965 }
966}
967
968fn arbitrary_perpendicular(direction: [f64; 3]) -> [f64; 3] {
970 if norm3(direction) <= 1e-12 {
971 return [0.0, 0.0, 1.0];
972 }
973 let seed = if dot3(direction, [0.0, 0.0, 1.0]).abs() < 0.9 {
974 [0.0, 0.0, 1.0]
975 } else {
976 [0.0, 1.0, 0.0]
977 };
978 let mut perp = cross3(direction, seed);
979 if norm3(perp) <= 1e-12 {
980 perp = cross3(direction, [1.0, 0.0, 0.0]);
981 }
982 if norm3(perp) <= 1e-12 {
983 [1.0, 0.0, 0.0]
984 } else {
985 normalize_or(perp, [1.0, 0.0, 0.0])
986 }
987}
988
989pub fn rotate_about_axis(v: [f64; 3], axis: [f64; 3], angle: f64) -> [f64; 3] {
992 crate::geometry3d::rotate3(v, normalize_or(axis, [0.0, 1.0, 0.0]), angle)
993}
994
995fn axis_basis(dir: [f64; 3]) -> ([f64; 3], [f64; 3]) {
997 let seed = if dir[0].abs() < 0.9 {
998 [1.0, 0.0, 0.0]
999 } else {
1000 [0.0, 1.0, 0.0]
1001 };
1002 let mut u = cross3(dir, seed);
1003 let un = norm3(u);
1004 if un < 1e-9 {
1005 u = [0.0, 1.0, 0.0];
1006 } else {
1007 u = [u[0] / un, u[1] / un, u[2] / un];
1008 }
1009 let v = cross3(dir, u);
1010 let vn = norm3(v).max(1e-9);
1011 (u, [v[0] / vn, v[1] / vn, v[2] / vn])
1012}
1013