Another project
0

Configure Feed

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

test(app): tools rectangle, parallelogram

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

author
Lewis
date (May 10, 2026, 7:29 PM +0300) commit 882f9cb0 parent efe5e887 change-id qvspvuzw
+646
+243
crates/bone-app/src/tools/tests/edge.rs
··· 1 + use bone_document::{SketchEntity, SketchEntityKind, SketchRelation}; 2 + use bone_types::{Point2, SketchEntityId}; 3 + use uom::si::angle::radian; 4 + 5 + use crate::sketch_mode::{ClickAnchor, Pending, SketchTool}; 6 + use crate::snap::{SnapHit, SnapKind}; 7 + use crate::tools::{add_line, add_point, place, preview}; 8 + 9 + use super::{arc_sweep, count_kind, distance, fresh, only_arc}; 10 + 11 + #[test] 12 + fn centerpoint_arc_picks_minor_arc_when_cursor_clockwise_of_start() { 13 + let center = Point2::from_mm(0.0, 0.0); 14 + let start = Point2::from_mm(5.0, 0.0); 15 + let cursor = Point2::from_mm(5.0, -5.0); 16 + let (_, pending) = place(fresh(), SketchTool::CenterpointArc, center, None, None); 17 + let (_, pending) = place(fresh(), SketchTool::CenterpointArc, start, pending, None); 18 + let (next, _) = place(fresh(), SketchTool::CenterpointArc, cursor, pending, None); 19 + let Some(next) = next else { 20 + panic!("centerpoint arc commits") 21 + }; 22 + let sweep = arc_sweep(&next, only_arc(&next)); 23 + assert!( 24 + sweep <= std::f64::consts::PI + 1e-9, 25 + "sweep {sweep} should be the minor arc when cursor is clockwise of start", 26 + ); 27 + } 28 + 29 + #[test] 30 + fn centerpoint_arc_preview_picks_minor_arc_when_cursor_clockwise_of_start() { 31 + let (sketch, center) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 32 + let (sketch, start) = add_point(sketch, Point2::from_mm(5.0, 0.0)); 33 + let pending = Some(Pending::Second( 34 + ClickAnchor::Endpoint(center), 35 + ClickAnchor::Endpoint(start), 36 + )); 37 + let cursor = Point2::from_mm(5.0, -5.0); 38 + let prev = preview(&sketch, SketchTool::CenterpointArc, cursor, pending, None); 39 + assert_eq!(prev.arcs.len(), 1, "ghost arc emitted"); 40 + let sweep = prev.arcs[0].sweep_angle.get::<radian>(); 41 + assert!( 42 + sweep <= std::f64::consts::PI + 1e-9, 43 + "preview sweep {sweep} should be the minor arc", 44 + ); 45 + } 46 + 47 + #[test] 48 + fn centerpoint_arc_with_off_circle_endpoint_snap_falls_back_to_projection() { 49 + let off_circle = Point2::from_mm(0.5, 5.2); 50 + let (sketch, q) = add_point(fresh(), off_circle); 51 + let pending = Some(Pending::Second( 52 + ClickAnchor::Position(Point2::from_mm(0.0, 0.0)), 53 + ClickAnchor::Position(Point2::from_mm(5.0, 0.0)), 54 + )); 55 + let snap = SnapHit { 56 + kind: SnapKind::Endpoint(q), 57 + world: off_circle, 58 + }; 59 + let (next, _) = place( 60 + sketch, 61 + SketchTool::CenterpointArc, 62 + off_circle, 63 + pending, 64 + Some(snap), 65 + ); 66 + let Some(next) = next else { 67 + panic!("centerpoint arc commits with fallback") 68 + }; 69 + let arc = only_arc(&next); 70 + assert!( 71 + arc.start() != q && arc.end() != q, 72 + "off-circle endpoint must not be reused as arc start or end", 73 + ); 74 + let resolve = |id| match next.entities().get(id) { 75 + Some(SketchEntity::Point(p)) => p.at(), 76 + _ => panic!("point"), 77 + }; 78 + let center_pos = resolve(arc.center()); 79 + let radius = distance(center_pos, resolve(arc.start())); 80 + let end_radial = distance(center_pos, resolve(arc.end())); 81 + assert!( 82 + (end_radial - radius).abs() < 1e-9, 83 + "arc end must lie on the circle through start: {end_radial} vs {radius}", 84 + ); 85 + } 86 + 87 + #[test] 88 + fn line_first_click_with_midpoint_snap_defers_commit() { 89 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 90 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 91 + let (sketch, line) = add_line(sketch, a, b, false); 92 + let snap = SnapHit { 93 + kind: SnapKind::Midpoint(line), 94 + world: Point2::from_mm(5.0, 0.0), 95 + }; 96 + let (next, pending) = place( 97 + sketch, 98 + SketchTool::Line, 99 + Point2::from_mm(5.0, 0.0), 100 + None, 101 + Some(snap), 102 + ); 103 + assert!(next.is_none(), "first click must not commit any entity"); 104 + let Some(Pending::First(ClickAnchor::MidpointOf { 105 + line: pending_line, 106 + position, 107 + })) = pending 108 + else { 109 + panic!("expected pending MidpointOf, got {pending:?}"); 110 + }; 111 + assert_eq!(pending_line, line); 112 + assert_eq!(position, Point2::from_mm(5.0, 0.0)); 113 + } 114 + 115 + #[test] 116 + fn line_second_click_after_midpoint_first_commits_in_one_step() { 117 + let (sketch, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 118 + let (sketch, b) = add_point(sketch, Point2::from_mm(10.0, 0.0)); 119 + let (sketch, line) = add_line(sketch, a, b, false); 120 + let snap1 = SnapHit { 121 + kind: SnapKind::Midpoint(line), 122 + world: Point2::from_mm(5.0, 0.0), 123 + }; 124 + let (none_first, pending) = place( 125 + sketch.clone(), 126 + SketchTool::Line, 127 + Point2::from_mm(5.0, 0.0), 128 + None, 129 + Some(snap1), 130 + ); 131 + assert!(none_first.is_none()); 132 + let (next, _) = place( 133 + sketch, 134 + SketchTool::Line, 135 + Point2::from_mm(5.0, 8.0), 136 + pending, 137 + None, 138 + ); 139 + let Some(next) = next else { 140 + panic!("second click commits") 141 + }; 142 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 4); 143 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 2); 144 + let mids = next 145 + .relation_order() 146 + .iter() 147 + .filter(|r| { 148 + matches!( 149 + next.relations().get(**r), 150 + Some(SketchRelation::Midpoint { line: l, .. }) if *l == line, 151 + ) 152 + }) 153 + .count(); 154 + assert_eq!(mids, 1, "midpoint relation emitted exactly once on commit"); 155 + } 156 + 157 + #[test] 158 + fn line_tool_stale_endpoint_pending_clears_without_panic() { 159 + let pending = Some(Pending::First(ClickAnchor::Endpoint( 160 + SketchEntityId::default(), 161 + ))); 162 + let (next, kept) = place( 163 + fresh(), 164 + SketchTool::Line, 165 + Point2::from_mm(5.0, 5.0), 166 + pending, 167 + None, 168 + ); 169 + assert!(next.is_none(), "stale pending must not commit"); 170 + assert_eq!(kept, None, "stale pending must clear so user can re-anchor"); 171 + } 172 + 173 + #[test] 174 + fn three_point_corner_rectangle_holds_when_third_click_is_collinear() { 175 + let p1 = Point2::from_mm(0.0, 0.0); 176 + let p2 = Point2::from_mm(4.0, 0.0); 177 + let collinear = Point2::from_mm(2.0, 0.0); 178 + let (_, pending) = place( 179 + fresh(), 180 + SketchTool::ThreePointCornerRectangle, 181 + p1, 182 + None, 183 + None, 184 + ); 185 + let (_, pending) = place( 186 + fresh(), 187 + SketchTool::ThreePointCornerRectangle, 188 + p2, 189 + pending, 190 + None, 191 + ); 192 + let (next, kept) = place( 193 + fresh(), 194 + SketchTool::ThreePointCornerRectangle, 195 + collinear, 196 + pending, 197 + None, 198 + ); 199 + assert!(next.is_none()); 200 + assert!(matches!(kept, Some(Pending::Second(_, _)))); 201 + } 202 + 203 + #[test] 204 + fn three_point_center_rectangle_holds_when_third_click_is_collinear() { 205 + let center = Point2::from_mm(0.0, 0.0); 206 + let mid = Point2::from_mm(5.0, 0.0); 207 + let collinear = Point2::from_mm(7.0, 0.0); 208 + let (_, pending) = place( 209 + fresh(), 210 + SketchTool::ThreePointCenterRectangle, 211 + center, 212 + None, 213 + None, 214 + ); 215 + let (_, pending) = place( 216 + fresh(), 217 + SketchTool::ThreePointCenterRectangle, 218 + mid, 219 + pending, 220 + None, 221 + ); 222 + let (next, kept) = place( 223 + fresh(), 224 + SketchTool::ThreePointCenterRectangle, 225 + collinear, 226 + pending, 227 + None, 228 + ); 229 + assert!(next.is_none()); 230 + assert!(matches!(kept, Some(Pending::Second(_, _)))); 231 + } 232 + 233 + #[test] 234 + fn parallelogram_holds_when_third_corner_collinear_with_base() { 235 + let p1 = Point2::from_mm(0.0, 0.0); 236 + let p2 = Point2::from_mm(4.0, 0.0); 237 + let collinear = Point2::from_mm(8.0, 0.0); 238 + let (_, pending) = place(fresh(), SketchTool::Parallelogram, p1, None, None); 239 + let (_, pending) = place(fresh(), SketchTool::Parallelogram, p2, pending, None); 240 + let (next, kept) = place(fresh(), SketchTool::Parallelogram, collinear, pending, None); 241 + assert!(next.is_none()); 242 + assert!(matches!(kept, Some(Pending::Second(_, _)))); 243 + }
+403
crates/bone-app/src/tools/tests/rect.rs
··· 1 + use bone_document::{SketchEntityKind, SketchRelation}; 2 + use bone_types::Point2; 3 + 4 + use crate::sketch_mode::{ClickAnchor, Pending, SketchTool}; 5 + use crate::snap::{SnapHit, SnapKind}; 6 + use crate::tools::{add_point, place}; 7 + 8 + use super::{count_kind, fresh}; 9 + 10 + #[test] 11 + fn corner_rectangle_emits_four_lines_and_axis_relations() { 12 + let p1 = Point2::from_mm(0.0, 0.0); 13 + let p2 = Point2::from_mm(10.0, 5.0); 14 + let (_, pending) = place(fresh(), SketchTool::CornerRectangle, p1, None, None); 15 + let (next, pending) = place(fresh(), SketchTool::CornerRectangle, p2, pending, None); 16 + let Some(next) = next else { 17 + panic!("corner rect commits") 18 + }; 19 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 4); 20 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 4); 21 + assert_eq!(pending, None); 22 + let h = next 23 + .relation_order() 24 + .iter() 25 + .filter(|r| { 26 + matches!( 27 + next.relations().get(**r), 28 + Some(SketchRelation::Horizontal(_)) 29 + ) 30 + }) 31 + .count(); 32 + let v = next 33 + .relation_order() 34 + .iter() 35 + .filter(|r| matches!(next.relations().get(**r), Some(SketchRelation::Vertical(_)))) 36 + .count(); 37 + assert_eq!(h, 2); 38 + assert_eq!(v, 2); 39 + } 40 + 41 + #[test] 42 + fn center_rectangle_emits_six_lines_diagonals_and_midpoint_relations() { 43 + let center = Point2::from_mm(0.0, 0.0); 44 + let corner = Point2::from_mm(4.0, 3.0); 45 + let (_, pending) = place(fresh(), SketchTool::CenterRectangle, center, None, None); 46 + let (next, _) = place(fresh(), SketchTool::CenterRectangle, corner, pending, None); 47 + let Some(next) = next else { 48 + panic!("center rect commits") 49 + }; 50 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 5); 51 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 6); 52 + let mids = next 53 + .relation_order() 54 + .iter() 55 + .filter(|r| { 56 + matches!( 57 + next.relations().get(**r), 58 + Some(SketchRelation::Midpoint { .. }) 59 + ) 60 + }) 61 + .count(); 62 + assert_eq!(mids, 2); 63 + } 64 + 65 + #[test] 66 + fn three_point_corner_rectangle_emits_parallel_and_perpendicular() { 67 + let p1 = Point2::from_mm(0.0, 0.0); 68 + let p2 = Point2::from_mm(4.0, 3.0); 69 + let click3 = Point2::from_mm(0.0, 5.0); 70 + let (_, pending) = place( 71 + fresh(), 72 + SketchTool::ThreePointCornerRectangle, 73 + p1, 74 + None, 75 + None, 76 + ); 77 + let (_, pending) = place( 78 + fresh(), 79 + SketchTool::ThreePointCornerRectangle, 80 + p2, 81 + pending, 82 + None, 83 + ); 84 + let (next, _) = place( 85 + fresh(), 86 + SketchTool::ThreePointCornerRectangle, 87 + click3, 88 + pending, 89 + None, 90 + ); 91 + let Some(next) = next else { 92 + panic!("3-pt corner rect commits") 93 + }; 94 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 4); 95 + let par = next 96 + .relation_order() 97 + .iter() 98 + .filter(|r| { 99 + matches!( 100 + next.relations().get(**r), 101 + Some(SketchRelation::Parallel(_, _)) 102 + ) 103 + }) 104 + .count(); 105 + let perp = next 106 + .relation_order() 107 + .iter() 108 + .filter(|r| { 109 + matches!( 110 + next.relations().get(**r), 111 + Some(SketchRelation::Perpendicular(_, _)) 112 + ) 113 + }) 114 + .count(); 115 + assert_eq!(par, 2); 116 + assert_eq!(perp, 1); 117 + } 118 + 119 + #[test] 120 + fn three_point_center_rectangle_emits_diagonals_and_orientation_pin() { 121 + let center = Point2::from_mm(0.0, 0.0); 122 + let mid = Point2::from_mm(5.0, 0.0); 123 + let click3 = Point2::from_mm(5.0, 3.0); 124 + let (_, pending) = place( 125 + fresh(), 126 + SketchTool::ThreePointCenterRectangle, 127 + center, 128 + None, 129 + None, 130 + ); 131 + let (_, pending) = place( 132 + fresh(), 133 + SketchTool::ThreePointCenterRectangle, 134 + mid, 135 + pending, 136 + None, 137 + ); 138 + let (next, _) = place( 139 + fresh(), 140 + SketchTool::ThreePointCenterRectangle, 141 + click3, 142 + pending, 143 + None, 144 + ); 145 + let Some(next) = next else { 146 + panic!("3-pt center rect commits") 147 + }; 148 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 6); 149 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 6); 150 + let mids = next 151 + .relation_order() 152 + .iter() 153 + .filter(|r| { 154 + matches!( 155 + next.relations().get(**r), 156 + Some(SketchRelation::Midpoint { .. }) 157 + ) 158 + }) 159 + .count(); 160 + assert_eq!( 161 + mids, 3, 162 + "two diagonal midpoints plus mid-edge midpoint pinning orientation", 163 + ); 164 + let par = next 165 + .relation_order() 166 + .iter() 167 + .filter(|r| { 168 + matches!( 169 + next.relations().get(**r), 170 + Some(SketchRelation::Parallel(_, _)) 171 + ) 172 + }) 173 + .count(); 174 + assert_eq!( 175 + par, 0, 176 + "parallels are redundant given diagonal-midpoint constraints", 177 + ); 178 + } 179 + 180 + #[test] 181 + fn three_point_center_rectangle_reuses_mid_endpoint_on_snap() { 182 + let (sketch, q) = add_point(fresh(), Point2::from_mm(5.0, 0.0)); 183 + let snap_q = SnapHit { 184 + kind: SnapKind::Endpoint(q), 185 + world: Point2::from_mm(5.0, 0.0), 186 + }; 187 + let pending = Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 188 + 0.0, 0.0, 189 + )))); 190 + let (_, pending) = place( 191 + sketch.clone(), 192 + SketchTool::ThreePointCenterRectangle, 193 + Point2::from_mm(5.0, 0.0), 194 + pending, 195 + Some(snap_q), 196 + ); 197 + let (next, _) = place( 198 + sketch, 199 + SketchTool::ThreePointCenterRectangle, 200 + Point2::from_mm(5.0, 3.0), 201 + pending, 202 + None, 203 + ); 204 + let Some(next) = next else { panic!("commit") }; 205 + assert!( 206 + next.entity_order().contains(&q), 207 + "snapped mid endpoint reused", 208 + ); 209 + assert!( 210 + next.relation_order().iter().any(|rid| matches!( 211 + next.relations().get(*rid), 212 + Some(SketchRelation::Midpoint { point, .. }) if *point == q, 213 + )), 214 + "midpoint relation pins reused mid to the right edge", 215 + ); 216 + } 217 + 218 + #[test] 219 + fn centerpoint_arc_drops_pending_when_start_and_end_share_endpoint() { 220 + let (sketch, q) = add_point(fresh(), Point2::from_mm(5.0, 0.0)); 221 + let snap_q = SnapHit { 222 + kind: SnapKind::Endpoint(q), 223 + world: Point2::from_mm(5.0, 0.0), 224 + }; 225 + let pending = Some(Pending::Second( 226 + ClickAnchor::Position(Point2::from_mm(10.0, 10.0)), 227 + ClickAnchor::Endpoint(q), 228 + )); 229 + let before = count_kind(&sketch, SketchEntityKind::Point); 230 + let (next, kept) = place( 231 + sketch, 232 + SketchTool::CenterpointArc, 233 + Point2::from_mm(5.0, 0.0), 234 + pending, 235 + Some(snap_q), 236 + ); 237 + assert!( 238 + next.is_none(), 239 + "must not commit when end snap collides with start" 240 + ); 241 + assert!(matches!(kept, Some(Pending::Second(_, _)))); 242 + assert_eq!(before, 1, "no orphan center point added"); 243 + } 244 + 245 + #[test] 246 + fn corner_rectangle_rejects_zero_width_drag() { 247 + let p1 = Point2::from_mm(0.0, 0.0); 248 + let zero_width = Point2::from_mm(0.0, 5.0); 249 + let (_, pending) = place(fresh(), SketchTool::CornerRectangle, p1, None, None); 250 + let (next, kept) = place( 251 + fresh(), 252 + SketchTool::CornerRectangle, 253 + zero_width, 254 + pending, 255 + None, 256 + ); 257 + assert!(next.is_none(), "zero-width drag must not commit"); 258 + assert!(matches!(kept, Some(Pending::First(_)))); 259 + } 260 + 261 + #[test] 262 + fn corner_rectangle_rejects_zero_height_drag() { 263 + let p1 = Point2::from_mm(0.0, 0.0); 264 + let zero_height = Point2::from_mm(5.0, 0.0); 265 + let (_, pending) = place(fresh(), SketchTool::CornerRectangle, p1, None, None); 266 + let (next, kept) = place( 267 + fresh(), 268 + SketchTool::CornerRectangle, 269 + zero_height, 270 + pending, 271 + None, 272 + ); 273 + assert!(next.is_none(), "zero-height drag must not commit"); 274 + assert!(matches!(kept, Some(Pending::First(_)))); 275 + } 276 + 277 + #[test] 278 + fn center_rectangle_rejects_zero_width_drag() { 279 + let center = Point2::from_mm(0.0, 0.0); 280 + let zero_width = Point2::from_mm(0.0, 5.0); 281 + let (_, pending) = place(fresh(), SketchTool::CenterRectangle, center, None, None); 282 + let (next, kept) = place( 283 + fresh(), 284 + SketchTool::CenterRectangle, 285 + zero_width, 286 + pending, 287 + None, 288 + ); 289 + assert!(next.is_none()); 290 + assert!(matches!(kept, Some(Pending::First(_)))); 291 + } 292 + 293 + #[test] 294 + fn center_rectangle_rejects_zero_height_drag() { 295 + let center = Point2::from_mm(0.0, 0.0); 296 + let zero_height = Point2::from_mm(5.0, 0.0); 297 + let (_, pending) = place(fresh(), SketchTool::CenterRectangle, center, None, None); 298 + let (next, kept) = place( 299 + fresh(), 300 + SketchTool::CenterRectangle, 301 + zero_height, 302 + pending, 303 + None, 304 + ); 305 + assert!(next.is_none()); 306 + assert!(matches!(kept, Some(Pending::First(_)))); 307 + } 308 + 309 + #[test] 310 + fn corner_rectangle_reuses_endpoint_id_on_diagonal_corner() { 311 + let (sketch, q) = add_point(fresh(), Point2::from_mm(5.0, 3.0)); 312 + let snap_q = SnapHit { 313 + kind: SnapKind::Endpoint(q), 314 + world: Point2::from_mm(5.0, 3.0), 315 + }; 316 + let pending = Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 317 + 0.0, 0.0, 318 + )))); 319 + let (next, _) = place( 320 + sketch, 321 + SketchTool::CornerRectangle, 322 + Point2::from_mm(5.0, 3.0), 323 + pending, 324 + Some(snap_q), 325 + ); 326 + let Some(next) = next else { panic!("commit") }; 327 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 4); 328 + assert!(next.entity_order().contains(&q), "diagonal endpoint reused"); 329 + } 330 + 331 + #[test] 332 + fn center_rectangle_reuses_endpoint_id_on_corner() { 333 + let (sketch, q) = add_point(fresh(), Point2::from_mm(4.0, 3.0)); 334 + let snap_q = SnapHit { 335 + kind: SnapKind::Endpoint(q), 336 + world: Point2::from_mm(4.0, 3.0), 337 + }; 338 + let pending = Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 339 + 0.0, 0.0, 340 + )))); 341 + let (next, _) = place( 342 + sketch, 343 + SketchTool::CenterRectangle, 344 + Point2::from_mm(4.0, 3.0), 345 + pending, 346 + Some(snap_q), 347 + ); 348 + let Some(next) = next else { panic!("commit") }; 349 + assert!(next.entity_order().contains(&q), "corner endpoint reused"); 350 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 5); 351 + } 352 + 353 + #[test] 354 + fn parallelogram_reuses_endpoint_on_third_corner() { 355 + let (sketch, q) = add_point(fresh(), Point2::from_mm(5.0, 3.0)); 356 + let snap_q = SnapHit { 357 + kind: SnapKind::Endpoint(q), 358 + world: Point2::from_mm(5.0, 3.0), 359 + }; 360 + let p1 = Point2::from_mm(0.0, 0.0); 361 + let p2 = Point2::from_mm(4.0, 0.0); 362 + let (_, pending) = place(sketch.clone(), SketchTool::Parallelogram, p1, None, None); 363 + let (_, pending) = place(sketch.clone(), SketchTool::Parallelogram, p2, pending, None); 364 + let (next, _) = place( 365 + sketch, 366 + SketchTool::Parallelogram, 367 + Point2::from_mm(5.0, 3.0), 368 + pending, 369 + Some(snap_q), 370 + ); 371 + let Some(next) = next else { panic!("commit") }; 372 + assert!( 373 + next.entity_order().contains(&q), 374 + "third corner endpoint reused" 375 + ); 376 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 4); 377 + } 378 + 379 + #[test] 380 + fn parallelogram_fourth_corner_is_implied_by_first_three() { 381 + let p1 = Point2::from_mm(0.0, 0.0); 382 + let p2 = Point2::from_mm(4.0, 0.0); 383 + let p3 = Point2::from_mm(5.0, 3.0); 384 + let (_, pending) = place(fresh(), SketchTool::Parallelogram, p1, None, None); 385 + let (_, pending) = place(fresh(), SketchTool::Parallelogram, p2, pending, None); 386 + let (next, _) = place(fresh(), SketchTool::Parallelogram, p3, pending, None); 387 + let Some(next) = next else { 388 + panic!("parallelogram commits") 389 + }; 390 + assert_eq!(count_kind(&next, SketchEntityKind::Point), 4); 391 + assert_eq!(count_kind(&next, SketchEntityKind::Line), 4); 392 + let par = next 393 + .relation_order() 394 + .iter() 395 + .filter(|r| { 396 + matches!( 397 + next.relations().get(**r), 398 + Some(SketchRelation::Parallel(_, _)) 399 + ) 400 + }) 401 + .count(); 402 + assert_eq!(par, 2); 403 + }