Another project
0

Configure Feed

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

feat: arc2 center-start-end ctor, other stuff

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

author
Lewis
date (Jun 2, 2026, 11:16 AM +0300) commit e700487e parent f3b3cc3e change-id wmxmmsmn
+103 -26
+58 -2
crates/bone-kernel/src/arc2.rs
··· 46 46 }) 47 47 } 48 48 49 + pub fn from_center_start_end( 50 + center: Point2, 51 + start: Point2, 52 + end: Point2, 53 + tolerance: Tolerance, 54 + ) -> Result<Self, KernelError> { 55 + let (cx, cy) = center.coords_mm(); 56 + let (sx, sy) = start.coords_mm(); 57 + let (ex, ey) = end.coords_mm(); 58 + let radius = (sx - cx).hypot(sy - cy); 59 + let start_angle = (sy - cy).atan2(sx - cx); 60 + let sweep = ((ey - cy).atan2(ex - cx) - start_angle).rem_euclid(TAU); 61 + if !(radius.is_finite() && sweep.is_finite()) { 62 + return Err(KernelError::DegenerateArc); 63 + } 64 + Self::new( 65 + center, 66 + Length::new::<millimeter>(radius), 67 + Angle::new::<radian>(start_angle), 68 + Angle::new::<radian>(sweep), 69 + tolerance, 70 + ) 71 + } 72 + 49 73 #[must_use] 50 74 pub const fn center(self) -> Point2 { 51 75 self.center ··· 226 250 227 251 #[cfg(test)] 228 252 mod tests { 229 - use super::arc_bounding_box; 230 - use bone_types::{Angle, Length, Point2}; 253 + use super::{Arc2, arc_bounding_box}; 254 + use bone_types::{Angle, Length, Point2, Tolerance}; 231 255 use core::f64::consts::{FRAC_PI_2, PI, TAU}; 232 256 use uom::si::angle::radian; 233 257 use uom::si::length::millimeter; 234 258 235 259 fn approx(a: f64, b: f64) -> bool { 236 260 (a - b).abs() < 1e-9 261 + } 262 + 263 + #[test] 264 + fn center_start_end_sweep_is_ccw_and_order_picks_minor_or_major() { 265 + let tol = Tolerance::new(1e-9); 266 + let center = Point2::from_mm(0.0, 0.0); 267 + let east = Point2::from_mm(5.0, 0.0); 268 + let north = Point2::from_mm(0.0, 5.0); 269 + 270 + let Ok(minor) = Arc2::from_center_start_end(center, east, north, tol) else { 271 + panic!("east->north is a buildable quarter arc"); 272 + }; 273 + assert!(approx(minor.radius_mm(), 5.0)); 274 + assert!(approx(minor.start_rad(), 0.0)); 275 + assert!(approx(minor.sweep_rad(), FRAC_PI_2)); 276 + 277 + let Ok(major) = Arc2::from_center_start_end(center, north, east, tol) else { 278 + panic!("north->east is the complementary major arc"); 279 + }; 280 + assert!(approx(major.start_rad(), FRAC_PI_2)); 281 + assert!(approx(major.sweep_rad(), 3.0 * FRAC_PI_2)); 282 + } 283 + 284 + #[test] 285 + fn center_start_end_rejects_non_finite_and_degenerate() { 286 + let tol = Tolerance::new(1e-9); 287 + let center = Point2::from_mm(0.0, 0.0); 288 + let coincident = Point2::from_mm(0.0, 0.0); 289 + let any = Point2::from_mm(5.0, 0.0); 290 + assert!(Arc2::from_center_start_end(center, coincident, any, tol).is_err()); 291 + let nan = Point2::from_mm(f64::NAN, 0.0); 292 + assert!(Arc2::from_center_start_end(center, nan, any, tol).is_err()); 237 293 } 238 294 239 295 fn arc(cx: f64, cy: f64, r_mm: f64, start_rad: f64, sweep_rad: f64) -> (Point2, Point2) {
+3 -1
crates/bone-kernel/src/brep/mod.rs
··· 76 76 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 77 77 pub enum ProfileDefect { 78 78 OpenLoop, 79 + BranchingVertex, 79 80 SelfIntersectingLoop, 80 81 ZeroArea, 81 82 UncontainedLoop, ··· 86 87 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 87 88 f.write_str(match self { 88 89 Self::OpenLoop => "an open loop", 90 + Self::BranchingVertex => "a vertex shared by more than two edges", 89 91 Self::SelfIntersectingLoop => "a self-intersecting loop", 90 92 Self::ZeroArea => "zero area", 91 93 Self::UncontainedLoop => "a loop outside the outer boundary", ··· 94 96 } 95 97 } 96 98 97 - #[derive(Debug, thiserror::Error)] 99 + #[derive(Debug, Clone, thiserror::Error)] 98 100 pub enum BrepError { 99 101 #[error("extrude profile has {reason}")] 100 102 InvalidProfile { reason: ProfileDefect },
+12 -23
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, arc_bounding_box}; 5 + use bone_kernel::{Aabb2, Arc2, arc_bounding_box}; 6 6 use bone_types::{ 7 - Angle, Length, Point2, SketchDimensionId, SketchEntityId, SketchRelationId, Vec2, 7 + Angle, Length, Point2, SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, Vec2, 8 8 }; 9 - use core::f64::consts::{FRAC_1_SQRT_2, TAU}; 10 - use uom::si::angle::{degree, radian}; 9 + use core::f64::consts::FRAC_1_SQRT_2; 10 + use uom::si::angle::degree; 11 11 use uom::si::length::millimeter; 12 12 13 13 use crate::pick::{EntityKindTag, PickId, PickIdError, PickIndex}; 14 + 15 + const ARC_TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 14 16 15 17 #[derive(Copy, Clone, Debug, PartialEq)] 16 18 pub struct ScenePoint { ··· 605 607 let center = point_position(sketch, data.center()); 606 608 let start = point_position(sketch, data.start()); 607 609 let end = point_position(sketch, data.end()); 608 - let (cx, cy) = center.coords_mm(); 609 - let (start_x, start_y) = start.coords_mm(); 610 - let (end_x, end_y) = end.coords_mm(); 611 - let start_offset_x = start_x - cx; 612 - let start_offset_y = start_y - cy; 613 - let radius_mm = (start_offset_x * start_offset_x + start_offset_y * start_offset_y).sqrt(); 614 - if !(radius_mm.is_finite() && radius_mm > 0.0) { 615 - return None; 616 - } 617 - let start_angle = start_offset_y.atan2(start_offset_x); 618 - let end_angle = (end_y - cy).atan2(end_x - cx); 619 - let sweep = (end_angle - start_angle).rem_euclid(TAU); 620 - if !(sweep.is_finite() && sweep > 0.0) { 621 - return None; 622 - } 610 + let arc = Arc2::from_center_start_end(center, start, end, ARC_TOLERANCE).ok()?; 623 611 Some(SceneArc { 624 - center, 625 - radius: Length::new::<millimeter>(radius_mm), 626 - start_angle: Angle::new::<radian>(start_angle), 627 - sweep_angle: Angle::new::<radian>(sweep), 612 + center: arc.center(), 613 + radius: arc.radius(), 614 + start_angle: arc.start_angle(), 615 + sweep_angle: arc.sweep_angle(), 628 616 entity: id, 629 617 pick, 630 618 for_construction: data.for_construction(), ··· 636 624 use super::*; 637 625 use bone_document::{EditOutcome, SketchEdit}; 638 626 use bone_types::{Point3, SketchPlaneBasis, Tolerance, UnitVec3}; 627 + use uom::si::angle::radian; 639 628 use uom::si::length::millimeter; 640 629 641 630 fn plane() -> SketchPlaneBasis {
+24
crates/bone-types/src/lib.rs
··· 274 274 } 275 275 } 276 276 277 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 278 + pub struct GeometryGeneration(u64); 279 + 280 + impl GeometryGeneration { 281 + #[must_use] 282 + pub fn from_solid_key(key: SolidKey) -> Self { 283 + let folded = key 284 + .bytes() 285 + .chunks_exact(8) 286 + .map(|word| { 287 + let mut bytes = [0u8; 8]; 288 + bytes.copy_from_slice(word); 289 + u64::from_le_bytes(bytes) 290 + }) 291 + .fold(0u64, core::ops::BitXor::bitxor); 292 + Self(folded) 293 + } 294 + 295 + #[must_use] 296 + pub const fn value(self) -> u64 { 297 + self.0 298 + } 299 + } 300 + 277 301 impl core::fmt::Display for MeshGeneration { 278 302 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 279 303 write!(f, "mesh_gen={}", self.0)
+6
crates/bone-types/src/space.rs
··· 589 589 } 590 590 } 591 591 592 + impl From<SketchPlaneBasis> for Plane3 { 593 + fn from(basis: SketchPlaneBasis) -> Self { 594 + Self::new_unchecked(basis.origin(), basis.x_axis(), basis.y_axis()) 595 + } 596 + } 597 + 592 598 impl core::fmt::Display for SketchPlaneBasis { 593 599 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 594 600 write!(