Another project
0

Configure Feed

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

test(app): tools point, line, circle, arc tests

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

author
Lewis
date (May 10, 2026, 7:29 PM +0300) commit efe5e887 parent ca9d70ca change-id svkvrkvr
+461
+416
crates/bone-app/src/tools/tests/basic.rs
··· 1 + use bone_document::{SketchEntity, SketchEntityKind, SketchRelation}; 2 + use bone_types::Point2; 3 + use uom::si::length::millimeter; 4 + 5 + use crate::sketch_mode::{ClickAnchor, Pending, SketchTool}; 6 + use crate::snap::{SnapHit, SnapKind}; 7 + use crate::tools::{add_arc, add_line, add_point, place, preview}; 8 + 9 + use super::{arc_sweep, count_kind, fresh}; 10 + 11 + #[test] 12 + fn point_tool_commits_a_point_per_click() { 13 + let (next, pending) = place( 14 + fresh(), 15 + SketchTool::Point, 16 + Point2::from_mm(3.0, 4.0), 17 + None, 18 + None, 19 + ); 20 + let Some(next) = next else { 21 + panic!("point tool must commit") 22 + }; 23 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 1); 24 + assert_eq!(pending, None); 25 + } 26 + 27 + #[test] 28 + fn point_tool_endpoint_snap_suppresses_duplicate() { 29 + let (sketch, existing) = add_point(fresh(), Point2::from_mm(1.0, 1.0)); 30 + let snap = SnapHit { 31 + kind: SnapKind::Endpoint(existing), 32 + world: Point2::from_mm(1.0, 1.0), 33 + }; 34 + let (next, pending) = place( 35 + sketch, 36 + SketchTool::Point, 37 + Point2::from_mm(1.05, 1.0), 38 + None, 39 + Some(snap), 40 + ); 41 + assert!(next.is_none()); 42 + assert_eq!(pending, None); 43 + } 44 + 45 + #[test] 46 + fn line_first_click_pends_anchor() { 47 + let world = Point2::from_mm(1.0, 2.0); 48 + let (next, pending) = place(fresh(), SketchTool::Line, world, None, None); 49 + assert!(next.is_none()); 50 + assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(world))),); 51 + } 52 + 53 + #[test] 54 + fn line_second_click_emits_line() { 55 + let start = Point2::from_mm(0.0, 0.0); 56 + let end = Point2::from_mm(5.0, 5.0); 57 + let (next, pending) = place( 58 + fresh(), 59 + SketchTool::Line, 60 + end, 61 + Some(Pending::First(ClickAnchor::Position(start))), 62 + None, 63 + ); 64 + let Some(next) = next else { 65 + panic!("second click commits") 66 + }; 67 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 2); 68 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 1); 69 + assert!(matches!( 70 + pending, 71 + Some(Pending::First(ClickAnchor::Endpoint(_))) 72 + )); 73 + } 74 + 75 + #[test] 76 + fn circle_first_click_pends_center() { 77 + let center = Point2::from_mm(1.0, 1.0); 78 + let (next, pending) = place(fresh(), SketchTool::Circle, center, None, None); 79 + assert!(next.is_none()); 80 + assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(center))),); 81 + } 82 + 83 + #[test] 84 + fn circle_second_click_emits_center_point_and_circle() { 85 + let center = Point2::from_mm(0.0, 0.0); 86 + let radius_click = Point2::from_mm(3.0, 4.0); 87 + let (next, pending) = place( 88 + fresh(), 89 + SketchTool::Circle, 90 + radius_click, 91 + Some(Pending::First(ClickAnchor::Position(center))), 92 + None, 93 + ); 94 + let Some(next) = next else { 95 + panic!("circle tool commits on second click") 96 + }; 97 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 1); 98 + assert_eq!(count_kind(&next, SketchEntityKind::Circle), 1); 99 + assert_eq!(pending, None); 100 + let Some((_, circle)) = next 101 + .entities() 102 + .iter() 103 + .find(|(_, e)| matches!(e, SketchEntity::Circle(_))) 104 + else { 105 + panic!("circle entity"); 106 + }; 107 + let SketchEntity::Circle(data) = circle else { 108 + panic!("kind"); 109 + }; 110 + let r = data.radius().get::<millimeter>(); 111 + assert!((r - 5.0).abs() < 1e-9, "r={r}"); 112 + } 113 + 114 + #[test] 115 + fn perimeter_circle_emits_after_three_clicks() { 116 + let p1 = Point2::from_mm(1.0, 0.0); 117 + let p2 = Point2::from_mm(-1.0, 0.0); 118 + let p3 = Point2::from_mm(0.0, 1.0); 119 + let (s, pending) = place(fresh(), SketchTool::PerimeterCircle, p1, None, None); 120 + assert!(s.is_none()); 121 + assert_eq!(pending, Some(Pending::First(ClickAnchor::Position(p1))),); 122 + let (s, pending) = place(fresh(), SketchTool::PerimeterCircle, p2, pending, None); 123 + assert!(s.is_none()); 124 + let Some(Pending::Second(_, _)) = pending else { 125 + panic!("two anchors after second click"); 126 + }; 127 + let (next, pending) = place(fresh(), SketchTool::PerimeterCircle, p3, pending, None); 128 + let Some(next) = next else { 129 + panic!("third click commits") 130 + }; 131 + assert_eq!(count_kind(&next, SketchEntityKind::Circle), 1); 132 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 1); 133 + assert_eq!(pending, None); 134 + } 135 + 136 + #[test] 137 + fn perimeter_circle_collinear_holds_state() { 138 + let p1 = Point2::from_mm(0.0, 0.0); 139 + let p2 = Point2::from_mm(1.0, 0.0); 140 + let collinear = Point2::from_mm(2.0, 0.0); 141 + let (_, pending) = place(fresh(), SketchTool::PerimeterCircle, p1, None, None); 142 + let (_, pending) = place(fresh(), SketchTool::PerimeterCircle, p2, pending, None); 143 + let (next, kept) = place( 144 + fresh(), 145 + SketchTool::PerimeterCircle, 146 + collinear, 147 + pending, 148 + None, 149 + ); 150 + assert!(next.is_none()); 151 + assert!(matches!(kept, Some(Pending::Second(_, _)))); 152 + } 153 + 154 + #[test] 155 + fn centerpoint_arc_emits_three_clicks() { 156 + let center = Point2::from_mm(0.0, 0.0); 157 + let start = Point2::from_mm(5.0, 0.0); 158 + let end = Point2::from_mm(0.0, 5.0); 159 + let (_, pending) = place(fresh(), SketchTool::CenterpointArc, center, None, None); 160 + let (_, pending) = place(fresh(), SketchTool::CenterpointArc, start, pending, None); 161 + let (next, pending) = place(fresh(), SketchTool::CenterpointArc, end, pending, None); 162 + let Some(next) = next else { 163 + panic!("arc commits on third click") 164 + }; 165 + assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1); 166 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 3); 167 + assert_eq!(pending, None); 168 + } 169 + 170 + #[test] 171 + fn three_point_arc_emits_four_entities() { 172 + let start = Point2::from_mm(-5.0, 0.0); 173 + let end = Point2::from_mm(5.0, 0.0); 174 + let mid = Point2::from_mm(0.0, 5.0); 175 + let (_, pending) = place(fresh(), SketchTool::ThreePointArc, start, None, None); 176 + let (_, pending) = place(fresh(), SketchTool::ThreePointArc, end, pending, None); 177 + let (next, pending) = place(fresh(), SketchTool::ThreePointArc, mid, pending, None); 178 + let Some(next) = next else { 179 + panic!("3-point arc commits") 180 + }; 181 + assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1); 182 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 3); 183 + assert_eq!(pending, None); 184 + } 185 + 186 + #[test] 187 + fn tangent_arc_requires_endpoint_anchor() { 188 + let world = Point2::from_mm(1.0, 1.0); 189 + let (next, pending) = place(fresh(), SketchTool::TangentArc, world, None, None); 190 + assert!(next.is_none()); 191 + assert_eq!(pending, None); 192 + } 193 + 194 + #[test] 195 + fn tangent_arc_preview_with_no_pending_emits_snap_dot_when_snap_fires() { 196 + let (sketch, _a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 197 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 198 + let snap_b = SnapHit { 199 + kind: SnapKind::Endpoint(b), 200 + world: Point2::from_mm(10.0, 0.0), 201 + }; 202 + let prev = preview( 203 + &sketch, 204 + SketchTool::TangentArc, 205 + Point2::from_mm(10.0, 0.0), 206 + None, 207 + Some(snap_b), 208 + ); 209 + assert_eq!( 210 + prev.snap, 211 + Some(Point2::from_mm(10.0, 0.0)), 212 + "snap dot must surface for hover over endpoint", 213 + ); 214 + assert!(prev.anchors.is_empty(), "no anchor before first click"); 215 + assert!(prev.arcs.is_empty(), "no ghost arc before first click"); 216 + } 217 + 218 + #[test] 219 + fn tangent_arc_picks_minor_arc_when_cursor_on_other_side() { 220 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 221 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 222 + let (sketch, _line) = add_line(sketch, a, b, false); 223 + let snap_b = SnapHit { 224 + kind: SnapKind::Endpoint(b), 225 + world: Point2::from_mm(10.0, 0.0), 226 + }; 227 + let (_, pending) = place( 228 + sketch.clone(), 229 + SketchTool::TangentArc, 230 + Point2::from_mm(10.0, 0.0), 231 + None, 232 + Some(snap_b), 233 + ); 234 + let (next, _) = place( 235 + sketch, 236 + SketchTool::TangentArc, 237 + Point2::from_mm(15.0, -3.0), 238 + pending, 239 + None, 240 + ); 241 + let Some(next) = next else { 242 + panic!("tangent arc commits"); 243 + }; 244 + let sweep = arc_sweep(&next, super::only_arc(&next)); 245 + assert!( 246 + sweep <= std::f64::consts::PI + 1e-9, 247 + "sweep {sweep} should be minor arc", 248 + ); 249 + } 250 + 251 + #[test] 252 + fn tangent_arc_preview_picks_minor_arc_when_cursor_on_other_side() { 253 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 254 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 255 + let (sketch, _line) = add_line(sketch, a, b, false); 256 + let pending = Some(Pending::First(ClickAnchor::Endpoint(b))); 257 + let cursor = Point2::from_mm(15.0, -3.0); 258 + let prev = preview(&sketch, SketchTool::TangentArc, cursor, pending, None); 259 + assert_eq!(prev.arcs.len(), 1, "ghost arc emitted"); 260 + let sweep = prev.arcs[0].sweep_angle.get::<uom::si::angle::radian>(); 261 + assert!( 262 + sweep <= std::f64::consts::PI + 1e-9, 263 + "preview sweep {sweep} should be minor arc", 264 + ); 265 + } 266 + 267 + #[test] 268 + fn tangent_arc_rejects_first_click_on_orphan_point() { 269 + let (sketch, orphan) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 270 + let snap = SnapHit { 271 + kind: SnapKind::Endpoint(orphan), 272 + world: Point2::from_mm(0.0, 0.0), 273 + }; 274 + let (next, pending) = place( 275 + sketch, 276 + SketchTool::TangentArc, 277 + Point2::from_mm(0.0, 0.0), 278 + None, 279 + Some(snap), 280 + ); 281 + assert!(next.is_none()); 282 + assert_eq!(pending, None, "no pending when host curve is missing"); 283 + } 284 + 285 + #[test] 286 + fn tangent_arc_accepts_first_click_on_arc_endpoint() { 287 + let (sketch, center) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 288 + let (sketch, start) = add_point(sketch, Point2::from_mm(5.0, 0.0)); 289 + let (sketch, end) = add_point(sketch, Point2::from_mm(0.0, 5.0)); 290 + let (sketch, _arc) = add_arc(sketch, center, start, end, false); 291 + let snap = SnapHit { 292 + kind: SnapKind::Endpoint(start), 293 + world: Point2::from_mm(5.0, 0.0), 294 + }; 295 + let (_, pending) = place( 296 + sketch, 297 + SketchTool::TangentArc, 298 + Point2::from_mm(5.0, 0.0), 299 + None, 300 + Some(snap), 301 + ); 302 + assert!(matches!( 303 + pending, 304 + Some(Pending::First(ClickAnchor::Endpoint(id))) if id == start 305 + )); 306 + } 307 + 308 + #[test] 309 + fn tangent_arc_continues_minor_arc_off_arc_end_host() { 310 + let (sketch, host_center) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 311 + let (sketch, host_start) = add_point(sketch, Point2::from_mm(5.0, 0.0)); 312 + let (sketch, host_end) = add_point(sketch, Point2::from_mm(0.0, 5.0)); 313 + let (sketch, host_arc) = add_arc(sketch, host_center, host_start, host_end, false); 314 + let snap_join = SnapHit { 315 + kind: SnapKind::Endpoint(host_end), 316 + world: Point2::from_mm(0.0, 5.0), 317 + }; 318 + let (_, pending) = place( 319 + sketch.clone(), 320 + SketchTool::TangentArc, 321 + Point2::from_mm(0.0, 5.0), 322 + None, 323 + Some(snap_join), 324 + ); 325 + let (next, _) = place( 326 + sketch, 327 + SketchTool::TangentArc, 328 + Point2::from_mm(-3.0, 8.0), 329 + pending, 330 + None, 331 + ); 332 + let Some(next) = next else { 333 + panic!("tangent arc commits"); 334 + }; 335 + let Some((_, SketchEntity::Arc(arc))) = next 336 + .entities() 337 + .iter() 338 + .find(|(id, e)| *id != host_arc && matches!(e, SketchEntity::Arc(_))) 339 + else { 340 + panic!("new arc entity"); 341 + }; 342 + let sweep = arc_sweep(&next, *arc); 343 + assert!( 344 + sweep <= std::f64::consts::PI + 1e-9, 345 + "sweep {sweep} should be the minor arc, tangent continuity at host arc.end", 346 + ); 347 + } 348 + 349 + #[test] 350 + fn tangent_arc_drops_pending_on_no_op_second_click_at_start() { 351 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 352 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 353 + let (sketch, _line) = add_line(sketch, a, b, false); 354 + let snap_b = SnapHit { 355 + kind: SnapKind::Endpoint(b), 356 + world: Point2::from_mm(10.0, 0.0), 357 + }; 358 + let (_, pending) = place( 359 + sketch.clone(), 360 + SketchTool::TangentArc, 361 + Point2::from_mm(10.0, 0.0), 362 + None, 363 + Some(snap_b), 364 + ); 365 + let (next, kept) = place( 366 + sketch, 367 + SketchTool::TangentArc, 368 + Point2::from_mm(10.0, 0.0), 369 + pending, 370 + Some(snap_b), 371 + ); 372 + assert!( 373 + next.is_none(), 374 + "second-click on starting endpoint must not commit", 375 + ); 376 + assert!( 377 + matches!(kept, Some(Pending::First(ClickAnchor::Endpoint(id))) if id == b), 378 + "pending kept so the user can click again", 379 + ); 380 + } 381 + 382 + #[test] 383 + fn tangent_arc_emits_arc_and_relation() { 384 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 385 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 386 + let (sketch, _line) = add_line(sketch, a, b, false); 387 + let snap_b = SnapHit { 388 + kind: SnapKind::Endpoint(b), 389 + world: Point2::from_mm(10.0, 0.0), 390 + }; 391 + let (_, pending) = place( 392 + sketch.clone(), 393 + SketchTool::TangentArc, 394 + Point2::from_mm(10.0, 0.0), 395 + None, 396 + Some(snap_b), 397 + ); 398 + assert!(matches!( 399 + pending, 400 + Some(Pending::First(ClickAnchor::Endpoint(_))) 401 + )); 402 + let end = Point2::from_mm(10.0, 6.0); 403 + let (next, pending) = place(sketch, SketchTool::TangentArc, end, pending, None); 404 + let Some(next) = next else { 405 + panic!("tangent arc commits") 406 + }; 407 + assert_eq!(count_kind(&next, SketchEntityKind::Arc), 1); 408 + assert_eq!(pending, None); 409 + assert!( 410 + next.relation_order().iter().any(|rid| matches!( 411 + next.relations().get(*rid), 412 + Some(SketchRelation::Tangent(_, _)), 413 + )), 414 + "tangent relation emitted", 415 + ); 416 + }
+45
crates/bone-app/src/tools/tests/mod.rs
··· 1 + use bone_document::{ArcData, Sketch, SketchEntity, SketchEntityKind}; 2 + 3 + use crate::sketch_mode::Plane; 4 + 5 + mod basic; 6 + mod edge; 7 + mod rect; 8 + 9 + pub(super) use super::geometry::distance; 10 + 11 + pub(super) fn count_kind(sketch: &Sketch, kind: SketchEntityKind) -> usize { 12 + sketch 13 + .entities() 14 + .iter() 15 + .filter(|(_, e)| e.kind() == kind) 16 + .count() 17 + } 18 + 19 + pub(super) fn fresh() -> Sketch { 20 + Sketch::new(Plane::Xy.basis()) 21 + } 22 + 23 + pub(super) fn arc_sweep(sketch: &Sketch, arc: ArcData) -> f64 { 24 + let resolve = |id| match sketch.entities().get(id) { 25 + Some(SketchEntity::Point(p)) => p.at(), 26 + _ => panic!("point id {id:?} did not resolve"), 27 + }; 28 + let (cx, cy) = resolve(arc.center()).coords_mm(); 29 + let (sx, sy) = resolve(arc.start()).coords_mm(); 30 + let (ex, ey) = resolve(arc.end()).coords_mm(); 31 + let start_angle = (sy - cy).atan2(sx - cx); 32 + let end_angle = (ey - cy).atan2(ex - cx); 33 + (end_angle - start_angle).rem_euclid(std::f64::consts::TAU) 34 + } 35 + 36 + pub(super) fn only_arc(sketch: &Sketch) -> ArcData { 37 + let Some((_, SketchEntity::Arc(arc))) = sketch 38 + .entities() 39 + .iter() 40 + .find(|(_, e)| matches!(e, SketchEntity::Arc(_))) 41 + else { 42 + panic!("arc entity"); 43 + }; 44 + *arc 45 + }