Another project
0

Configure Feed

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

feat(app): tools dispatch

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

author
Lewis
date (May 10, 2026, 7:29 PM +0300) commit 70ab8c4e parent 882f9cb0 change-id zmopqqov
+192 -469
+160 -455
crates/bone-app/src/main.rs
··· 3 3 use std::path::{Path, PathBuf}; 4 4 use std::sync::Arc; 5 5 6 - use bone_document::{ 7 - Document, EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation, UndoStack, 8 - }; 6 + use bone_document::{Document, Sketch, SketchEdit, SketchEntity, UndoStack}; 9 7 use bone_render::{ 10 8 Camera2, ChromeInstance, ChromePipeline, ChromeTextPipeline, PickQuery, PickedItem, 11 9 PixelsPerMm, RenderTargets, SketchPreview, SketchRenderer, SketchScene, Style, SurfaceContext, ··· 44 42 mod sketch_mode; 45 43 mod snap; 46 44 mod strings; 45 + mod tools; 47 46 48 - use sketch_mode::{Mode, Pending, Plane, SketchSession, SketchTool}; 49 - use snap::{Anchor, SnapHit, SnapKind}; 47 + use sketch_mode::{ClickAnchor, Mode, Pending, Plane, SketchSession, SketchTool}; 48 + use snap::{Anchor, SnapHit}; 50 49 51 50 #[derive(Debug, thiserror::Error)] 52 51 enum AppError { ··· 204 203 input: InputState, 205 204 } 206 205 207 - fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) { 208 - let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 209 - SketchEntity::point(Point2::from_mm(x, y)), 210 - )) else { 211 - unreachable!("AddEntity(Point) on fresh sketch yields Entity outcome"); 212 - }; 213 - (next, id) 214 - } 215 - 216 - fn add_line(sketch: Sketch, a: SketchEntityId, b: SketchEntityId) -> Sketch { 217 - add_line_with_id(sketch, a, b).0 218 - } 219 - 220 - fn add_line_with_id( 221 - sketch: Sketch, 222 - a: SketchEntityId, 223 - b: SketchEntityId, 224 - ) -> (Sketch, SketchEntityId) { 225 - let Ok((next, EditOutcome::Entity(id))) = 226 - sketch.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 227 - else { 228 - unreachable!("AddEntity(Line) referencing extant points yields Entity outcome"); 229 - }; 230 - (next, id) 231 - } 232 - 233 - fn add_relation(sketch: Sketch, relation: SketchRelation) -> Sketch { 234 - match sketch.clone().apply(SketchEdit::AddRelation(relation)) { 235 - Ok((next, _)) => next, 236 - Err(e) => { 237 - tracing::warn!(error = %e, ?relation, "snap inferred relation rejected; skipping"); 238 - sketch 239 - } 240 - } 241 - } 242 - 243 - fn add_circle(sketch: Sketch, center: SketchEntityId, radius_mm: f64) -> Sketch { 244 - let Ok((next, _)) = sketch.apply(SketchEdit::AddEntity(SketchEntity::circle( 245 - center, 246 - Length::new::<millimeter>(radius_mm), 247 - false, 248 - ))) else { 249 - unreachable!("AddEntity(Circle) referencing freshly added center succeeds"); 250 - }; 251 - next 252 - } 253 - 254 - fn add_arc( 255 - sketch: Sketch, 256 - center: SketchEntityId, 257 - start: SketchEntityId, 258 - end: SketchEntityId, 259 - ) -> Sketch { 260 - let Ok((next, _)) = sketch.apply(SketchEdit::AddEntity(SketchEntity::arc( 261 - center, start, end, false, 262 - ))) else { 263 - unreachable!("AddEntity(Arc) referencing freshly added points succeeds"); 264 - }; 265 - next 266 - } 267 - 268 206 fn default_sketch() -> Sketch { 269 207 let sketch = Sketch::new(Plane::Xy.basis()); 270 - let (sketch, p0) = add_point(sketch, -20.0, -12.5); 271 - let (sketch, p1) = add_point(sketch, 20.0, -12.5); 272 - let (sketch, p2) = add_point(sketch, 20.0, 12.5); 273 - let (sketch, p3) = add_point(sketch, -20.0, 12.5); 274 - let sketch = add_line(sketch, p0, p1); 275 - let sketch = add_line(sketch, p1, p2); 276 - let sketch = add_line(sketch, p2, p3); 277 - let sketch = add_line(sketch, p3, p0); 278 - let (sketch, origin) = add_point(sketch, 0.0, 0.0); 279 - let sketch = add_circle(sketch, origin, 5.0); 280 - let (sketch, arc_center) = add_point(sketch, -12.0, 0.0); 281 - let (sketch, arc_start) = add_point(sketch, -8.0, 0.0); 282 - let (sketch, arc_end) = add_point(sketch, -12.0, 4.0); 283 - add_arc(sketch, arc_center, arc_start, arc_end) 208 + let (sketch, p0) = tools::add_point(sketch, Point2::from_mm(-20.0, -12.5)); 209 + let (sketch, p1) = tools::add_point(sketch, Point2::from_mm(20.0, -12.5)); 210 + let (sketch, p2) = tools::add_point(sketch, Point2::from_mm(20.0, 12.5)); 211 + let (sketch, p3) = tools::add_point(sketch, Point2::from_mm(-20.0, 12.5)); 212 + let (sketch, _) = tools::add_line(sketch, p0, p1, false); 213 + let (sketch, _) = tools::add_line(sketch, p1, p2, false); 214 + let (sketch, _) = tools::add_line(sketch, p2, p3, false); 215 + let (sketch, _) = tools::add_line(sketch, p3, p0, false); 216 + let (sketch, origin) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0)); 217 + let (sketch, _) = tools::add_circle(sketch, origin, Length::new::<millimeter>(5.0), false); 218 + let (sketch, arc_center) = tools::add_point(sketch, Point2::from_mm(-12.0, 0.0)); 219 + let (sketch, arc_start) = tools::add_point(sketch, Point2::from_mm(-8.0, 0.0)); 220 + let (sketch, arc_end) = tools::add_point(sketch, Point2::from_mm(-12.0, 4.0)); 221 + let (sketch, _) = tools::add_arc(sketch, arc_center, arc_start, arc_end, false); 222 + sketch 284 223 } 285 224 286 225 fn initial_document(sketch: Sketch) -> (Document, SketchId) { ··· 379 318 )) 380 319 } 381 320 382 - fn place_in_sketch( 383 - sketch: Sketch, 384 - tool: SketchTool, 385 - world: Point2, 386 - pending: Option<Pending>, 387 - snap: Option<SnapHit>, 388 - ) -> (Option<Sketch>, Option<Pending>) { 389 - let (wx, wy) = world.coords_mm(); 390 - match tool { 391 - SketchTool::Point if matches!(snap.map(|s| s.kind), Some(SnapKind::Endpoint(_))) => { 392 - (None, None) 393 - } 394 - SketchTool::Point => { 395 - let (next, _) = add_point(sketch, wx, wy); 396 - (Some(next), None) 397 - } 398 - SketchTool::Line => match pending { 399 - None => first_click_line(sketch, world, snap), 400 - Some(prev) => second_click_line(sketch, world, prev, snap), 401 - }, 402 - _ => (None, pending), 403 - } 404 - } 405 - 406 - fn first_click_line( 407 - sketch: Sketch, 408 - world: Point2, 409 - snap: Option<SnapHit>, 410 - ) -> (Option<Sketch>, Option<Pending>) { 411 - match snap.map(|s| s.kind) { 412 - Some(SnapKind::Endpoint(id)) => (None, Some(Pending::Endpoint(id))), 413 - Some(SnapKind::Midpoint(line)) => { 414 - let snap_world = snap.map_or(world, |s| s.world); 415 - let (sx, sy) = snap_world.coords_mm(); 416 - let (sketch, point) = add_point(sketch, sx, sy); 417 - let sketch = add_relation(sketch, SketchRelation::Midpoint { point, line }); 418 - (Some(sketch), Some(Pending::Endpoint(point))) 419 - } 420 - None | Some(SnapKind::Tangent(_) | SnapKind::Horizontal | SnapKind::Vertical) => ( 421 - None, 422 - Some(Pending::Position(snap.map_or(world, |s| s.world))), 423 - ), 424 - } 425 - } 426 - 427 - fn second_click_line( 428 - sketch: Sketch, 429 - world: Point2, 430 - pending: Pending, 431 - snap: Option<SnapHit>, 432 - ) -> (Option<Sketch>, Option<Pending>) { 433 - let (sketch, prev_id) = match pending { 434 - Pending::Position(start) => { 435 - let (sx, sy) = start.coords_mm(); 436 - add_point(sketch, sx, sy) 437 - } 438 - Pending::Endpoint(id) => (sketch, id), 439 - }; 440 - let snap_world = snap.map_or(world, |s| s.world); 441 - let (sx, sy) = snap_world.coords_mm(); 442 - let (sketch, end_id) = match snap.map(|s| s.kind) { 443 - Some(SnapKind::Endpoint(id)) => (sketch, id), 444 - Some(_) | None => add_point(sketch, sx, sy), 445 - }; 446 - if end_id == prev_id { 447 - return (Some(sketch), Some(Pending::Endpoint(prev_id))); 448 - } 449 - let (sketch, line_id) = add_line_with_id(sketch, prev_id, end_id); 450 - let sketch = match snap.map(|s| s.kind) { 451 - Some(SnapKind::Midpoint(line)) => add_relation( 452 - sketch, 453 - SketchRelation::Midpoint { 454 - point: end_id, 455 - line, 456 - }, 457 - ), 458 - Some(SnapKind::Tangent(curve)) => { 459 - add_relation(sketch, SketchRelation::Tangent(line_id, curve)) 460 - } 461 - Some(SnapKind::Horizontal) => add_relation(sketch, SketchRelation::Horizontal(line_id)), 462 - Some(SnapKind::Vertical) => add_relation(sketch, SketchRelation::Vertical(line_id)), 463 - Some(SnapKind::Endpoint(_)) | None => sketch, 464 - }; 465 - (Some(sketch), Some(Pending::Endpoint(end_id))) 466 - } 467 - 468 321 fn try_place(state: &mut RenderState, world: Point2) { 469 322 let Mode::Sketch { 470 323 sketch_id, ··· 482 335 return; 483 336 }; 484 337 let snap = match tool { 485 - SketchTool::Line => compute_snap(&sketch, &state.camera, world, prev_pending), 486 - SketchTool::Point => compute_endpoint_snap(&sketch, &state.camera, world), 487 - _ => None, 338 + SketchTool::Line => { 339 + compute_snap(&sketch, &state.camera, world, latest_anchor(prev_pending)) 340 + } 341 + _ => compute_endpoint_snap(&sketch, &state.camera, world), 488 342 }; 489 - let (next_sketch, new_pending) = place_in_sketch(sketch, tool, world, prev_pending, snap); 343 + let (next_sketch, new_pending) = tools::place(sketch, tool, world, prev_pending, snap); 490 344 if let Some(next) = next_sketch { 491 345 state.undo.record(state.document.clone()); 492 346 state.document.replace_sketch(sketch_id, next); ··· 503 357 } = state.mode 504 358 { 505 359 session.pending = new_pending; 360 + } 361 + } 362 + 363 + const fn latest_anchor(pending: Option<Pending>) -> Option<ClickAnchor> { 364 + match pending { 365 + None => None, 366 + Some(Pending::First(a) | Pending::Second(_, a)) => Some(a), 506 367 } 507 368 } 508 369 ··· 622 483 sketch_id, 623 484 session: 624 485 SketchSession { 625 - tool: Some(SketchTool::Line), 486 + tool: Some(tool), 626 487 pending, 627 488 drag: None, 628 489 }, ··· 630 491 else { 631 492 return SketchPreview::empty(); 632 493 }; 633 - let snap = cursor_world.and_then(|cursor| { 634 - document 635 - .sketch(*sketch_id) 636 - .and_then(|sketch| compute_snap(sketch, camera, cursor, *pending)) 637 - }); 638 - let snap_world = snap.map(|s| s.world); 639 - let Some(start) = pending 640 - .and_then(|p| resolve_anchor(document.sketch(*sketch_id), p)) 641 - .map(|a| a.pos) 642 - else { 643 - return SketchPreview { 644 - anchor: None, 645 - rubber_band: None, 646 - snap: snap_world, 647 - }; 494 + let Some(sketch) = document.sketch(*sketch_id) else { 495 + return SketchPreview::empty(); 648 496 }; 649 497 let Some(cursor) = cursor_world else { 650 - return SketchPreview { 651 - anchor: Some(start), 652 - rubber_band: None, 653 - snap: snap_world, 654 - }; 498 + return tools::preview_anchors_only(sketch, *pending); 655 499 }; 656 - let endpoint = snap_world.unwrap_or(cursor); 657 - SketchPreview { 658 - anchor: Some(start), 659 - rubber_band: Some((start, endpoint)), 660 - snap: snap_world, 661 - } 500 + let snap = match tool { 501 + SketchTool::Line => compute_snap(sketch, camera, cursor, latest_anchor(*pending)), 502 + _ => compute_endpoint_snap(sketch, camera, cursor), 503 + }; 504 + tools::preview(sketch, *tool, cursor, *pending, snap) 662 505 } 663 506 664 507 fn snap_tolerance(camera: &Camera2) -> Option<Length> { ··· 674 517 sketch: &Sketch, 675 518 camera: &Camera2, 676 519 cursor_world: Point2, 677 - pending: Option<Pending>, 520 + click: Option<ClickAnchor>, 678 521 ) -> Option<SnapHit> { 679 522 snap::detect( 680 523 cursor_world, 681 - pending.and_then(|p| resolve_anchor(Some(sketch), p)), 524 + click.and_then(|c| resolve_anchor(Some(sketch), c)), 682 525 sketch, 683 526 snap_tolerance(camera)?, 684 527 ) ··· 692 535 snap::detect_endpoint_only(cursor_world, sketch, snap_tolerance(camera)?) 693 536 } 694 537 695 - fn resolve_anchor(sketch: Option<&Sketch>, pending: Pending) -> Option<Anchor> { 696 - match pending { 697 - Pending::Position(pos) => Some(Anchor { pos, id: None }), 698 - Pending::Endpoint(id) => match sketch?.entities().get(id)? { 538 + fn resolve_anchor(sketch: Option<&Sketch>, click: ClickAnchor) -> Option<Anchor> { 539 + match click { 540 + ClickAnchor::Position(pos) | ClickAnchor::MidpointOf { position: pos, .. } => { 541 + Some(Anchor { pos, id: None }) 542 + } 543 + ClickAnchor::Endpoint(id) => match sketch?.entities().get(id)? { 699 544 SketchEntity::Point(p) => Some(Anchor { 700 545 pos: p.at(), 701 546 id: Some(id), ··· 1397 1242 #[cfg(test)] 1398 1243 mod tests { 1399 1244 use super::*; 1400 - use bone_document::SketchEntityKind; 1401 - 1402 - fn count_kind(sketch: &Sketch, kind: SketchEntityKind) -> usize { 1403 - sketch 1404 - .entities() 1405 - .iter() 1406 - .filter(|(_, e)| e.kind() == kind) 1407 - .count() 1408 - } 1409 - 1410 - #[test] 1411 - fn point_tool_commits_a_point_per_click() { 1412 - let sketch = Sketch::new(Plane::Xy.basis()); 1413 - let (next, pending) = place_in_sketch( 1414 - sketch, 1415 - SketchTool::Point, 1416 - Point2::from_mm(3.0, 4.0), 1417 - None, 1418 - None, 1419 - ); 1420 - let Some(next) = next else { 1421 - panic!("point tool must commit"); 1422 - }; 1423 - assert_eq!(count_kind(&next, SketchEntityKind::Point), 1); 1424 - assert_eq!(pending, None); 1425 - } 1426 - 1427 - #[test] 1428 - fn point_tool_endpoint_snap_suppresses_duplicate() { 1429 - let sketch = Sketch::new(Plane::Xy.basis()); 1430 - let (sketch, existing) = add_point(sketch, 1.0, 1.0); 1431 - let (next, pending) = place_in_sketch( 1432 - sketch, 1433 - SketchTool::Point, 1434 - Point2::from_mm(1.05, 1.0), 1435 - None, 1436 - Some(SnapHit { 1437 - kind: SnapKind::Endpoint(existing), 1438 - world: Point2::from_mm(1.0, 1.0), 1439 - }), 1440 - ); 1441 - assert!(next.is_none(), "endpoint snap must suppress new point"); 1442 - assert_eq!(pending, None); 1443 - } 1444 - 1445 - #[test] 1446 - fn line_tool_first_click_pends_anchor_only() { 1447 - let sketch = Sketch::new(Plane::Xy.basis()); 1448 - let world = Point2::from_mm(1.0, 2.0); 1449 - let (next, pending) = place_in_sketch(sketch, SketchTool::Line, world, None, None); 1450 - assert!(next.is_none()); 1451 - assert_eq!(pending, Some(Pending::Position(world))); 1452 - } 1453 - 1454 - #[test] 1455 - fn line_tool_second_click_emits_two_points_and_a_line() { 1456 - let sketch = Sketch::new(Plane::Xy.basis()); 1457 - let start = Point2::from_mm(0.0, 0.0); 1458 - let end = Point2::from_mm(5.0, 5.0); 1459 - let (next, pending) = place_in_sketch( 1460 - sketch, 1461 - SketchTool::Line, 1462 - end, 1463 - Some(Pending::Position(start)), 1464 - None, 1465 - ); 1466 - let Some(next) = next else { 1467 - panic!("second click must commit"); 1468 - }; 1469 - assert_eq!(count_kind(&next, SketchEntityKind::Point), 2); 1470 - assert_eq!(count_kind(&next, SketchEntityKind::Line), 1); 1471 - let Some(Pending::Endpoint(_)) = pending else { 1472 - panic!("second click hands back endpoint to chain"); 1473 - }; 1474 - } 1475 - 1476 - #[test] 1477 - fn line_tool_chain_reuses_endpoint_no_duplicate_point() { 1478 - let sketch = Sketch::new(Plane::Xy.basis()); 1479 - let (sketch, anchor_id) = add_point(sketch, 0.0, 0.0); 1480 - let (next, pending) = place_in_sketch( 1481 - sketch, 1482 - SketchTool::Line, 1483 - Point2::from_mm(5.0, 0.0), 1484 - Some(Pending::Endpoint(anchor_id)), 1485 - None, 1486 - ); 1487 - let Some(next) = next else { 1488 - panic!("chain click commits"); 1489 - }; 1490 - assert_eq!(count_kind(&next, SketchEntityKind::Point), 2); 1491 - assert_eq!(count_kind(&next, SketchEntityKind::Line), 1); 1492 - let Some(Pending::Endpoint(new_id)) = pending else { 1493 - panic!("chain click hands back new endpoint"); 1494 - }; 1495 - assert_ne!(new_id, anchor_id); 1496 - } 1497 - 1498 - #[test] 1499 - fn unsupported_tool_is_a_noop_and_preserves_pending() { 1500 - let sketch = Sketch::new(Plane::Xy.basis()); 1501 - let pending = Some(Pending::Position(Point2::from_mm(1.0, 1.0))); 1502 - let (next, kept) = place_in_sketch( 1503 - sketch, 1504 - SketchTool::Circle, 1505 - Point2::from_mm(2.0, 2.0), 1506 - pending, 1507 - None, 1508 - ); 1509 - assert!(next.is_none()); 1510 - assert_eq!(kept, pending); 1511 - } 1512 - 1513 - fn snap_endpoint(id: SketchEntityId, world: Point2) -> SnapHit { 1514 - SnapHit { 1515 - kind: SnapKind::Endpoint(id), 1516 - world, 1517 - } 1518 - } 1519 - 1520 - fn snap_midpoint(line: SketchEntityId, world: Point2) -> SnapHit { 1521 - SnapHit { 1522 - kind: SnapKind::Midpoint(line), 1523 - world, 1524 - } 1525 - } 1526 - 1527 - fn snap_axis(kind: SnapKind, world: Point2) -> SnapHit { 1528 - SnapHit { kind, world } 1529 - } 1530 - 1531 - #[test] 1532 - fn line_first_click_endpoint_snap_pends_existing_id_no_edit() { 1533 - let sketch = Sketch::new(Plane::Xy.basis()); 1534 - let (sketch, existing) = add_point(sketch, 1.0, 1.0); 1535 - let (next, pending) = place_in_sketch( 1536 - sketch, 1537 - SketchTool::Line, 1538 - Point2::from_mm(1.05, 1.05), 1539 - None, 1540 - Some(snap_endpoint(existing, Point2::from_mm(1.0, 1.0))), 1541 - ); 1542 - assert!(next.is_none()); 1543 - assert_eq!(pending, Some(Pending::Endpoint(existing))); 1544 - } 1545 - 1546 - #[test] 1547 - fn line_first_click_midpoint_snap_emits_point_and_relation() { 1548 - let sketch = Sketch::new(Plane::Xy.basis()); 1549 - let (sketch, a) = add_point(sketch, 0.0, 0.0); 1550 - let (sketch, b) = add_point(sketch, 10.0, 0.0); 1551 - let Ok((sketch, EditOutcome::Entity(line_id))) = 1552 - sketch.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) 1553 - else { 1554 - panic!("seed line"); 1555 - }; 1556 - let (next, pending) = place_in_sketch( 1557 - sketch, 1558 - SketchTool::Line, 1559 - Point2::from_mm(5.0, 0.0), 1560 - None, 1561 - Some(snap_midpoint(line_id, Point2::from_mm(5.0, 0.0))), 1562 - ); 1563 - let Some(next) = next else { 1564 - panic!("midpoint snap must materialise point + relation"); 1565 - }; 1566 - assert_eq!(count_kind(&next, SketchEntityKind::Point), 3); 1567 - assert_eq!(next.relation_order().len(), 1); 1568 - let Some(Pending::Endpoint(_)) = pending else { 1569 - panic!("first-click midpoint pends the materialised point"); 1570 - }; 1571 - } 1572 - 1573 - #[test] 1574 - fn line_second_click_endpoint_snap_does_not_duplicate_point() { 1575 - let sketch = Sketch::new(Plane::Xy.basis()); 1576 - let (sketch, existing) = add_point(sketch, 5.0, 0.0); 1577 - let start = Point2::from_mm(0.0, 0.0); 1578 - let (next, pending) = place_in_sketch( 1579 - sketch, 1580 - SketchTool::Line, 1581 - Point2::from_mm(5.05, 0.05), 1582 - Some(Pending::Position(start)), 1583 - Some(snap_endpoint(existing, Point2::from_mm(5.0, 0.0))), 1584 - ); 1585 - let Some(next) = next else { 1586 - panic!("commit"); 1587 - }; 1588 - assert_eq!( 1589 - count_kind(&next, SketchEntityKind::Point), 1590 - 2, 1591 - "endpoint snap reuses existing id, only adds the start point", 1592 - ); 1593 - assert_eq!(count_kind(&next, SketchEntityKind::Line), 1); 1594 - assert_eq!(pending, Some(Pending::Endpoint(existing))); 1595 - } 1596 - 1597 - #[test] 1598 - fn line_second_click_horizontal_snap_emits_horizontal_relation() { 1599 - let sketch = Sketch::new(Plane::Xy.basis()); 1600 - let start = Point2::from_mm(0.0, 0.0); 1601 - let snapped = Point2::from_mm(5.0, 0.0); 1602 - let (next, _) = place_in_sketch( 1603 - sketch, 1604 - SketchTool::Line, 1605 - Point2::from_mm(5.0, 0.05), 1606 - Some(Pending::Position(start)), 1607 - Some(snap_axis(SnapKind::Horizontal, snapped)), 1608 - ); 1609 - let Some(next) = next else { 1610 - panic!("commit"); 1611 - }; 1612 - assert_eq!(next.relation_order().len(), 1); 1613 - } 1614 - 1615 - #[test] 1616 - fn line_second_click_endpoint_back_to_anchor_skips_zero_length_line() { 1617 - let sketch = Sketch::new(Plane::Xy.basis()); 1618 - let (sketch, anchor) = add_point(sketch, 0.0, 0.0); 1619 - let (next, pending) = place_in_sketch( 1620 - sketch, 1621 - SketchTool::Line, 1622 - Point2::from_mm(0.05, 0.05), 1623 - Some(Pending::Endpoint(anchor)), 1624 - Some(snap_endpoint(anchor, Point2::from_mm(0.0, 0.0))), 1625 - ); 1626 - let Some(next) = next else { 1627 - panic!("zero-length must not error"); 1628 - }; 1629 - assert_eq!(count_kind(&next, SketchEntityKind::Line), 0); 1630 - assert_eq!(pending, Some(Pending::Endpoint(anchor))); 1631 - } 1632 1245 1633 1246 #[test] 1634 1247 fn cursor_to_world_at_window_center_equals_camera_pan() { ··· 1741 1354 sketch_id: SketchId::default(), 1742 1355 session: SketchSession { 1743 1356 tool: Some(SketchTool::Line), 1744 - pending: Some(Pending::Position(Point2::from_mm(1.0, 2.0))), 1357 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1358 + 1.0, 2.0, 1359 + )))), 1745 1360 drag: None, 1746 1361 }, 1747 1362 }; ··· 1779 1394 } 1780 1395 1781 1396 #[test] 1782 - fn build_preview_without_armed_line_tool_is_empty() { 1397 + fn build_preview_without_armed_tool_is_empty() { 1783 1398 let document = Document::new(DocumentId::default(), "doc".to_owned()); 1784 1399 let mode = Mode::enter_sketch(SketchId::default()); 1785 1400 let preview = build_preview( ··· 1792 1407 } 1793 1408 1794 1409 #[test] 1795 - fn build_preview_with_position_pending_emits_anchor_and_rubber_band() { 1796 - let document = Document::new(DocumentId::default(), "doc".to_owned()); 1410 + fn build_preview_with_position_pending_emits_anchor_and_segment() { 1411 + let sketch = Sketch::new(Plane::Xy.basis()); 1412 + let (document, sketch_id) = initial_document(sketch); 1797 1413 let anchor = Point2::from_mm(2.0, 3.0); 1798 1414 let mode = Mode::Sketch { 1799 - sketch_id: SketchId::default(), 1415 + sketch_id, 1800 1416 session: SketchSession { 1801 1417 tool: Some(SketchTool::Line), 1802 - pending: Some(Pending::Position(anchor)), 1418 + pending: Some(Pending::First(ClickAnchor::Position(anchor))), 1803 1419 drag: None, 1804 1420 }, 1805 1421 }; 1806 1422 let cursor = Point2::from_mm(5.0, 7.0); 1807 1423 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1808 - assert_eq!(preview.anchor, Some(anchor)); 1809 - assert_eq!(preview.rubber_band, Some((anchor, cursor))); 1424 + assert_eq!(preview.anchors, vec![anchor]); 1425 + assert_eq!(preview.segments, vec![(anchor, cursor)]); 1810 1426 } 1811 1427 1812 1428 #[test] 1813 1429 fn build_preview_with_endpoint_pending_resolves_via_document() { 1814 1430 let sketch = Sketch::new(Plane::Xy.basis()); 1815 1431 let target = Point2::from_mm(-4.0, 6.0); 1816 - let (sketch, endpoint) = add_point(sketch, -4.0, 6.0); 1432 + let (sketch, endpoint) = tools::add_point(sketch, target); 1817 1433 let (document, sketch_id) = initial_document(sketch); 1818 1434 let mode = Mode::Sketch { 1819 1435 sketch_id, 1820 1436 session: SketchSession { 1821 1437 tool: Some(SketchTool::Line), 1822 - pending: Some(Pending::Endpoint(endpoint)), 1438 + pending: Some(Pending::First(ClickAnchor::Endpoint(endpoint))), 1823 1439 drag: None, 1824 1440 }, 1825 1441 }; 1826 1442 let cursor = Point2::from_mm(0.0, 0.0); 1827 1443 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1828 - assert_eq!(preview.anchor, Some(target)); 1829 - assert_eq!(preview.rubber_band, Some((target, cursor))); 1444 + assert_eq!(preview.anchors, vec![target]); 1445 + assert_eq!(preview.segments, vec![(target, cursor)]); 1830 1446 } 1831 1447 1832 1448 #[test] 1833 - fn build_preview_drops_rubber_band_when_cursor_outside_viewport() { 1834 - let document = Document::new(DocumentId::default(), "doc".to_owned()); 1449 + fn build_preview_keeps_anchor_when_cursor_outside_viewport() { 1450 + let sketch = Sketch::new(Plane::Xy.basis()); 1451 + let (document, sketch_id) = initial_document(sketch); 1835 1452 let anchor = Point2::from_mm(1.0, 1.0); 1836 1453 let mode = Mode::Sketch { 1837 - sketch_id: SketchId::default(), 1454 + sketch_id, 1838 1455 session: SketchSession { 1839 1456 tool: Some(SketchTool::Line), 1840 - pending: Some(Pending::Position(anchor)), 1457 + pending: Some(Pending::First(ClickAnchor::Position(anchor))), 1841 1458 drag: None, 1842 1459 }, 1843 1460 }; 1844 1461 let preview = build_preview(&mode, &document, None, &far_camera()); 1845 - assert_eq!(preview.anchor, Some(anchor)); 1846 - assert_eq!(preview.rubber_band, None); 1462 + assert_eq!(preview.anchors, vec![anchor]); 1463 + assert!(preview.segments.is_empty()); 1847 1464 } 1848 1465 1849 1466 #[test] ··· 1853 1470 sketch_id: SketchId::default(), 1854 1471 session: SketchSession { 1855 1472 tool: Some(SketchTool::Line), 1856 - pending: Some(Pending::Position(Point2::from_mm(0.0, 0.0))), 1473 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1474 + 0.0, 0.0, 1475 + )))), 1857 1476 drag: Some(SketchEntityId::default()), 1858 1477 }, 1859 1478 }; ··· 1867 1486 } 1868 1487 1869 1488 #[test] 1489 + fn build_preview_circle_emits_ghost_circle() { 1490 + let sketch = Sketch::new(Plane::Xy.basis()); 1491 + let (document, sketch_id) = initial_document(sketch); 1492 + let center = Point2::from_mm(0.0, 0.0); 1493 + let mode = Mode::Sketch { 1494 + sketch_id, 1495 + session: SketchSession { 1496 + tool: Some(SketchTool::Circle), 1497 + pending: Some(Pending::First(ClickAnchor::Position(center))), 1498 + drag: None, 1499 + }, 1500 + }; 1501 + let cursor = Point2::from_mm(3.0, 4.0); 1502 + let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1503 + assert_eq!(preview.anchors, vec![center]); 1504 + assert_eq!(preview.circles.len(), 1); 1505 + let r = preview.circles[0].radius.get::<millimeter>(); 1506 + assert!((r - 5.0).abs() < 1e-9, "r={r}"); 1507 + } 1508 + 1509 + #[test] 1510 + fn build_preview_corner_rectangle_emits_four_segments() { 1511 + let sketch = Sketch::new(Plane::Xy.basis()); 1512 + let (document, sketch_id) = initial_document(sketch); 1513 + let corner = Point2::from_mm(0.0, 0.0); 1514 + let mode = Mode::Sketch { 1515 + sketch_id, 1516 + session: SketchSession { 1517 + tool: Some(SketchTool::CornerRectangle), 1518 + pending: Some(Pending::First(ClickAnchor::Position(corner))), 1519 + drag: None, 1520 + }, 1521 + }; 1522 + let cursor = Point2::from_mm(5.0, 3.0); 1523 + let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1524 + assert_eq!(preview.anchors, vec![corner]); 1525 + assert_eq!(preview.segments.len(), 4); 1526 + } 1527 + 1528 + #[test] 1529 + fn build_preview_tangent_arc_emits_ghost_arc_after_endpoint_click() { 1530 + let sketch = Sketch::new(Plane::Xy.basis()); 1531 + let (sketch, a) = tools::add_point(sketch, Point2::from_mm(0.0, 0.0)); 1532 + let (sketch, b) = tools::add_point(sketch, Point2::from_mm(10.0, 0.0)); 1533 + let (sketch, _) = tools::add_line(sketch, a, b, false); 1534 + let (document, sketch_id) = initial_document(sketch); 1535 + let mode = Mode::Sketch { 1536 + sketch_id, 1537 + session: SketchSession { 1538 + tool: Some(SketchTool::TangentArc), 1539 + pending: Some(Pending::First(ClickAnchor::Endpoint(b))), 1540 + drag: None, 1541 + }, 1542 + }; 1543 + let cursor = Point2::from_mm(10.0, 6.0); 1544 + let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1545 + assert_eq!(preview.anchors.len(), 1, "start anchor visible"); 1546 + assert_eq!(preview.arcs.len(), 1, "ghost arc emitted"); 1547 + } 1548 + 1549 + #[test] 1550 + fn build_preview_centerpoint_arc_emits_ghost_arc_after_two_clicks() { 1551 + let sketch = Sketch::new(Plane::Xy.basis()); 1552 + let (document, sketch_id) = initial_document(sketch); 1553 + let center = Point2::from_mm(0.0, 0.0); 1554 + let start = Point2::from_mm(5.0, 0.0); 1555 + let mode = Mode::Sketch { 1556 + sketch_id, 1557 + session: SketchSession { 1558 + tool: Some(SketchTool::CenterpointArc), 1559 + pending: Some(Pending::Second( 1560 + ClickAnchor::Position(center), 1561 + ClickAnchor::Position(start), 1562 + )), 1563 + drag: None, 1564 + }, 1565 + }; 1566 + let cursor = Point2::from_mm(0.0, 5.0); 1567 + let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); 1568 + assert_eq!(preview.arcs.len(), 1); 1569 + assert_eq!(preview.anchors.len(), 2); 1570 + } 1571 + 1572 + #[test] 1870 1573 fn escape_with_armed_tool_no_pending_disarms_tool() { 1871 1574 let prev = Mode::enter_sketch(SketchId::default()).arm_tool(SketchTool::Line); 1872 1575 let next = next_mode( ··· 1916 1619 sketch_id: SketchId::default(), 1917 1620 session: SketchSession { 1918 1621 tool: Some(SketchTool::Line), 1919 - pending: Some(Pending::Position(Point2::from_mm(0.0, 0.0))), 1622 + pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1623 + 0.0, 0.0, 1624 + )))), 1920 1625 drag: None, 1921 1626 }, 1922 1627 };
+32 -14
crates/bone-app/src/snap.rs
··· 38 38 } 39 39 40 40 #[must_use] 41 - pub fn detect_endpoint_only( 42 - cursor: Point2, 43 - sketch: &Sketch, 44 - tolerance: Length, 45 - ) -> Option<SnapHit> { 41 + pub fn detect_endpoint_only(cursor: Point2, sketch: &Sketch, tolerance: Length) -> Option<SnapHit> { 46 42 endpoint_snap(cursor, None, sketch, finite_tol(tolerance)?) 47 43 } 48 44 ··· 374 370 fn horizontal_axis_with_anchor() { 375 371 let sketch = Sketch::new(xy_basis()); 376 372 let cursor = Point2::from_mm(5.0, 0.05); 377 - let Some(hit) = detect(cursor, Some(anchor_at(Point2::from_mm(0.0, 0.0))), &sketch, tol()) 378 - else { 373 + let Some(hit) = detect( 374 + cursor, 375 + Some(anchor_at(Point2::from_mm(0.0, 0.0))), 376 + &sketch, 377 + tol(), 378 + ) else { 379 379 panic!("expected horizontal snap"); 380 380 }; 381 381 assert_eq!(hit.kind, SnapKind::Horizontal); ··· 388 388 fn vertical_axis_with_anchor() { 389 389 let sketch = Sketch::new(xy_basis()); 390 390 let cursor = Point2::from_mm(0.05, 5.0); 391 - let Some(hit) = detect(cursor, Some(anchor_at(Point2::from_mm(0.0, 0.0))), &sketch, tol()) 392 - else { 391 + let Some(hit) = detect( 392 + cursor, 393 + Some(anchor_at(Point2::from_mm(0.0, 0.0))), 394 + &sketch, 395 + tol(), 396 + ) else { 393 397 panic!("expected vertical snap"); 394 398 }; 395 399 assert_eq!(hit.kind, SnapKind::Vertical); ··· 418 422 let (sketch, c) = add_point(sketch, 5.0, 0.0); 419 423 let (sketch, circle) = add_circle(sketch, c, 3.0); 420 424 let cursor = Point2::from_mm(3.2, 2.4); 421 - let Some(hit) = detect(cursor, Some(anchor_at(Point2::from_mm(0.0, 0.0))), &sketch, tol()) 422 - else { 425 + let Some(hit) = detect( 426 + cursor, 427 + Some(anchor_at(Point2::from_mm(0.0, 0.0))), 428 + &sketch, 429 + tol(), 430 + ) else { 423 431 panic!("expected tangent snap"); 424 432 }; 425 433 assert_eq!(hit.kind, SnapKind::Tangent(circle)); ··· 470 478 let (sketch, end) = add_point(sketch, 5.0, -3.0); 471 479 let (sketch, arc) = add_arc(sketch, center, start, end); 472 480 let cursor = Point2::from_mm(3.2, 2.4); 473 - let Some(hit) = detect(cursor, Some(anchor_at(Point2::from_mm(0.0, 0.0))), &sketch, tol()) 474 - else { 481 + let Some(hit) = detect( 482 + cursor, 483 + Some(anchor_at(Point2::from_mm(0.0, 0.0))), 484 + &sketch, 485 + tol(), 486 + ) else { 475 487 panic!("expected tangent snap on arc"); 476 488 }; 477 489 assert_eq!(hit.kind, SnapKind::Tangent(arc)); ··· 489 501 let (sketch, _) = add_arc(sketch, center, start, end); 490 502 let cursor = Point2::from_mm(3.2, 2.4); 491 503 assert!( 492 - detect(cursor, Some(anchor_at(Point2::from_mm(0.0, 0.0))), &sketch, tol()).is_none(), 504 + detect( 505 + cursor, 506 + Some(anchor_at(Point2::from_mm(0.0, 0.0))), 507 + &sketch, 508 + tol() 509 + ) 510 + .is_none(), 493 511 ); 494 512 } 495 513