Another project
1use bone_render::{EdgeScene, GenuineEdge, PickId, SolidScene, Style};
2use bone_types::{
3 Angle, AxisAngle, Camera3, CreaseAngle, Length, LinearRgba, Point3, Projection, Tolerance,
4 UnitVec3, millimeter, radian,
5};
6
7const TOL: Tolerance = Tolerance::new(1.0e-9);
8const RIGHT_ANGLE: CreaseAngle = CreaseAngle::from_radians(core::f64::consts::FRAC_PI_2);
9const CYL_SEGMENTS: u32 = 20;
10const TUBE_SEGMENTS: u32 = 14;
11const SPHERE_LON: u32 = 20;
12const SPHERE_LAT: u32 = 14;
13
14type V3 = [f64; 3];
15
16#[derive(Copy, Clone, Debug, PartialEq)]
17pub struct IconMaterial {
18 pub color: LinearRgba,
19}
20
21impl IconMaterial {
22 #[must_use]
23 pub const fn new(color: LinearRgba) -> Self {
24 Self { color }
25 }
26}
27
28#[derive(Copy, Clone, Debug, PartialEq)]
29pub struct Rotation {
30 axis: UnitVec3,
31 radians: f64,
32}
33
34impl Rotation {
35 #[must_use]
36 pub fn identity() -> Self {
37 Self {
38 axis: UnitVec3::z_axis(),
39 radians: 0.0,
40 }
41 }
42
43 #[must_use]
44 pub fn about(axis: UnitVec3, angle: Angle) -> Self {
45 Self {
46 axis,
47 radians: angle.get::<radian>(),
48 }
49 }
50
51 #[must_use]
52 pub fn about_x(angle: Angle) -> Self {
53 Self::about(UnitVec3::x_axis(), angle)
54 }
55
56 #[must_use]
57 pub fn about_y(angle: Angle) -> Self {
58 Self::about(UnitVec3::y_axis(), angle)
59 }
60
61 #[must_use]
62 pub fn about_z(angle: Angle) -> Self {
63 Self::about(UnitVec3::z_axis(), angle)
64 }
65
66 fn axis_angle(self) -> AxisAngle {
67 AxisAngle::new(self.axis, Angle::new::<radian>(self.radians))
68 }
69
70 fn rotate_offset(self, o: V3) -> V3 {
71 let rotated =
72 Point3::from_mm(o[0], o[1], o[2]).rotated_about(Point3::origin(), self.axis_angle());
73 let (x, y, z) = rotated.coords_mm();
74 [x, y, z]
75 }
76
77 fn rotate_unit(self, u: UnitVec3) -> UnitVec3 {
78 u.rotated(self.axis_angle())
79 }
80}
81
82#[derive(Clone, Debug, PartialEq)]
83pub enum Prim {
84 Box {
85 center: [f32; 3],
86 half: [f32; 3],
87 rotation: Rotation,
88 material: IconMaterial,
89 },
90 Cylinder {
91 from: [f32; 3],
92 to: [f32; 3],
93 radius: f32,
94 material: IconMaterial,
95 },
96 Sphere {
97 center: [f32; 3],
98 radius: f32,
99 material: IconMaterial,
100 },
101 SweptTube {
102 path: Vec<[f32; 3]>,
103 radius: f32,
104 closed: bool,
105 material: IconMaterial,
106 },
107 Arrow {
108 from: [f32; 3],
109 to: [f32; 3],
110 shaft_radius: f32,
111 head_radius: f32,
112 head_length: f32,
113 material: IconMaterial,
114 },
115}
116
117impl Prim {
118 #[must_use]
119 pub fn cuboid(center: [f32; 3], half: [f32; 3], material: IconMaterial) -> Self {
120 Self::Box {
121 center,
122 half,
123 rotation: Rotation::identity(),
124 material,
125 }
126 }
127
128 #[must_use]
129 pub fn tilted_cuboid(
130 center: [f32; 3],
131 half: [f32; 3],
132 rotation: Rotation,
133 material: IconMaterial,
134 ) -> Self {
135 Self::Box {
136 center,
137 half,
138 rotation,
139 material,
140 }
141 }
142
143 #[must_use]
144 pub fn bar(from: [f32; 3], to: [f32; 3], radius: f32, material: IconMaterial) -> Self {
145 Self::Cylinder {
146 from,
147 to,
148 radius,
149 material,
150 }
151 }
152
153 #[must_use]
154 pub fn ball(center: [f32; 3], radius: f32, material: IconMaterial) -> Self {
155 Self::Sphere {
156 center,
157 radius,
158 material,
159 }
160 }
161
162 #[must_use]
163 pub fn loop_tube(path: Vec<[f32; 3]>, radius: f32, material: IconMaterial) -> Self {
164 Self::SweptTube {
165 path,
166 radius,
167 closed: true,
168 material,
169 }
170 }
171
172 #[must_use]
173 pub fn open_tube(path: Vec<[f32; 3]>, radius: f32, material: IconMaterial) -> Self {
174 Self::SweptTube {
175 path,
176 radius,
177 closed: false,
178 material,
179 }
180 }
181
182 #[must_use]
183 pub fn arrow(
184 from: [f32; 3],
185 to: [f32; 3],
186 shaft_radius: f32,
187 head_radius: f32,
188 head_length: f32,
189 material: IconMaterial,
190 ) -> Self {
191 Self::Arrow {
192 from,
193 to,
194 shaft_radius,
195 head_radius,
196 head_length,
197 material,
198 }
199 }
200
201 fn emit(&self, mesh: &mut MeshBuilder) {
202 match self {
203 Prim::Box {
204 center,
205 half,
206 rotation,
207 material,
208 } => emit_cuboid(mesh, *center, *half, *rotation, *material),
209 Prim::Cylinder {
210 from,
211 to,
212 radius,
213 material,
214 } => emit_cylinder(mesh, *from, *to, *radius, *material),
215 Prim::Sphere {
216 center,
217 radius,
218 material,
219 } => emit_sphere(mesh, *center, *radius, *material),
220 Prim::SweptTube {
221 path,
222 radius,
223 closed,
224 material,
225 } => emit_swept_tube(mesh, path, *radius, *closed, *material),
226 Prim::Arrow {
227 from,
228 to,
229 shaft_radius,
230 head_radius,
231 head_length,
232 material,
233 } => emit_arrow(
234 mesh,
235 *from,
236 *to,
237 *shaft_radius,
238 *head_radius,
239 *head_length,
240 *material,
241 ),
242 }
243 }
244}
245
246#[derive(Copy, Clone, Debug, PartialEq, Eq)]
247pub enum IconView {
248 Iso,
249 Front,
250}
251
252#[derive(Clone, Debug, PartialEq)]
253pub struct IconModel {
254 prims: Vec<Prim>,
255 view: IconView,
256}
257
258impl IconModel {
259 #[must_use]
260 pub fn new(prims: Vec<Prim>) -> Self {
261 Self {
262 prims,
263 view: IconView::Iso,
264 }
265 }
266
267 #[must_use]
268 pub fn with_view(mut self, view: IconView) -> Self {
269 self.view = view;
270 self
271 }
272
273 #[must_use]
274 pub fn view(&self) -> IconView {
275 self.view
276 }
277
278 #[must_use]
279 pub fn tessellate(&self) -> (SolidScene, EdgeScene) {
280 let mesh = self
281 .prims
282 .iter()
283 .fold(MeshBuilder::default(), |mut mesh, prim| {
284 prim.emit(&mut mesh);
285 mesh
286 });
287 let scene =
288 SolidScene::from_parts(mesh.positions, mesh.normals, mesh.colors, mesh.triangles);
289 let edges = EdgeScene::from_genuine(mesh.edges);
290 (scene, edges)
291 }
292}
293
294#[derive(Default)]
295struct MeshBuilder {
296 positions: Vec<Point3>,
297 normals: Vec<UnitVec3>,
298 colors: Vec<LinearRgba>,
299 triangles: Vec<[u32; 3]>,
300 edges: Vec<GenuineEdge>,
301}
302
303impl MeshBuilder {
304 fn base(&self) -> u32 {
305 let Ok(base) = u32::try_from(self.positions.len()) else {
306 panic!("icon mesh vertex count fits a u32 index");
307 };
308 base
309 }
310
311 fn push_quad(&mut self, corners: [Point3; 4], normal: UnitVec3, color: LinearRgba) {
312 self.push_quad_smooth(corners, [normal; 4], color);
313 }
314
315 fn push_quad_smooth(
316 &mut self,
317 corners: [Point3; 4],
318 normals: [UnitVec3; 4],
319 color: LinearRgba,
320 ) {
321 let base = self.base();
322 self.positions.extend_from_slice(&corners);
323 self.normals.extend_from_slice(&normals);
324 (0..4).for_each(|_| self.colors.push(color));
325 self.triangles.push([base, base + 1, base + 2]);
326 self.triangles.push([base, base + 2, base + 3]);
327 }
328
329 fn push_tri(&mut self, corners: [Point3; 3], normals: [UnitVec3; 3], color: LinearRgba) {
330 let base = self.base();
331 self.positions.extend_from_slice(&corners);
332 self.normals.extend_from_slice(&normals);
333 (0..3).for_each(|_| self.colors.push(color));
334 self.triangles.push([base, base + 1, base + 2]);
335 }
336
337 fn push_edge(&mut self, a: Point3, b: Point3) {
338 self.edges
339 .push(GenuineEdge::new(a, b, PickId::NONE, RIGHT_ANGLE));
340 }
341
342 fn push_loop_edges(&mut self, ring: &[Point3]) {
343 ring.iter().enumerate().for_each(|(i, &a)| {
344 let b = ring[(i + 1) % ring.len()];
345 self.push_edge(a, b);
346 });
347 }
348}
349
350fn sub(a: V3, b: V3) -> V3 {
351 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
352}
353
354fn add(a: V3, b: V3) -> V3 {
355 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
356}
357
358fn scale(a: V3, s: f64) -> V3 {
359 [a[0] * s, a[1] * s, a[2] * s]
360}
361
362fn dot(a: V3, b: V3) -> f64 {
363 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
364}
365
366fn cross(a: V3, b: V3) -> V3 {
367 [
368 a[1] * b[2] - a[2] * b[1],
369 a[2] * b[0] - a[0] * b[2],
370 a[0] * b[1] - a[1] * b[0],
371 ]
372}
373
374fn length(a: V3) -> f64 {
375 dot(a, a).sqrt()
376}
377
378fn normalize(a: V3) -> V3 {
379 let n = length(a);
380 if n <= f64::EPSILON {
381 [0.0, 0.0, 1.0]
382 } else {
383 scale(a, 1.0 / n)
384 }
385}
386
387fn vf(a: [f32; 3]) -> V3 {
388 [f64::from(a[0]), f64::from(a[1]), f64::from(a[2])]
389}
390
391fn pt(v: V3) -> Point3 {
392 Point3::from_mm(v[0], v[1], v[2])
393}
394
395fn unit(v: V3) -> UnitVec3 {
396 let n = normalize(v);
397 UnitVec3::new_unchecked(n[0], n[1], n[2])
398}
399
400fn perpendicular_frame(dir: V3) -> (V3, V3) {
401 let helper = if dir[1].abs() < 0.9 {
402 [0.0, 1.0, 0.0]
403 } else {
404 [1.0, 0.0, 0.0]
405 };
406 let u = normalize(cross(helper, dir));
407 let v = cross(dir, u);
408 (u, v)
409}
410
411fn ring_points(center: V3, u: V3, v: V3, radius: f64, segments: u32) -> Vec<V3> {
412 (0..segments)
413 .map(|i| {
414 let theta = core::f64::consts::TAU * f64::from(i) / f64::from(segments);
415 add(
416 center,
417 add(
418 scale(u, radius * theta.cos()),
419 scale(v, radius * theta.sin()),
420 ),
421 )
422 })
423 .collect()
424}
425
426fn emit_cuboid(
427 mesh: &mut MeshBuilder,
428 center: [f32; 3],
429 half: [f32; 3],
430 rotation: Rotation,
431 material: IconMaterial,
432) {
433 let c = vf(center);
434 let h = vf(half);
435 let place = |signs: [f64; 3]| -> Point3 {
436 let local = [signs[0] * h[0], signs[1] * h[1], signs[2] * h[2]];
437 pt(add(c, rotation.rotate_offset(local)))
438 };
439 (0..3).for_each(|axis| {
440 let u = (axis + 1) % 3;
441 let v = (axis + 2) % 3;
442 [1.0_f64, -1.0_f64].into_iter().for_each(|sign| {
443 let mut axis_dir = [0.0_f64; 3];
444 axis_dir[axis] = sign;
445 let normal = rotation.rotate_unit(unit(axis_dir));
446 let at = |du: f64, dv: f64| {
447 let mut signs = [0.0_f64; 3];
448 signs[axis] = sign;
449 signs[u] = du;
450 signs[v] = dv;
451 place(signs)
452 };
453 let p00 = at(-1.0, -1.0);
454 let p10 = at(1.0, -1.0);
455 let p11 = at(1.0, 1.0);
456 let p01 = at(-1.0, 1.0);
457 let quad = if sign > 0.0 {
458 [p00, p10, p11, p01]
459 } else {
460 [p00, p01, p11, p10]
461 };
462 mesh.push_quad(quad, normal, material.color);
463 });
464 });
465 emit_cuboid_edges(mesh, place);
466}
467
468fn emit_cuboid_edges(mesh: &mut MeshBuilder, place: impl Fn([f64; 3]) -> Point3) {
469 (0..3).for_each(|axis| {
470 let u = (axis + 1) % 3;
471 let v = (axis + 2) % 3;
472 [-1.0_f64, 1.0_f64].into_iter().for_each(|su| {
473 [-1.0_f64, 1.0_f64].into_iter().for_each(|sv| {
474 let mut lo = [0.0_f64; 3];
475 lo[u] = su;
476 lo[v] = sv;
477 lo[axis] = -1.0;
478 let mut hi = lo;
479 hi[axis] = 1.0;
480 mesh.push_edge(place(lo), place(hi));
481 });
482 });
483 });
484}
485
486fn emit_tube_band(
487 mesh: &mut MeshBuilder,
488 ring_a: &[V3],
489 ring_b: &[V3],
490 center_a: V3,
491 center_b: V3,
492 color: LinearRgba,
493) {
494 let n = ring_a.len();
495 (0..n).for_each(|i| {
496 let j = (i + 1) % n;
497 let a0 = ring_a[i];
498 let a1 = ring_a[j];
499 let b0 = ring_b[i];
500 let b1 = ring_b[j];
501 let na0 = unit(sub(a0, center_a));
502 let na1 = unit(sub(a1, center_a));
503 let nb0 = unit(sub(b0, center_b));
504 let nb1 = unit(sub(b1, center_b));
505 mesh.push_quad_smooth(
506 [pt(a0), pt(a1), pt(b1), pt(b0)],
507 [na0, na1, nb1, nb0],
508 color,
509 );
510 });
511}
512
513fn emit_cap(mesh: &mut MeshBuilder, ring: &[V3], center: V3, normal: UnitVec3, color: LinearRgba) {
514 let n = ring.len();
515 let (nx, ny, nz) = normal.components();
516 let default_dir = cross(sub(ring[0], center), sub(ring[1], center));
517 let flip = dot(default_dir, [nx, ny, nz]) < 0.0;
518 (0..n).for_each(|i| {
519 let j = (i + 1) % n;
520 let (a, b) = if flip {
521 (ring[j], ring[i])
522 } else {
523 (ring[i], ring[j])
524 };
525 mesh.push_tri([pt(center), pt(a), pt(b)], [normal; 3], color);
526 });
527}
528
529fn emit_cylinder(
530 mesh: &mut MeshBuilder,
531 from: [f32; 3],
532 to: [f32; 3],
533 radius: f32,
534 material: IconMaterial,
535) {
536 let p0 = vf(from);
537 let p1 = vf(to);
538 let dir = normalize(sub(p1, p0));
539 let (u, v) = perpendicular_frame(dir);
540 let r = f64::from(radius);
541 let ring0 = ring_points(p0, u, v, r, CYL_SEGMENTS);
542 let ring1 = ring_points(p1, u, v, r, CYL_SEGMENTS);
543 emit_tube_band(mesh, &ring0, &ring1, p0, p1, material.color);
544 emit_cap(mesh, &ring0, p0, unit(scale(dir, -1.0)), material.color);
545 emit_cap(mesh, &ring1, p1, unit(dir), material.color);
546 mesh.push_loop_edges(&ring0.iter().map(|&p| pt(p)).collect::<Vec<_>>());
547 mesh.push_loop_edges(&ring1.iter().map(|&p| pt(p)).collect::<Vec<_>>());
548}
549
550fn emit_sphere(mesh: &mut MeshBuilder, center: [f32; 3], radius: f32, material: IconMaterial) {
551 let c = vf(center);
552 let r = f64::from(radius);
553 let lat = SPHERE_LAT;
554 let lon = SPHERE_LON;
555 let point = |i: u32, j: u32| -> (V3, UnitVec3) {
556 let phi = core::f64::consts::PI * f64::from(i) / f64::from(lat);
557 let theta = core::f64::consts::TAU * f64::from(j) / f64::from(lon);
558 let n = [phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin()];
559 (add(c, scale(n, r)), unit(n))
560 };
561 (0..lat).for_each(|i| {
562 (0..lon).for_each(|j| {
563 let (p00, n00) = point(i, j);
564 let (p01, n01) = point(i, j + 1);
565 let (p10, n10) = point(i + 1, j);
566 let (p11, n11) = point(i + 1, j + 1);
567 mesh.push_quad_smooth(
568 [pt(p00), pt(p01), pt(p11), pt(p10)],
569 [n00, n01, n11, n10],
570 material.color,
571 );
572 });
573 });
574}
575
576fn path_normal(path: &[V3]) -> V3 {
577 let n = path.len();
578 let newell = (0..n).fold([0.0_f64; 3], |acc, i| {
579 let a = path[i];
580 let b = path[(i + 1) % n];
581 [
582 acc[0] + (a[1] - b[1]) * (a[2] + b[2]),
583 acc[1] + (a[2] - b[2]) * (a[0] + b[0]),
584 acc[2] + (a[0] - b[0]) * (a[1] + b[1]),
585 ]
586 });
587 if length(newell) <= f64::EPSILON {
588 let dir = normalize(sub(path[1.min(n - 1)], path[0]));
589 let (u, _) = perpendicular_frame(dir);
590 u
591 } else {
592 normalize(newell)
593 }
594}
595
596fn tangent_at(path: &[V3], i: usize, closed: bool) -> V3 {
597 let n = path.len();
598 let prev = if i == 0 {
599 if closed { Some(path[n - 1]) } else { None }
600 } else {
601 Some(path[i - 1])
602 };
603 let next = if i + 1 == n {
604 if closed { Some(path[0]) } else { None }
605 } else {
606 Some(path[i + 1])
607 };
608 match (prev, next) {
609 (Some(a), Some(b)) => normalize(sub(b, a)),
610 (Some(a), None) => normalize(sub(path[i], a)),
611 (None, Some(b)) => normalize(sub(b, path[i])),
612 (None, None) => [0.0, 0.0, 1.0],
613 }
614}
615
616fn emit_swept_tube(
617 mesh: &mut MeshBuilder,
618 path: &[[f32; 3]],
619 radius: f32,
620 closed: bool,
621 material: IconMaterial,
622) {
623 if path.len() < 2 {
624 return;
625 }
626 let points: Vec<V3> = path.iter().map(|&p| vf(p)).collect();
627 let normal = path_normal(&points);
628 let r = f64::from(radius);
629 let rings: Vec<Vec<V3>> = points
630 .iter()
631 .enumerate()
632 .map(|(i, ¢er)| {
633 let tangent = tangent_at(&points, i, closed);
634 let in_plane = normalize(cross(tangent, normal));
635 ring_points(center, in_plane, normal, r, TUBE_SEGMENTS)
636 })
637 .collect();
638 let bands = if closed {
639 points.len()
640 } else {
641 points.len() - 1
642 };
643 (0..bands).for_each(|i| {
644 let j = (i + 1) % points.len();
645 emit_tube_band(
646 mesh,
647 &rings[i],
648 &rings[j],
649 points[i],
650 points[j],
651 material.color,
652 );
653 });
654 if !closed {
655 let last = points.len() - 1;
656 let t0 = tangent_at(&points, 0, closed);
657 let t1 = tangent_at(&points, last, closed);
658 emit_cap(
659 mesh,
660 &rings[0],
661 points[0],
662 unit(scale(t0, -1.0)),
663 material.color,
664 );
665 emit_cap(mesh, &rings[last], points[last], unit(t1), material.color);
666 }
667}
668
669fn emit_arrow(
670 mesh: &mut MeshBuilder,
671 from: [f32; 3],
672 to: [f32; 3],
673 shaft_radius: f32,
674 head_radius: f32,
675 head_length: f32,
676 material: IconMaterial,
677) {
678 let p0 = vf(from);
679 let tip = vf(to);
680 let dir = normalize(sub(tip, p0));
681 let head_len = f64::from(head_length);
682 let base = sub(tip, scale(dir, head_len));
683 let (frame_u, frame_v) = perpendicular_frame(dir);
684 let shaft_r = f64::from(shaft_radius);
685 let shaft0 = ring_points(p0, frame_u, frame_v, shaft_r, CYL_SEGMENTS);
686 let shaft1 = ring_points(base, frame_u, frame_v, shaft_r, CYL_SEGMENTS);
687 emit_tube_band(mesh, &shaft0, &shaft1, p0, base, material.color);
688 emit_cap(mesh, &shaft0, p0, unit(scale(dir, -1.0)), material.color);
689 let head_r = f64::from(head_radius);
690 let base_ring = ring_points(base, frame_u, frame_v, head_r, CYL_SEGMENTS);
691 emit_cap(
692 mesh,
693 &base_ring,
694 base,
695 unit(scale(dir, -1.0)),
696 material.color,
697 );
698 let count = base_ring.len();
699 (0..count).for_each(|i| {
700 let j = (i + 1) % count;
701 let cap_a = base_ring[i];
702 let cap_b = base_ring[j];
703 let na = unit(add(scale(dir, head_r), sub(cap_a, base)));
704 let nb = unit(add(scale(dir, head_r), sub(cap_b, base)));
705 let nt = unit(dir);
706 mesh.push_tri(
707 [pt(cap_a), pt(cap_b), pt(tip)],
708 [na, nb, nt],
709 material.color,
710 );
711 });
712 mesh.push_loop_edges(&base_ring.iter().map(|&p| pt(p)).collect::<Vec<_>>());
713}
714
715#[must_use]
716pub fn icon_camera(view: IconView) -> Camera3 {
717 match view {
718 IconView::Iso => iso_camera(),
719 IconView::Front => front_camera(),
720 }
721}
722
723fn iso_camera() -> Camera3 {
724 let target = Point3::origin();
725 let Ok(direction) = UnitVec3::try_from_components(1.0, 0.8, 1.0, TOL) else {
726 panic!("icon camera direction is nonzero");
727 };
728 let eye = target + direction.into_vec(Length::new::<millimeter>(10.0));
729 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(1.5)) else {
730 panic!("icon camera half-height is positive");
731 };
732 let Ok(camera) = Camera3::new(eye, target, UnitVec3::y_axis(), projection) else {
733 panic!("icon camera eye and target are 10 mm apart");
734 };
735 camera
736}
737
738fn front_camera() -> Camera3 {
739 let target = Point3::origin();
740 let eye = target + UnitVec3::z_axis().into_vec(Length::new::<millimeter>(10.0));
741 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(1.3)) else {
742 panic!("icon front camera half-height is positive");
743 };
744 let Ok(camera) = Camera3::new(eye, target, UnitVec3::y_axis(), projection) else {
745 panic!("icon front camera eye and target are 10 mm apart");
746 };
747 camera
748}
749
750#[must_use]
751pub fn icon_style() -> Style {
752 Style::light().with_solid_base_color(LinearRgba::new(1.0, 1.0, 1.0, 1.0))
753}