Another project
0

Configure Feed

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

test(app): tools placement inference

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

author
Lewis
date (May 11, 2026, 9:33 AM +0300) commit c30f7b10 parent 7a631b44 change-id wyuloqyt
+219
+218
crates/bone-app/src/tools/tests/infer.rs
··· 1 + use bone_document::{LineData, Sketch, SketchEntity, SketchEntityKind, SketchRelation}; 2 + use bone_types::{Point2, SketchEntityId}; 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_circle, add_line, add_point, place}; 8 + 9 + use super::{count_kind, fresh}; 10 + 11 + fn only_line(sketch: &Sketch) -> (SketchEntityId, LineData) { 12 + let Some((id, SketchEntity::Line(line))) = sketch 13 + .entities() 14 + .iter() 15 + .find(|(_, e)| matches!(e, SketchEntity::Line(_))) 16 + else { 17 + panic!("line entity"); 18 + }; 19 + (id, *line) 20 + } 21 + 22 + fn line_relations(sketch: &Sketch, line: SketchEntityId) -> Vec<SketchRelation> { 23 + sketch 24 + .relation_order() 25 + .iter() 26 + .filter_map(|rid| sketch.relations().get(*rid).copied()) 27 + .filter(|r| r.references().into_iter().any(|id| id == line)) 28 + .collect() 29 + } 30 + 31 + fn end_point(sketch: &Sketch, line: LineData) -> Point2 { 32 + let Some(SketchEntity::Point(p)) = sketch.entities().get(line.b()) else { 33 + panic!("end point"); 34 + }; 35 + p.at() 36 + } 37 + 38 + #[test] 39 + fn line_first_click_endpoint_snap_reuses_existing_point() { 40 + let (sketch, existing) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 41 + let snap = SnapHit { 42 + kind: SnapKind::Endpoint(existing), 43 + world: Point2::from_mm(0.0, 0.0), 44 + }; 45 + let (next, pending) = place( 46 + sketch, 47 + SketchTool::Line, 48 + Point2::from_mm(0.05, 0.0), 49 + None, 50 + Some(snap), 51 + ); 52 + assert!(next.is_none(), "first click does not commit"); 53 + assert_eq!( 54 + pending, 55 + Some(Pending::First(ClickAnchor::Endpoint(existing))), 56 + "endpoint inference recorded into pending anchor", 57 + ); 58 + } 59 + 60 + #[test] 61 + fn line_end_click_endpoint_snap_reuses_existing_point_and_adds_no_axis_relation() { 62 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 63 + let (sketch, b) = add_point(sketch, Point2::from_mm(7.0, 3.0)); 64 + let snap = SnapHit { 65 + kind: SnapKind::Endpoint(b), 66 + world: Point2::from_mm(7.0, 3.0), 67 + }; 68 + let (next, _) = place( 69 + sketch, 70 + SketchTool::Line, 71 + Point2::from_mm(7.05, 3.0), 72 + Some(Pending::First(ClickAnchor::Endpoint(a))), 73 + Some(snap), 74 + ); 75 + let Some(next) = next else { 76 + panic!("line commits with endpoint snap on end click"); 77 + }; 78 + assert_eq!( 79 + count_kind(&next, SketchEntityKind::Point), 80 + 2, 81 + "no new point materialised; existing endpoints reused", 82 + ); 83 + let (line_id, line) = only_line(&next); 84 + assert_eq!(line.a(), a); 85 + assert_eq!(line.b(), b); 86 + assert!( 87 + line_relations(&next, line_id).is_empty(), 88 + "endpoint snap fires no extra relation; coincidence is structural", 89 + ); 90 + } 91 + 92 + #[test] 93 + fn line_end_click_horizontal_snap_adds_horizontal_relation() { 94 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 95 + let snap = SnapHit { 96 + kind: SnapKind::Horizontal, 97 + world: Point2::from_mm(10.0, 0.0), 98 + }; 99 + let (next, _) = place( 100 + sketch, 101 + SketchTool::Line, 102 + Point2::from_mm(10.0, 0.05), 103 + Some(Pending::First(ClickAnchor::Endpoint(a))), 104 + Some(snap), 105 + ); 106 + let Some(next) = next else { 107 + panic!("line commits"); 108 + }; 109 + let (line_id, line) = only_line(&next); 110 + let relations = line_relations(&next, line_id); 111 + assert!( 112 + relations 113 + .iter() 114 + .any(|r| matches!(r, SketchRelation::Horizontal(id) if *id == line_id)), 115 + "horizontal inference must add Horizontal relation: {relations:?}", 116 + ); 117 + let (_, ey) = end_point(&next, line).coords_mm(); 118 + assert!( 119 + ey.abs() < 1e-9, 120 + "end point projected onto axis (y == anchor.y), got {ey}", 121 + ); 122 + } 123 + 124 + #[test] 125 + fn line_end_click_vertical_snap_adds_vertical_relation() { 126 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 127 + let snap = SnapHit { 128 + kind: SnapKind::Vertical, 129 + world: Point2::from_mm(0.0, 10.0), 130 + }; 131 + let (next, _) = place( 132 + sketch, 133 + SketchTool::Line, 134 + Point2::from_mm(0.05, 10.0), 135 + Some(Pending::First(ClickAnchor::Endpoint(a))), 136 + Some(snap), 137 + ); 138 + let Some(next) = next else { 139 + panic!("line commits"); 140 + }; 141 + let (line_id, line) = only_line(&next); 142 + let relations = line_relations(&next, line_id); 143 + assert!( 144 + relations 145 + .iter() 146 + .any(|r| matches!(r, SketchRelation::Vertical(id) if *id == line_id)), 147 + "vertical inference must add Vertical relation: {relations:?}", 148 + ); 149 + let (ex, _) = end_point(&next, line).coords_mm(); 150 + assert!( 151 + ex.abs() < 1e-9, 152 + "end point projected onto axis (x == anchor.x), got {ex}", 153 + ); 154 + } 155 + 156 + #[test] 157 + fn line_end_click_tangent_snap_adds_tangent_relation() { 158 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 159 + let (sketch, c) = add_point(sketch, Point2::from_mm(5.0, 0.0)); 160 + let (sketch, circle) = add_circle(sketch, c, bone_types::Length::new::<millimeter>(3.0), false); 161 + let snap = SnapHit { 162 + kind: SnapKind::Tangent(circle), 163 + world: Point2::from_mm(3.2, 2.4), 164 + }; 165 + let (next, _) = place( 166 + sketch, 167 + SketchTool::Line, 168 + Point2::from_mm(3.2, 2.4), 169 + Some(Pending::First(ClickAnchor::Endpoint(a))), 170 + Some(snap), 171 + ); 172 + let Some(next) = next else { 173 + panic!("line commits"); 174 + }; 175 + let (line_id, _) = only_line(&next); 176 + let relations = line_relations(&next, line_id); 177 + assert!( 178 + relations.iter().any(|r| matches!( 179 + r, 180 + SketchRelation::Tangent(left, right) 181 + if (*left == line_id && *right == circle) || (*left == circle && *right == line_id) 182 + )), 183 + "tangent inference must add Tangent(line, circle) relation: {relations:?}", 184 + ); 185 + } 186 + 187 + #[test] 188 + fn line_end_click_midpoint_snap_adds_midpoint_relation() { 189 + let (sketch, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 190 + let (sketch, p1) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 191 + let (sketch, host) = add_line(sketch, p0, p1, false); 192 + let (sketch, anchor) = add_point(sketch, Point2::from_mm(5.0, 5.0)); 193 + let snap = SnapHit { 194 + kind: SnapKind::Midpoint(host), 195 + world: Point2::from_mm(5.0, 0.0), 196 + }; 197 + let (next, _) = place( 198 + sketch, 199 + SketchTool::Line, 200 + Point2::from_mm(5.0, 0.0), 201 + Some(Pending::First(ClickAnchor::Endpoint(anchor))), 202 + Some(snap), 203 + ); 204 + let Some(next) = next else { 205 + panic!("line commits"); 206 + }; 207 + let mid_relations: Vec<_> = next 208 + .relation_order() 209 + .iter() 210 + .filter_map(|rid| next.relations().get(*rid).copied()) 211 + .filter(|r| matches!(r, SketchRelation::Midpoint { line, .. } if *line == host)) 212 + .collect(); 213 + assert_eq!( 214 + mid_relations.len(), 215 + 1, 216 + "midpoint inference adds exactly one Midpoint relation against the host line", 217 + ); 218 + }
+1
crates/bone-app/src/tools/tests/mod.rs
··· 4 4 5 5 mod basic; 6 6 mod edge; 7 + mod infer; 7 8 mod rect; 8 9 9 10 pub(super) use super::geometry::distance;