Another project
0

Configure Feed

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

feat(kernel): brep edges for render

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

author
Lewis
date (May 30, 2026, 1:38 PM +0300) commit b2fc5bcc parent 8615bdf9 change-id qztvxpmz
+583 -33
+143 -15
crates/bone-kernel/src/brep/build.rs
··· 2 2 use std::collections::HashSet; 3 3 4 4 use bone_types::{ 5 - BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, EdgeLabel, EdgeRole, FaceLabel, 6 - SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole, 5 + BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, CreaseAngle, EdgeLabel, EdgeRole, 6 + FaceLabel, Parameter, Point3, SideKind, SketchEntityId, Tolerance, UnitVec3, VertexLabel, 7 + VertexRole, 7 8 }; 8 9 use slotmap::{Key, SlotMap}; 9 10 use truck_modeling::{ 10 - BoundedCurve, Edge, EdgeID, FaceID, ParameterDivision1D, Shell, Solid, VertexID, 11 + BoundedCurve, Edge, EdgeID, FaceID, ParameterDivision1D, ParametricCurve, ParametricSurface3D, 12 + SPHint2D, SearchNearestParameter, Shell, Solid, VertexID, 11 13 }; 12 14 15 + use super::convert::{point_from_truck, try_unit_from_truck}; 16 + use super::edges::{crease_from_normals, line_between, EdgeCurve3}; 13 17 use super::{BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, LabelKind}; 18 + use crate::curve3::Curve3; 14 19 15 20 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] 16 21 pub(crate) struct BoundaryIndex(usize); ··· 67 72 pub(crate) edges: HashMap<EdgeID, EdgeLabel>, 68 73 pub(crate) vertices: HashMap<VertexID, VertexLabel>, 69 74 pub(crate) closed_curves: HashSet<SketchEntityId>, 75 + pub(crate) edge_curves: HashMap<EdgeID, EdgeCurve3>, 70 76 } 71 77 72 78 fn suppressed_edge(label: EdgeLabel, closed: &HashSet<SketchEntityId>) -> bool { ··· 84 90 85 91 pub(crate) fn assemble(solid: Solid, labeling: &SolidLabeling) -> Result<BrepSolid, BrepError> { 86 92 let vertices = build_vertices(&solid, labeling)?; 87 - let edges = build_edges(&solid, labeling, &vertices.by_truck)?; 93 + let truck_faces: HashMap<FaceID, truck_modeling::Face> = solid 94 + .boundaries() 95 + .iter() 96 + .flat_map(Shell::face_iter) 97 + .map(|face| (face.id(), face.clone())) 98 + .collect(); 99 + let truck_face_adj = gather_truck_face_adjacency(&solid); 100 + let edges = build_edges( 101 + &solid, 102 + labeling, 103 + &vertices.by_truck, 104 + &vertices.positions, 105 + &truck_face_adj, 106 + &truck_faces, 107 + )?; 88 108 let faces = build_faces(&solid, labeling, &edges)?; 89 109 90 110 let shell_order = ordered_by(&faces.shells, |shell| shell.boundary_index); ··· 115 135 }) 116 136 } 117 137 138 + fn gather_truck_face_adjacency(solid: &Solid) -> HashMap<EdgeID, Vec<FaceID>> { 139 + solid 140 + .boundaries() 141 + .iter() 142 + .flat_map(Shell::face_iter) 143 + .fold(HashMap::<EdgeID, Vec<FaceID>>::new(), |mut adj, face| { 144 + face.boundaries().iter().for_each(|wire| { 145 + wire.edge_iter().for_each(|edge| { 146 + adj.entry(edge.id()).or_default().push(face.id()); 147 + }); 148 + }); 149 + adj 150 + }) 151 + } 152 + 118 153 fn ordered_by<K: Key, V, T: Ord>(map: &SlotMap<K, V>, key: impl Fn(&V) -> T) -> Vec<K> { 119 154 let mut keys: Vec<K> = map.keys().collect(); 120 155 keys.sort_by_cached_key(|k| key(&map[*k])); ··· 124 159 struct VertexTable { 125 160 map: SlotMap<BrepVertexId, BrepVertex>, 126 161 by_truck: HashMap<VertexID, BrepVertexId>, 162 + positions: HashMap<BrepVertexId, Point3>, 127 163 } 128 164 129 165 fn build_vertices(solid: &Solid, labeling: &SolidLabeling) -> Result<VertexTable, BrepError> { 130 - let (map, _by_label, by_truck) = solid.vertex_iter().try_fold( 166 + let (map, _by_label, by_truck, positions) = solid.vertex_iter().try_fold( 131 167 ( 132 168 SlotMap::with_key(), 133 169 HashMap::<VertexLabel, BrepVertexId>::new(), 134 170 HashMap::<VertexID, BrepVertexId>::new(), 171 + HashMap::<BrepVertexId, Point3>::new(), 135 172 ), 136 - |(mut map, mut by_label, mut by_truck), vertex| { 173 + |(mut map, mut by_label, mut by_truck, mut positions), vertex| { 137 174 if by_truck.contains_key(&vertex.id()) { 138 - return Ok::<_, BrepError>((map, by_label, by_truck)); 175 + return Ok::<_, BrepError>((map, by_label, by_truck, positions)); 139 176 } 140 177 let label = *labeling 141 178 .vertices ··· 144 181 kind: LabelKind::Vertex, 145 182 })?; 146 183 if suppressed_vertex(label, &labeling.closed_curves) { 147 - return Ok((map, by_label, by_truck)); 184 + return Ok((map, by_label, by_truck, positions)); 148 185 } 186 + let position = point_from_truck(vertex.point()); 149 187 let brep_id = by_label.get(&label).copied().unwrap_or_else(|| { 150 - let id = map.insert_with_key(|id| BrepVertex { id, label }); 188 + let id = map.insert_with_key(|id| BrepVertex { 189 + id, 190 + label, 191 + position, 192 + }); 151 193 by_label.insert(label, id); 152 194 id 153 195 }); 154 196 by_truck.insert(vertex.id(), brep_id); 155 - Ok((map, by_label, by_truck)) 197 + positions.entry(brep_id).or_insert(position); 198 + Ok((map, by_label, by_truck, positions)) 156 199 }, 157 200 )?; 158 - Ok(VertexTable { map, by_truck }) 201 + Ok(VertexTable { 202 + map, 203 + by_truck, 204 + positions, 205 + }) 159 206 } 160 207 161 208 struct EdgeTable { ··· 167 214 struct EdgeAccum { 168 215 handles: Vec<EdgeArenaHandle>, 169 216 incident: Vec<BrepVertexId>, 217 + analytic: Option<EdgeCurve3>, 170 218 } 171 219 172 220 fn build_edges( 173 221 solid: &Solid, 174 222 labeling: &SolidLabeling, 175 223 vertex_dedup: &HashMap<VertexID, BrepVertexId>, 224 + vertex_positions: &HashMap<BrepVertexId, Point3>, 225 + truck_face_adj: &HashMap<EdgeID, Vec<FaceID>>, 226 + truck_faces: &HashMap<FaceID, truck_modeling::Face>, 176 227 ) -> Result<EdgeTable, BrepError> { 177 228 let (accum, arena, truck_label) = solid.edge_iter().try_fold( 178 229 ( ··· 198 249 let accumulator = accum.entry(label).or_insert_with(|| EdgeAccum { 199 250 handles: Vec::new(), 200 251 incident: Vec::new(), 252 + analytic: None, 201 253 }); 202 254 accumulator.handles.push(handle); 255 + if accumulator.analytic.is_none() { 256 + accumulator.analytic = labeling.edge_curves.get(&edge.id()).cloned(); 257 + } 203 258 [edge.front().id(), edge.back().id()] 204 259 .into_iter() 205 260 .filter_map(|truck_vertex| vertex_dedup.get(&truck_vertex).copied()) ··· 214 269 let (map, label_to_id) = ordered_edges.into_iter().fold( 215 270 (SlotMap::with_key(), HashMap::<EdgeLabel, BrepEdgeId>::new()), 216 271 |(mut map, mut label_to_id), (label, slot)| { 217 - let vertices = logical_endpoints(&slot.incident); 272 + let primary = &arena[slot.handles[0].0]; 273 + let curve = resolve_curve(slot.analytic, primary); 274 + let vertices = ordered_endpoints(&slot.incident, &curve, vertex_positions); 275 + let crease = compute_crease(truck_face_adj.get(&primary.id()), truck_faces, primary); 218 276 let id = map.insert_with_key(|id| BrepEdge { 219 277 id, 220 278 label, 221 279 handles: slot.handles, 222 280 vertices, 281 + curve, 282 + crease, 223 283 }); 224 284 label_to_id.insert(label, id); 225 285 (map, label_to_id) ··· 238 298 }) 239 299 } 240 300 241 - fn logical_endpoints(incident: &[BrepVertexId]) -> [BrepVertexId; 2] { 301 + fn resolve_curve(analytic: Option<EdgeCurve3>, primary: &Edge) -> EdgeCurve3 { 302 + analytic.unwrap_or_else(|| { 303 + let front = point_from_truck(primary.absolute_front().point()); 304 + let back = point_from_truck(primary.absolute_back().point()); 305 + line_between(front, back) 306 + }) 307 + } 308 + 309 + fn compute_crease( 310 + adjacent: Option<&Vec<FaceID>>, 311 + truck_faces: &HashMap<FaceID, truck_modeling::Face>, 312 + edge: &Edge, 313 + ) -> CreaseAngle { 314 + let Some(face_ids) = adjacent else { 315 + return CreaseAngle::FLAT; 316 + }; 317 + let midpoint = edge_midpoint(edge); 318 + let normals: Vec<UnitVec3> = face_ids 319 + .iter() 320 + .filter_map(|face_id| truck_faces.get(face_id)) 321 + .filter_map(|face| face_normal_at(face, midpoint)) 322 + .collect(); 323 + match normals.as_slice() { 324 + [n1, n2] => crease_from_normals(*n1, *n2), 325 + _ => CreaseAngle::FLAT, 326 + } 327 + } 328 + 329 + fn edge_midpoint(edge: &Edge) -> truck_modeling::Point3 { 330 + let curve = edge.curve(); 331 + let (t0, t1) = curve.range_tuple(); 332 + curve.subs(f64::midpoint(t0, t1)) 333 + } 334 + 335 + fn face_normal_at(face: &truck_modeling::Face, point: truck_modeling::Point3) -> Option<UnitVec3> { 336 + let surface = face.surface(); 337 + let (u, v) = surface.search_nearest_parameter(point, SPHint2D::None, 20)?; 338 + let mut normal = surface.normal(u, v); 339 + if !face.orientation() { 340 + normal = -normal; 341 + } 342 + try_unit_from_truck(normal, Tolerance::new(1.0e-9)) 343 + } 344 + 345 + fn ordered_endpoints( 346 + incident: &[BrepVertexId], 347 + curve: &EdgeCurve3, 348 + vertex_positions: &HashMap<BrepVertexId, Point3>, 349 + ) -> [BrepVertexId; 2] { 242 350 let counts = incident.iter().fold( 243 351 HashMap::<BrepVertexId, usize>::new(), 244 352 |mut counts, vertex| { ··· 251 359 .filter_map(|(vertex, count)| (*count % 2 == 1).then_some(*vertex)) 252 360 .collect(); 253 361 odd.sort_unstable(); 254 - if let [front, back] = odd.as_slice() { 255 - [*front, *back] 362 + if let [a, b] = odd.as_slice() { 363 + let start = curve.evaluate(Parameter::new(0.0)); 364 + let a_dist = vertex_positions 365 + .get(a) 366 + .map_or(f64::INFINITY, |pos| squared_distance(*pos, start)); 367 + let b_dist = vertex_positions 368 + .get(b) 369 + .map_or(f64::INFINITY, |pos| squared_distance(*pos, start)); 370 + if a_dist <= b_dist { 371 + [*a, *b] 372 + } else { 373 + [*b, *a] 374 + } 256 375 } else { 257 376 let representative = counts.keys().copied().min().unwrap_or_default(); 258 377 [representative, representative] 259 378 } 379 + } 380 + 381 + fn squared_distance(a: Point3, b: Point3) -> f64 { 382 + let (ax, ay, az) = a.coords_mm(); 383 + let (bx, by, bz) = b.coords_mm(); 384 + let dx = ax - bx; 385 + let dy = ay - by; 386 + let dz = az - bz; 387 + dx * dx + dy * dy + dz * dz 260 388 } 261 389 262 390 struct FaceTable {
+261
crates/bone-kernel/src/brep/edges.rs
··· 1 + use bone_types::{ 2 + Aabb3, BrepEdgeId, ChordHeightTolerance, CreaseAngle, EdgeLabel, EdgeRole, Parameter, Plane3, 3 + Point2, Point3, SideKind, Tolerance, UnitVec3, Vec3, 4 + }; 5 + use std::ops::Deref; 6 + 7 + use crate::arc3::Arc3; 8 + use crate::circle3::Circle3; 9 + use crate::closest::ClosestPoint3; 10 + use crate::curve2::Curve2Kind; 11 + use crate::curve3::Curve3; 12 + use crate::line3::Line3; 13 + 14 + #[derive(Clone, Debug, PartialEq)] 15 + pub enum EdgeCurve3 { 16 + Line(Line3), 17 + Arc(Arc3), 18 + Circle(Circle3), 19 + } 20 + 21 + impl Curve3 for EdgeCurve3 { 22 + fn evaluate(&self, t: Parameter) -> Point3 { 23 + match self { 24 + Self::Line(c) => c.evaluate(t), 25 + Self::Arc(c) => c.evaluate(t), 26 + Self::Circle(c) => c.evaluate(t), 27 + } 28 + } 29 + 30 + fn derivative(&self, t: Parameter) -> Vec3 { 31 + match self { 32 + Self::Line(c) => c.derivative(t), 33 + Self::Arc(c) => c.derivative(t), 34 + Self::Circle(c) => c.derivative(t), 35 + } 36 + } 37 + 38 + fn bounding_box(&self) -> Aabb3 { 39 + match self { 40 + Self::Line(c) => c.bounding_box(), 41 + Self::Arc(c) => c.bounding_box(), 42 + Self::Circle(c) => c.bounding_box(), 43 + } 44 + } 45 + 46 + fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 { 47 + match self { 48 + Self::Line(c) => c.closest_point(p, tolerance), 49 + Self::Arc(c) => c.closest_point(p, tolerance), 50 + Self::Circle(c) => c.closest_point(p, tolerance), 51 + } 52 + } 53 + 54 + fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> { 55 + match self { 56 + Self::Line(c) => c.tessellate(tolerance), 57 + Self::Arc(c) => c.tessellate(tolerance), 58 + Self::Circle(c) => c.tessellate(tolerance), 59 + } 60 + } 61 + } 62 + 63 + impl core::fmt::Display for EdgeCurve3 { 64 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 65 + match self { 66 + Self::Line(c) => write!(f, "{c}"), 67 + Self::Arc(c) => write!(f, "{c}"), 68 + Self::Circle(c) => write!(f, "{c}"), 69 + } 70 + } 71 + } 72 + 73 + #[derive(Clone, Debug, PartialEq)] 74 + pub struct EdgePolyline { 75 + edge: BrepEdgeId, 76 + label: EdgeLabel, 77 + points: Vec<Point3>, 78 + curve: EdgeCurve3, 79 + crease: CreaseAngle, 80 + } 81 + 82 + impl EdgePolyline { 83 + #[must_use] 84 + pub(crate) fn new( 85 + edge: BrepEdgeId, 86 + label: EdgeLabel, 87 + points: Vec<Point3>, 88 + curve: EdgeCurve3, 89 + crease: CreaseAngle, 90 + ) -> Self { 91 + Self { 92 + edge, 93 + label, 94 + points, 95 + curve, 96 + crease, 97 + } 98 + } 99 + 100 + #[must_use] 101 + pub fn edge(&self) -> BrepEdgeId { 102 + self.edge 103 + } 104 + 105 + #[must_use] 106 + pub fn label(&self) -> EdgeLabel { 107 + self.label 108 + } 109 + 110 + #[must_use] 111 + pub fn points(&self) -> &[Point3] { 112 + &self.points 113 + } 114 + 115 + #[must_use] 116 + pub fn curve(&self) -> &EdgeCurve3 { 117 + &self.curve 118 + } 119 + 120 + #[must_use] 121 + pub fn crease(&self) -> CreaseAngle { 122 + self.crease 123 + } 124 + } 125 + 126 + #[derive(Clone, Debug, PartialEq, Default)] 127 + pub struct EdgePolylines(Vec<EdgePolyline>); 128 + 129 + impl EdgePolylines { 130 + #[must_use] 131 + pub(crate) fn new(polylines: Vec<EdgePolyline>) -> Self { 132 + Self(polylines) 133 + } 134 + 135 + #[must_use] 136 + pub fn as_slice(&self) -> &[EdgePolyline] { 137 + &self.0 138 + } 139 + 140 + #[must_use] 141 + pub fn len(&self) -> usize { 142 + self.0.len() 143 + } 144 + 145 + #[must_use] 146 + pub fn is_empty(&self) -> bool { 147 + self.0.is_empty() 148 + } 149 + 150 + pub fn iter(&self) -> core::slice::Iter<'_, EdgePolyline> { 151 + self.0.iter() 152 + } 153 + } 154 + 155 + impl Deref for EdgePolylines { 156 + type Target = [EdgePolyline]; 157 + 158 + fn deref(&self) -> &Self::Target { 159 + &self.0 160 + } 161 + } 162 + 163 + impl IntoIterator for EdgePolylines { 164 + type Item = EdgePolyline; 165 + type IntoIter = std::vec::IntoIter<EdgePolyline>; 166 + 167 + fn into_iter(self) -> Self::IntoIter { 168 + self.0.into_iter() 169 + } 170 + } 171 + 172 + impl<'a> IntoIterator for &'a EdgePolylines { 173 + type Item = &'a EdgePolyline; 174 + type IntoIter = core::slice::Iter<'a, EdgePolyline>; 175 + 176 + fn into_iter(self) -> Self::IntoIter { 177 + self.0.iter() 178 + } 179 + } 180 + 181 + #[must_use] 182 + pub(crate) fn is_render_visible(label: EdgeLabel) -> bool { 183 + !matches!( 184 + label.role, 185 + EdgeRole::SideEdge { 186 + side: SideKind::Seam, 187 + .. 188 + } 189 + ) 190 + } 191 + 192 + #[must_use] 193 + pub(crate) fn lift_curve2( 194 + curve: Curve2Kind, 195 + plane: Plane3, 196 + offset_mm: f64, 197 + tolerance: Tolerance, 198 + ) -> Option<EdgeCurve3> { 199 + match curve { 200 + Curve2Kind::Line(line) => { 201 + let start = lift_point(plane, offset_mm, line.start()); 202 + let end = lift_point(plane, offset_mm, line.end()); 203 + Line3::new(start, end, tolerance).ok().map(EdgeCurve3::Line) 204 + } 205 + Curve2Kind::Arc(arc) => { 206 + let cap_plane = lifted_origin_plane(plane, offset_mm); 207 + let (cx, cy) = arc.center().coords_mm(); 208 + let arc_plane = recentered_plane(cap_plane, cx, cy); 209 + Arc3::new( 210 + arc_plane, 211 + arc.radius(), 212 + arc.start_angle(), 213 + arc.sweep_angle(), 214 + tolerance, 215 + ) 216 + .ok() 217 + .map(EdgeCurve3::Arc) 218 + } 219 + Curve2Kind::Circle(circle) => { 220 + let cap_plane = lifted_origin_plane(plane, offset_mm); 221 + let (cx, cy) = circle.center().coords_mm(); 222 + let circle_plane = recentered_plane(cap_plane, cx, cy); 223 + Circle3::new(circle_plane, circle.radius(), tolerance) 224 + .ok() 225 + .map(EdgeCurve3::Circle) 226 + } 227 + } 228 + } 229 + 230 + fn lifted_origin_plane(plane: Plane3, offset_mm: f64) -> Plane3 { 231 + Plane3::new_unchecked( 232 + plane.point_at_local(0.0, 0.0, offset_mm), 233 + plane.x_axis(), 234 + plane.y_axis(), 235 + ) 236 + } 237 + 238 + fn recentered_plane(plane: Plane3, cx: f64, cy: f64) -> Plane3 { 239 + Plane3::new_unchecked( 240 + plane.point_at_local(cx, cy, 0.0), 241 + plane.x_axis(), 242 + plane.y_axis(), 243 + ) 244 + } 245 + 246 + fn lift_point(plane: Plane3, offset_mm: f64, point: Point2) -> Point3 { 247 + let (u, v) = point.coords_mm(); 248 + plane.point_at_local(u, v, offset_mm) 249 + } 250 + 251 + #[must_use] 252 + pub(crate) fn line_between(start: Point3, end: Point3) -> EdgeCurve3 { 253 + EdgeCurve3::Line(Line3::new_unchecked(start, end)) 254 + } 255 + 256 + #[must_use] 257 + pub(crate) fn crease_from_normals(a: UnitVec3, b: UnitVec3) -> CreaseAngle { 258 + let (ax, ay, az) = a.components(); 259 + let (bx, by, bz) = b.components(); 260 + CreaseAngle::from_dot(ax * bx + ay * by + az * bz) 261 + }
+65 -13
crates/bone-kernel/src/brep/eval.rs
··· 13 13 use crate::intersect::{IntersectionSet, intersect_curves}; 14 14 15 15 use super::build::{SolidLabeling, assemble}; 16 + use super::edges::{lift_curve2, EdgeCurve3}; 16 17 use super::profile::{ExtrudeProfile, ProfileLoop}; 17 18 use super::{BrepError, BrepSolid, ProfileDefect, TruckGap}; 18 19 ··· 31 32 ) -> Result<BrepSolid, BrepError> { 32 33 let plan = sweep_plan(feature)?; 33 34 let resolved = resolve_profile(profile)?; 35 + let parent_curves = parent_curves(profile); 34 36 let (solid, labeling) = build_solid( 35 37 profile.plane(), 36 38 &resolved, 37 39 plan, 38 40 extrude, 39 41 closed_curves(profile), 42 + &parent_curves, 40 43 )?; 41 44 assemble(solid, &labeling) 45 + } 46 + 47 + fn parent_curves(profile: &ExtrudeProfile) -> HashMap<SketchEntityId, Curve2Kind> { 48 + profile 49 + .loops() 50 + .iter() 51 + .flat_map(|profile_loop| match profile_loop { 52 + ProfileLoop::Closed { 53 + curve, 54 + curve_entity, 55 + } => vec![(*curve_entity, *curve)], 56 + ProfileLoop::Open(edges) => edges 57 + .iter() 58 + .map(|edge| (edge.curve_entity(), edge.curve())) 59 + .collect(), 60 + }) 61 + .collect() 42 62 } 43 63 44 64 fn closed_curves(profile: &ExtrudeProfile) -> HashSet<SketchEntityId> { ··· 448 468 plan: SweepPlan, 449 469 extrude: FeatureId, 450 470 closed: HashSet<SketchEntityId>, 471 + parent_curves: &HashMap<SketchEntityId, Curve2Kind>, 451 472 ) -> Result<(Solid, SolidLabeling), BrepError> { 452 473 let mut book = Bookkeeping::default(); 453 474 let wires: Vec<Wire> = resolved ··· 469 490 let (nx, ny, nz) = plane.normal().components(); 470 491 let sweep = Vector3::new(nx * plan.depth_mm, ny * plan.depth_mm, nz * plan.depth_mm); 471 492 let solid = builder::tsweep(&face, sweep); 472 - let labeling = label_solid(&solid, &book, extrude, closed); 493 + let labeling = label_solid(&solid, &book, extrude, closed, plane, plan, parent_curves); 473 494 Ok((solid, labeling)) 474 495 } 475 496 ··· 507 528 508 529 fn lift(plane: Plane3, offset_mm: f64, point: Point2) -> truck_modeling::Point3 { 509 530 let (u, v) = point.coords_mm(); 510 - let (ox, oy, oz) = plane.origin().coords_mm(); 511 - let (nx, ny, nz) = plane.normal().components(); 512 - let (xx, xy, xz) = plane.x_axis().components(); 513 - let (yx, yy, yz) = plane.y_axis().components(); 514 - truck_modeling::Point3::new( 515 - ox + offset_mm * nx + u * xx + v * yx, 516 - oy + offset_mm * ny + u * xy + v * yy, 517 - oz + offset_mm * nz + u * xz + v * yz, 518 - ) 531 + let (wx, wy, wz) = plane.point_at_local(u, v, offset_mm).coords_mm(); 532 + truck_modeling::Point3::new(wx, wy, wz) 519 533 } 520 534 521 535 struct Labels { 522 536 faces: HashMap<FaceID, FaceLabel>, 523 537 edges: HashMap<EdgeID, EdgeLabel>, 524 538 vertices: HashMap<VertexID, VertexLabel>, 539 + edge_curves: HashMap<EdgeID, EdgeCurve3>, 525 540 feature: FeatureId, 526 541 closed: HashSet<SketchEntityId>, 527 542 } ··· 532 547 faces: HashMap::new(), 533 548 edges: HashMap::new(), 534 549 vertices: HashMap::new(), 550 + edge_curves: HashMap::new(), 535 551 feature, 536 552 closed, 537 553 } ··· 567 583 ); 568 584 } 569 585 586 + fn edge_curve(&mut self, id: EdgeID, curve: EdgeCurve3) { 587 + self.edge_curves.insert(id, curve); 588 + } 589 + 570 590 fn into_labeling(self) -> SolidLabeling { 571 591 SolidLabeling { 572 592 faces: self.faces, 573 593 edges: self.edges, 574 594 vertices: self.vertices, 575 595 closed_curves: self.closed, 596 + edge_curves: self.edge_curves, 576 597 } 577 598 } 578 599 } ··· 590 611 book: &Bookkeeping, 591 612 extrude: FeatureId, 592 613 closed: HashSet<SketchEntityId>, 614 + plane: Plane3, 615 + plan: SweepPlan, 616 + parent_curves: &HashMap<SketchEntityId, Curve2Kind>, 593 617 ) -> SolidLabeling { 594 - solid 618 + let mut labels = solid 595 619 .boundaries() 596 620 .iter() 597 621 .flat_map(|shell| shell.face_iter()) ··· 628 652 ), 629 653 } 630 654 labels 631 - }) 632 - .into_labeling() 655 + }); 656 + record_cap_curves(&mut labels, plane, plan, parent_curves); 657 + labels.into_labeling() 658 + } 659 + 660 + fn record_cap_curves( 661 + labels: &mut Labels, 662 + plane: Plane3, 663 + plan: SweepPlan, 664 + parent_curves: &HashMap<SketchEntityId, Curve2Kind>, 665 + ) { 666 + let entries: Vec<(EdgeID, EdgeRole)> = labels 667 + .edges 668 + .iter() 669 + .map(|(id, label)| (*id, label.role)) 670 + .collect(); 671 + entries.into_iter().for_each(|(edge_id, role)| { 672 + let (offset_mm, curve_entity) = match role { 673 + EdgeRole::StartCapEdge { from } => (plan.base_offset_mm, from), 674 + EdgeRole::EndCapEdge { from } => (plan.base_offset_mm + plan.depth_mm, from), 675 + _ => return, 676 + }; 677 + let Some(parent) = parent_curves.get(&curve_entity) else { 678 + return; 679 + }; 680 + let Some(lifted) = lift_curve2(*parent, plane, offset_mm, PROFILE_TOLERANCE) else { 681 + return; 682 + }; 683 + labels.edge_curve(edge_id, lifted); 684 + }); 633 685 } 634 686 635 687 fn label_side_face(
+38 -1
crates/bone-kernel/src/brep/mod.rs
··· 2 2 3 3 use bone_types::{ 4 4 Aabb3, AngleTolerance, BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, 5 - ChordHeightTolerance, EdgeLabel, EdgeRole, FaceLabel, SideKind, Tolerance, VertexLabel, 5 + ChordHeightTolerance, CreaseAngle, EdgeLabel, EdgeRole, FaceLabel, Point3, SideKind, Tolerance, 6 + VertexLabel, 6 7 }; 7 8 use slotmap::SlotMap; 8 9 use truck_modeling::ShellCondition; 9 10 10 11 mod build; 11 12 pub(crate) mod convert; 13 + mod edges; 12 14 pub mod eval; 13 15 pub mod profile; 14 16 pub mod tessellate; 17 + use crate::curve3::Curve3; 15 18 use build::{Arena, BoundaryIndex, EdgeArenaHandle, edge_length, edge_points}; 16 19 use convert::point_from_truck; 20 + pub use edges::{EdgeCurve3, EdgePolyline, EdgePolylines}; 17 21 pub use tessellate::{FaceMesh, MeshError, SolidMesh}; 18 22 19 23 #[derive(Debug, Clone, Copy, PartialEq, Eq)] ··· 174 178 label: EdgeLabel, 175 179 handles: Vec<EdgeArenaHandle>, 176 180 vertices: [BrepVertexId; 2], 181 + curve: EdgeCurve3, 182 + crease: CreaseAngle, 177 183 } 178 184 179 185 impl BrepEdge { ··· 191 197 pub fn vertices(&self) -> [BrepVertexId; 2] { 192 198 self.vertices 193 199 } 200 + 201 + #[must_use] 202 + pub fn curve(&self) -> &EdgeCurve3 { 203 + &self.curve 204 + } 205 + 206 + #[must_use] 207 + pub fn crease(&self) -> CreaseAngle { 208 + self.crease 209 + } 194 210 } 195 211 196 212 #[derive(Clone, Debug)] 197 213 pub struct BrepVertex { 198 214 id: BrepVertexId, 199 215 label: VertexLabel, 216 + position: Point3, 200 217 } 201 218 202 219 impl BrepVertex { ··· 209 226 pub fn label(&self) -> VertexLabel { 210 227 self.label 211 228 } 229 + 230 + #[must_use] 231 + pub fn position(&self) -> Point3 { 232 + self.position 233 + } 212 234 } 213 235 214 236 #[derive(Clone)] ··· 323 345 ) -> Result<SolidMesh, MeshError> { 324 346 tessellate::tessellate_solid(self, chord, angle) 325 347 } 348 + 349 + #[must_use] 350 + pub fn edges_for_render(&self, chord: ChordHeightTolerance) -> EdgePolylines { 351 + EdgePolylines::new( 352 + self.iter_edges() 353 + .filter(|edge| edges::is_render_visible(edge.label)) 354 + .map(|edge| { 355 + let points = edge.curve.tessellate(chord); 356 + EdgePolyline::new(edge.id, edge.label, points, edge.curve.clone(), edge.crease) 357 + }) 358 + .collect(), 359 + ) 360 + } 326 361 } 327 362 328 363 #[cfg(test)] ··· 482 517 edges, 483 518 vertices, 484 519 closed_curves: HashSet::new(), 520 + edge_curves: HashMap::new(), 485 521 } 486 522 } 487 523 ··· 661 697 edges: HashMap::new(), 662 698 vertices: HashMap::new(), 663 699 closed_curves: HashSet::new(), 700 + edge_curves: HashMap::new(), 664 701 }; 665 702 let Ok(brep) = assemble(solid, &labeling) else { 666 703 panic!("empty solid has nothing to label");
+3 -2
crates/bone-kernel/src/lib.rs
··· 27 27 pub use brep::eval::evaluate_extrude; 28 28 pub use brep::profile::{ExtrudeProfile, ProfileEdge, ProfileLoop}; 29 29 pub use brep::{ 30 - BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, FaceMesh, LabelKind, 31 - MeshError, ProfileDefect, SolidMesh, TruckGap, 30 + BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, EdgeCurve3, 31 + EdgePolyline, EdgePolylines, FaceMesh, LabelKind, MeshError, ProfileDefect, SolidMesh, 32 + TruckGap, 32 33 }; 33 34 pub use circle2::Circle2; 34 35 pub use circle3::Circle3;
+5
crates/bone-kernel/src/line3.rs
··· 20 20 } 21 21 22 22 #[must_use] 23 + pub const fn new_unchecked(start: Point3, end: Point3) -> Self { 24 + Self { start, end } 25 + } 26 + 27 + #[must_use] 23 28 pub const fn start(self) -> Point3 { 24 29 self.start 25 30 }
+55 -2
crates/bone-types/src/lib.rs
··· 223 223 } 224 224 } 225 225 226 + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] 227 + pub struct CreaseAngle(f64); 228 + 229 + impl CreaseAngle { 230 + pub const FLAT: Self = Self(0.0); 231 + 232 + #[must_use] 233 + pub const fn from_radians(value: f64) -> Self { 234 + Self(value) 235 + } 236 + 237 + #[must_use] 238 + pub fn from_dot(dot: f64) -> Self { 239 + Self(dot.clamp(-1.0, 1.0).acos()) 240 + } 241 + 242 + #[must_use] 243 + pub fn angle(self) -> Angle { 244 + Angle::new::<radian>(self.0) 245 + } 246 + 247 + #[must_use] 248 + pub const fn radians(self) -> f64 { 249 + self.0 250 + } 251 + } 252 + 253 + impl core::fmt::Display for CreaseAngle { 254 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 255 + write!(f, "crease={} rad", self.0) 256 + } 257 + } 258 + 226 259 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 227 260 pub struct MeshGeneration(u64); 228 261 ··· 248 281 mod tests { 249 282 use super::{ 250 283 Aabb3, Angle, AngleTolerance, AxisAngle, BodyId, BrepEdgeId, BrepFaceId, BrepLoopId, 251 - BrepShellId, BrepVertexId, Camera3, ChordHeightTolerance, DegreesOfFreedom, DisplayMode, 252 - DocumentId, EdgeId, EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, FaceRole, FeatureId, 284 + BrepShellId, BrepVertexId, Camera3, ChordHeightTolerance, CreaseAngle, DegreesOfFreedom, 285 + DisplayMode, DocumentId, EdgeId, EdgeLabel, EdgeRole, ExtrudeId, FaceId, FaceLabel, 286 + FaceRole, FeatureId, 253 287 ImportOrdinal, Length, LoopId, LoopIndex, MeshGeneration, NodeId, OrbitState, OrientedBox3, 254 288 Parameter, Plane3, Point2, Point3, PositiveLength, Projection, ProjectionKind, 255 289 ShadingModel, ShellId, SideKind, SketchDimensionId, SketchEntityId, SketchId, ··· 348 382 #[test] 349 383 fn angle_tolerance_zero_constant() { 350 384 assert!(AngleTolerance::ZERO.radians().abs() < f64::EPSILON); 385 + } 386 + 387 + #[test] 388 + fn crease_angle_flat_is_zero() { 389 + assert!(CreaseAngle::FLAT.radians().abs() < f64::EPSILON); 390 + } 391 + 392 + #[test] 393 + fn crease_angle_from_dot_right_angle() { 394 + let a = CreaseAngle::from_dot(0.0); 395 + assert!((a.radians() - core::f64::consts::FRAC_PI_2).abs() < 1e-12); 396 + } 397 + 398 + #[test] 399 + fn crease_angle_from_dot_clamps_outside_unit() { 400 + let a = CreaseAngle::from_dot(1.5); 401 + assert!(a.radians().abs() < 1e-12); 402 + let b = CreaseAngle::from_dot(-1.5); 403 + assert!((b.radians() - core::f64::consts::PI).abs() < 1e-12); 351 404 } 352 405 353 406 #[test]
+13
crates/bone-types/src/space.rs
··· 914 914 pub fn normal(self) -> UnitVec3 { 915 915 UnitVec3(Unit::new_normalize(self.x.0.cross(&self.y.0))) 916 916 } 917 + 918 + #[must_use] 919 + pub fn point_at_local(self, u_mm: f64, v_mm: f64, normal_offset_mm: f64) -> Point3 { 920 + let (ox, oy, oz) = self.origin.coords_mm(); 921 + let (xx, xy, xz) = self.x.components(); 922 + let (yx, yy, yz) = self.y.components(); 923 + let (nx, ny, nz) = self.normal().components(); 924 + Point3::from_mm( 925 + ox + u_mm * xx + v_mm * yx + normal_offset_mm * nx, 926 + oy + u_mm * xy + v_mm * yy + normal_offset_mm * ny, 927 + oz + u_mm * xz + v_mm * yz + normal_offset_mm * nz, 928 + ) 929 + } 917 930 } 918 931 919 932 impl core::fmt::Display for Plane3 {