Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 20 kB View raw
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, &center)| { 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}