Another project
0

Configure Feed

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

feat(render): edge scene extract, solid occluder fill

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 5, 2026, 8:54 PM +0300) commit f48fd745 parent 66dc5bcd change-id xvqvssuu
+268 -6
+23 -2
crates/bone-render/src/pipelines/solid.rs
··· 17 17 const SHADING_DEFAULT: u32 = 0; 18 18 const SHADING_PHONG: u32 = 1; 19 19 20 + const FACE_SHADED: u32 = 0; 21 + const FACE_OCCLUDER: u32 = 1; 22 + 20 23 const fn shading_code(model: ShadingModel) -> u32 { 21 24 match model { 22 25 ShadingModel::Phong => SHADING_PHONG, ··· 24 27 } 25 28 } 26 29 30 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 31 + pub enum FaceFill { 32 + Shaded, 33 + Occluder, 34 + } 35 + 36 + const fn fill_code(fill: FaceFill) -> u32 { 37 + match fill { 38 + FaceFill::Shaded => FACE_SHADED, 39 + FaceFill::Occluder => FACE_OCCLUDER, 40 + } 41 + } 42 + 27 43 #[derive(Copy, Clone)] 28 44 pub struct SolidView { 29 45 pub clip_from_world: [f32; 16], 30 46 pub eye_world: [f32; 3], 31 47 pub shading: ShadingModel, 48 + pub fill: FaceFill, 32 49 } 33 50 34 51 #[repr(C)] ··· 47 64 clip_from_world: [f32; 16], 48 65 light_dir: [f32; 4], 49 66 base_color: [f32; 4], 67 + background: [f32; 4], 50 68 eye_world: [f32; 4], 51 69 ambient: f32, 52 70 shading_model: u32, 53 - pad: [f32; 2], 71 + face_mode: u32, 72 + pad: f32, 54 73 } 55 74 56 75 const UNIFORM_SIZE: u64 = core::mem::size_of::<SolidUniform>() as u64; ··· 182 201 clip_from_world: view.clip_from_world, 183 202 light_dir: LIGHT_DIR, 184 203 base_color: BASE_COLOR, 204 + background: style.background().to_rgba_array(), 185 205 eye_world: [view.eye_world[0], view.eye_world[1], view.eye_world[2], 1.0], 186 206 ambient: AMBIENT, 187 207 shading_model: shading_code(view.shading), 188 - pad: [0.0; 2], 208 + face_mode: fill_code(view.fill), 209 + pad: 0.0, 189 210 }; 190 211 self.queue 191 212 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
+10 -1
crates/bone-render/src/pipelines/solid.wgsl
··· 2 2 clip_from_world: mat4x4<f32>, 3 3 light_dir: vec4<f32>, 4 4 base_color: vec4<f32>, 5 + background: vec4<f32>, 5 6 eye_world: vec4<f32>, 6 7 ambient: f32, 7 8 shading_model: u32, 9 + face_mode: u32, 8 10 pad0: f32, 9 - pad1: f32, 10 11 }; 11 12 12 13 const SHADING_PHONG: u32 = 1u; 14 + const FACE_OCCLUDER: u32 = 1u; 13 15 const SPECULAR_STRENGTH: f32 = 0.35; 14 16 const SHININESS: f32 = 48.0; 15 17 ··· 43 45 44 46 @fragment 45 47 fn fs(in: VsOut) -> FsOut { 48 + if (u.face_mode == FACE_OCCLUDER) { 49 + var occluded: FsOut; 50 + occluded.color = u.background; 51 + occluded.pick_id = in.pick_id; 52 + return occluded; 53 + } 54 + 46 55 let n = normalize(in.world_normal); 47 56 let l = normalize(u.light_dir.xyz); 48 57 let ndl = dot(n, l);
+235 -3
crates/bone-render/src/scene.rs
··· 2 2 ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity, 3 3 SketchRelation, 4 4 }; 5 - use bone_kernel::{Aabb2, Arc2, SolidMesh, arc_bounding_box}; 5 + use bone_kernel::{Aabb2, Arc2, BrepSolid, FaceMesh, SolidMesh, arc_bounding_box}; 6 6 use bone_types::{ 7 - Angle, Length, Point2, Point3, SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, 8 - UnitVec3, Vec2, 7 + Angle, ChordHeightTolerance, CreaseAngle, Length, Point2, Point3, SketchDimensionId, 8 + SketchEntityId, SketchRelationId, Tolerance, UnitVec3, Vec2, 9 9 }; 10 10 use core::f64::consts::FRAC_1_SQRT_2; 11 + use std::collections::BTreeMap; 11 12 use uom::si::angle::degree; 12 13 use uom::si::length::millimeter; 13 14 ··· 676 677 pub fn triangles(&self) -> &[[u32; 3]] { 677 678 &self.triangles 678 679 } 680 + } 681 + 682 + const NORMAL_TOLERANCE: Tolerance = Tolerance::new(1.0e-12); 683 + const COPLANAR_DOT: f64 = 1.0 - 1.0e-9; 684 + 685 + #[derive(Copy, Clone, Debug, PartialEq)] 686 + pub struct GenuineEdge { 687 + a: Point3, 688 + b: Point3, 689 + pick: PickId, 690 + crease: CreaseAngle, 691 + } 692 + 693 + impl GenuineEdge { 694 + #[must_use] 695 + pub const fn a(self) -> Point3 { 696 + self.a 697 + } 698 + 699 + #[must_use] 700 + pub const fn b(self) -> Point3 { 701 + self.b 702 + } 703 + 704 + #[must_use] 705 + pub const fn pick(self) -> PickId { 706 + self.pick 707 + } 708 + 709 + #[must_use] 710 + pub const fn crease(self) -> CreaseAngle { 711 + self.crease 712 + } 713 + } 714 + 715 + #[derive(Copy, Clone, Debug, PartialEq)] 716 + pub struct SilhouetteCandidate { 717 + a: Point3, 718 + b: Point3, 719 + normal_a: UnitVec3, 720 + normal_b: UnitVec3, 721 + pick: PickId, 722 + } 723 + 724 + impl SilhouetteCandidate { 725 + #[must_use] 726 + pub const fn a(self) -> Point3 { 727 + self.a 728 + } 729 + 730 + #[must_use] 731 + pub const fn pick(self) -> PickId { 732 + self.pick 733 + } 734 + 735 + #[must_use] 736 + pub const fn b(self) -> Point3 { 737 + self.b 738 + } 739 + 740 + #[must_use] 741 + pub const fn normal_a(self) -> UnitVec3 { 742 + self.normal_a 743 + } 744 + 745 + #[must_use] 746 + pub const fn normal_b(self) -> UnitVec3 { 747 + self.normal_b 748 + } 749 + } 750 + 751 + #[derive(Clone, Debug, Default, PartialEq)] 752 + pub struct EdgeScene { 753 + genuine: Vec<GenuineEdge>, 754 + silhouettes: Vec<SilhouetteCandidate>, 755 + } 756 + 757 + impl EdgeScene { 758 + #[must_use] 759 + pub const fn empty() -> Self { 760 + Self { 761 + genuine: Vec::new(), 762 + silhouettes: Vec::new(), 763 + } 764 + } 765 + 766 + pub fn from_solid( 767 + solid: &BrepSolid, 768 + mesh: &SolidMesh, 769 + chord: ChordHeightTolerance, 770 + ) -> Result<Self, PickIdError> { 771 + let genuine = 772 + solid 773 + .edges_for_render(chord) 774 + .iter() 775 + .try_fold(Vec::new(), |mut acc, polyline| { 776 + let pick = PickId::brep_edge(polyline.edge())?; 777 + let crease = polyline.crease(); 778 + acc.extend(polyline.points().windows(2).map(|pair| GenuineEdge { 779 + a: pair[0], 780 + b: pair[1], 781 + pick, 782 + crease, 783 + })); 784 + Ok::<_, PickIdError>(acc) 785 + })?; 786 + let silhouettes = mesh.faces().iter().try_fold(Vec::new(), |mut acc, slab| { 787 + let pick = PickId::brep_face(slab.face())?; 788 + acc.extend(slab_silhouettes(slab, pick)); 789 + Ok::<_, PickIdError>(acc) 790 + })?; 791 + Ok(Self { 792 + genuine, 793 + silhouettes, 794 + }) 795 + } 796 + 797 + #[must_use] 798 + pub fn genuine(&self) -> &[GenuineEdge] { 799 + &self.genuine 800 + } 801 + 802 + #[must_use] 803 + pub fn silhouettes(&self) -> &[SilhouetteCandidate] { 804 + &self.silhouettes 805 + } 806 + 807 + #[must_use] 808 + pub fn is_empty(&self) -> bool { 809 + self.genuine.is_empty() && self.silhouettes.is_empty() 810 + } 811 + } 812 + 813 + struct EdgeAdjacency { 814 + a: Point3, 815 + b: Point3, 816 + normals: [Option<UnitVec3>; 2], 817 + incidence: usize, 818 + } 819 + 820 + impl EdgeAdjacency { 821 + const fn new(a: Point3, b: Point3) -> Self { 822 + Self { 823 + a, 824 + b, 825 + normals: [None, None], 826 + incidence: 0, 827 + } 828 + } 829 + 830 + fn record(mut self, normal: UnitVec3) -> Self { 831 + if self.incidence < 2 { 832 + self.normals[self.incidence] = Some(normal); 833 + } 834 + self.incidence += 1; 835 + self 836 + } 837 + 838 + fn into_candidate(self, pick: PickId) -> Option<SilhouetteCandidate> { 839 + match (self.incidence, self.normals) { 840 + (2, [Some(normal_a), Some(normal_b)]) if normal_a.dot(normal_b) < COPLANAR_DOT => { 841 + Some(SilhouetteCandidate { 842 + a: self.a, 843 + b: self.b, 844 + normal_a, 845 + normal_b, 846 + pick, 847 + }) 848 + } 849 + _ => None, 850 + } 851 + } 852 + } 853 + 854 + fn slab_silhouettes(slab: &FaceMesh, pick: PickId) -> Vec<SilhouetteCandidate> { 855 + let positions = slab.positions(); 856 + slab.triangles() 857 + .iter() 858 + .fold( 859 + BTreeMap::<EdgeKey, EdgeAdjacency>::new(), 860 + |adjacency, tri| match triangle_normal(positions, *tri) { 861 + Some(normal) => { 862 + triangle_edges(*tri) 863 + .into_iter() 864 + .fold(adjacency, |mut adjacency, (i, j)| { 865 + let (a, b) = (positions[i as usize], positions[j as usize]); 866 + let key = EdgeKey::new(a, b); 867 + let entry = adjacency 868 + .remove(&key) 869 + .unwrap_or_else(|| EdgeAdjacency::new(a, b)); 870 + adjacency.insert(key, entry.record(normal)); 871 + adjacency 872 + }) 873 + } 874 + None => adjacency, 875 + }, 876 + ) 877 + .into_values() 878 + .filter_map(|entry| entry.into_candidate(pick)) 879 + .collect() 880 + } 881 + 882 + fn triangle_edges(tri: [u32; 3]) -> [(u32, u32); 3] { 883 + [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] 884 + } 885 + 886 + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 887 + struct VertexKey([u64; 3]); 888 + 889 + impl VertexKey { 890 + fn new(p: Point3) -> Self { 891 + let (x, y, z) = p.coords_mm(); 892 + Self([x.to_bits(), y.to_bits(), z.to_bits()]) 893 + } 894 + } 895 + 896 + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 897 + struct EdgeKey(VertexKey, VertexKey); 898 + 899 + impl EdgeKey { 900 + fn new(a: Point3, b: Point3) -> Self { 901 + let (ka, kb) = (VertexKey::new(a), VertexKey::new(b)); 902 + if ka <= kb { Self(ka, kb) } else { Self(kb, ka) } 903 + } 904 + } 905 + 906 + fn triangle_normal(positions: &[Point3], tri: [u32; 3]) -> Option<UnitVec3> { 907 + let a = positions[tri[0] as usize]; 908 + let b = positions[tri[1] as usize]; 909 + let c = positions[tri[2] as usize]; 910 + (b - a).cross(c - a).try_normalize(NORMAL_TOLERANCE).ok() 679 911 } 680 912 681 913 #[cfg(test)]