Another project
0

Configure Feed

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

feat(app): smart-dimension eligibility

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

author
Lewis
date (May 12, 2026, 11:33 AM +0300) commit ab6f6c2a parent f408ab8f change-id swrqvwwx
+476
+476
crates/bone-app/src/smart_dimension.rs
··· 1 + use bone_document::{ 2 + DimensionKind, DimensionValue, Sketch, SketchDimension, SketchEntity, SketchEntityKind, 3 + }; 4 + use bone_types::{Angle, Length, Point2, SketchEntityId}; 5 + use bone_ui::strings::StringKey; 6 + use uom::si::angle::radian; 7 + use uom::si::length::millimeter; 8 + 9 + use crate::sketch_mode::PendingDimension; 10 + use crate::strings; 11 + 12 + #[derive(Clone, Debug, PartialEq)] 13 + pub enum Eligibility { 14 + Eligible(PendingDimension), 15 + Disabled(StringKey), 16 + } 17 + 18 + #[must_use] 19 + pub fn eligibility(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility { 20 + let disabled = || Eligibility::Disabled(strings::DIM_HINT_GENERIC); 21 + match selection { 22 + [only] => single(sketch, *only).map_or_else(disabled, Eligibility::Eligible), 23 + [first, second] if first != second => { 24 + pair(sketch, *first, *second).map_or_else(disabled, Eligibility::Eligible) 25 + } 26 + _ => disabled(), 27 + } 28 + } 29 + 30 + #[must_use] 31 + pub fn swap_radius_diameter(dim: SketchDimension) -> Option<SketchDimension> { 32 + match dim { 33 + SketchDimension::Radius { 34 + target, 35 + value, 36 + kind, 37 + } => { 38 + let diameter = Length::new::<millimeter>(value.get::<millimeter>() * 2.0); 39 + Some(SketchDimension::Diameter { 40 + target, 41 + value: diameter, 42 + kind, 43 + }) 44 + } 45 + SketchDimension::Diameter { 46 + target, 47 + value, 48 + kind, 49 + } => { 50 + let radius = Length::new::<millimeter>(value.get::<millimeter>() * 0.5); 51 + Some(SketchDimension::Radius { 52 + target, 53 + value: radius, 54 + kind, 55 + }) 56 + } 57 + SketchDimension::Linear { .. } | SketchDimension::Angular { .. } => None, 58 + } 59 + } 60 + 61 + #[must_use] 62 + pub fn live_anchor(sketch: &Sketch, proto: SketchDimension) -> Option<Point2> { 63 + match proto { 64 + SketchDimension::Linear { a, b, .. } => { 65 + let pa = point_position(sketch, a)?; 66 + let pb = point_position(sketch, b)?; 67 + Some(midpoint(pa, pb)) 68 + } 69 + SketchDimension::Radius { target, .. } => match sketch.entities().get(target)? { 70 + SketchEntity::Arc(a) => point_position(sketch, a.center()), 71 + SketchEntity::Circle(c) => point_position(sketch, c.center()), 72 + _ => None, 73 + }, 74 + SketchDimension::Diameter { target, .. } => match sketch.entities().get(target)? { 75 + SketchEntity::Circle(c) => point_position(sketch, c.center()), 76 + SketchEntity::Arc(a) => point_position(sketch, a.center()), 77 + _ => None, 78 + }, 79 + SketchDimension::Angular { a, b, .. } => { 80 + let (a_start, a_end) = line_endpoints(sketch, a)?; 81 + let (b_start, b_end) = line_endpoints(sketch, b)?; 82 + Some(midpoint(midpoint(a_start, a_end), midpoint(b_start, b_end))) 83 + } 84 + } 85 + } 86 + 87 + #[must_use] 88 + pub const fn placeholder_for(value: DimensionValue) -> StringKey { 89 + match value { 90 + DimensionValue::Length(_) => strings::DIM_PLACEHOLDER_LENGTH, 91 + DimensionValue::Angle(_) => strings::DIM_PLACEHOLDER_ANGLE, 92 + } 93 + } 94 + 95 + fn single(sketch: &Sketch, id: SketchEntityId) -> Option<PendingDimension> { 96 + let entity = sketch.entities().get(id)?; 97 + match entity { 98 + SketchEntity::Point(_) => None, 99 + SketchEntity::Line(l) => { 100 + let a = point_position(sketch, l.a())?; 101 + let b = point_position(sketch, l.b())?; 102 + let value = Length::new::<millimeter>((b - a).norm_mm()); 103 + Some(PendingDimension { 104 + proto: SketchDimension::Linear { 105 + a: l.a(), 106 + b: l.b(), 107 + value, 108 + kind: DimensionKind::Driving, 109 + }, 110 + anchor: midpoint(a, b), 111 + }) 112 + } 113 + SketchEntity::Arc(a) => { 114 + let center = point_position(sketch, a.center())?; 115 + let start = point_position(sketch, a.start())?; 116 + let value = Length::new::<millimeter>((start - center).norm_mm()); 117 + Some(PendingDimension { 118 + proto: SketchDimension::Radius { 119 + target: id, 120 + value, 121 + kind: DimensionKind::Driving, 122 + }, 123 + anchor: center, 124 + }) 125 + } 126 + SketchEntity::Circle(c) => { 127 + let center = point_position(sketch, c.center())?; 128 + let value = Length::new::<millimeter>(c.radius().get::<millimeter>() * 2.0); 129 + Some(PendingDimension { 130 + proto: SketchDimension::Diameter { 131 + target: id, 132 + value, 133 + kind: DimensionKind::Driving, 134 + }, 135 + anchor: center, 136 + }) 137 + } 138 + } 139 + } 140 + 141 + fn pair(sketch: &Sketch, a: SketchEntityId, b: SketchEntityId) -> Option<PendingDimension> { 142 + let ka = sketch.entities().get(a).map(SketchEntity::kind)?; 143 + let kb = sketch.entities().get(b).map(SketchEntity::kind)?; 144 + match (ka, kb) { 145 + (SketchEntityKind::Point, SketchEntityKind::Point) => linear_points(sketch, a, b), 146 + (SketchEntityKind::Line, SketchEntityKind::Line) => angular_lines(sketch, a, b), 147 + _ => None, 148 + } 149 + } 150 + 151 + fn linear_points( 152 + sketch: &Sketch, 153 + a: SketchEntityId, 154 + b: SketchEntityId, 155 + ) -> Option<PendingDimension> { 156 + let pa = point_position(sketch, a)?; 157 + let pb = point_position(sketch, b)?; 158 + let value = Length::new::<millimeter>((pb - pa).norm_mm()); 159 + Some(PendingDimension { 160 + proto: SketchDimension::Linear { 161 + a, 162 + b, 163 + value, 164 + kind: DimensionKind::Driving, 165 + }, 166 + anchor: midpoint(pa, pb), 167 + }) 168 + } 169 + 170 + fn angular_lines( 171 + sketch: &Sketch, 172 + a: SketchEntityId, 173 + b: SketchEntityId, 174 + ) -> Option<PendingDimension> { 175 + let (a_start, a_end) = line_endpoints(sketch, a)?; 176 + let (b_start, b_end) = line_endpoints(sketch, b)?; 177 + let probe = SketchDimension::Angular { 178 + a, 179 + b, 180 + value: Angle::new::<radian>(0.0), 181 + kind: DimensionKind::Driving, 182 + }; 183 + let DimensionValue::Angle(value) = sketch.measure(probe)? else { 184 + return None; 185 + }; 186 + let mid_a = midpoint(a_start, a_end); 187 + let mid_b = midpoint(b_start, b_end); 188 + Some(PendingDimension { 189 + proto: SketchDimension::Angular { 190 + a, 191 + b, 192 + value, 193 + kind: DimensionKind::Driving, 194 + }, 195 + anchor: midpoint(mid_a, mid_b), 196 + }) 197 + } 198 + 199 + fn line_endpoints(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Point2)> { 200 + let SketchEntity::Line(line) = sketch.entities().get(id)? else { 201 + return None; 202 + }; 203 + let a = point_position(sketch, line.a())?; 204 + let b = point_position(sketch, line.b())?; 205 + Some((a, b)) 206 + } 207 + 208 + fn point_position(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> { 209 + match sketch.entities().get(id)? { 210 + SketchEntity::Point(p) => Some(p.at()), 211 + _ => None, 212 + } 213 + } 214 + 215 + fn midpoint(a: Point2, b: Point2) -> Point2 { 216 + let (ax, ay) = a.coords_mm(); 217 + let (bx, by) = b.coords_mm(); 218 + Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by)) 219 + } 220 + 221 + #[cfg(test)] 222 + mod tests { 223 + use super::*; 224 + use crate::sketch_mode::Plane; 225 + use crate::tools::{add_arc, add_circle, add_line, add_point}; 226 + use bone_types::Point2; 227 + use uom::si::angle::degree; 228 + 229 + fn fresh() -> Sketch { 230 + Sketch::new(Plane::Xy.basis()) 231 + } 232 + 233 + fn approx_eq(a: f64, b: f64) { 234 + assert!((a - b).abs() < 1e-9, "{a} !~= {b}"); 235 + } 236 + 237 + #[test] 238 + fn empty_selection_disabled() { 239 + assert_eq!( 240 + eligibility(&fresh(), &[]), 241 + Eligibility::Disabled(strings::DIM_HINT_GENERIC), 242 + ); 243 + } 244 + 245 + #[test] 246 + fn single_point_selection_disabled() { 247 + let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 248 + assert_eq!( 249 + eligibility(&s, &[p]), 250 + Eligibility::Disabled(strings::DIM_HINT_GENERIC), 251 + ); 252 + } 253 + 254 + #[test] 255 + fn single_line_yields_linear_through_endpoints() { 256 + let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 257 + let (s, b) = add_point(s, Point2::from_mm(3.0, 4.0)); 258 + let (s, line) = add_line(s, a, b, false); 259 + let Eligibility::Eligible(req) = eligibility(&s, &[line]) else { 260 + panic!("expected eligible"); 261 + }; 262 + match req.proto { 263 + SketchDimension::Linear { 264 + a: pa, 265 + b: pb, 266 + value, 267 + kind, 268 + } => { 269 + assert_eq!(pa, a); 270 + assert_eq!(pb, b); 271 + approx_eq(value.get::<millimeter>(), 5.0); 272 + assert_eq!(kind, DimensionKind::Driving); 273 + } 274 + other => panic!("expected Linear, got {other:?}"), 275 + } 276 + let (mx, my) = req.anchor.coords_mm(); 277 + approx_eq(mx, 1.5); 278 + approx_eq(my, 2.0); 279 + } 280 + 281 + #[test] 282 + fn two_points_yield_linear() { 283 + let (s, a) = add_point(fresh(), Point2::from_mm(-1.0, 0.0)); 284 + let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0)); 285 + let Eligibility::Eligible(req) = eligibility(&s, &[a, b]) else { 286 + panic!("expected eligible"); 287 + }; 288 + match req.proto { 289 + SketchDimension::Linear { value, .. } => { 290 + approx_eq(value.get::<millimeter>(), 2.0); 291 + } 292 + other => panic!("expected Linear, got {other:?}"), 293 + } 294 + } 295 + 296 + #[test] 297 + fn arc_default_is_radius() { 298 + let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 299 + let (s, start) = add_point(s, Point2::from_mm(7.0, 0.0)); 300 + let (s, end) = add_point(s, Point2::from_mm(0.0, 7.0)); 301 + let (s, arc) = add_arc(s, c, start, end, false); 302 + let Eligibility::Eligible(req) = eligibility(&s, &[arc]) else { 303 + panic!("expected eligible"); 304 + }; 305 + match req.proto { 306 + SketchDimension::Radius { target, value, .. } => { 307 + assert_eq!(target, arc); 308 + approx_eq(value.get::<millimeter>(), 7.0); 309 + } 310 + other => panic!("expected Radius, got {other:?}"), 311 + } 312 + let (cx, cy) = req.anchor.coords_mm(); 313 + approx_eq(cx, 0.0); 314 + approx_eq(cy, 0.0); 315 + } 316 + 317 + #[test] 318 + fn circle_default_is_diameter() { 319 + let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 320 + let (s, circle) = add_circle(s, c, Length::new::<millimeter>(5.0), false); 321 + let Eligibility::Eligible(req) = eligibility(&s, &[circle]) else { 322 + panic!("expected eligible"); 323 + }; 324 + match req.proto { 325 + SketchDimension::Diameter { target, value, .. } => { 326 + assert_eq!(target, circle); 327 + approx_eq(value.get::<millimeter>(), 10.0); 328 + } 329 + other => panic!("expected Diameter, got {other:?}"), 330 + } 331 + } 332 + 333 + #[test] 334 + fn two_perpendicular_lines_yield_ninety_degrees() { 335 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 336 + let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0)); 337 + let (s, p2) = add_point(s, Point2::from_mm(0.0, 0.0)); 338 + let (s, p3) = add_point(s, Point2::from_mm(0.0, 10.0)); 339 + let (s, l1) = add_line(s, p0, p1, false); 340 + let (s, l2) = add_line(s, p2, p3, false); 341 + let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else { 342 + panic!("expected eligible"); 343 + }; 344 + match req.proto { 345 + SketchDimension::Angular { value, .. } => { 346 + approx_eq(value.get::<degree>(), 90.0); 347 + } 348 + other => panic!("expected Angular, got {other:?}"), 349 + } 350 + } 351 + 352 + #[test] 353 + fn two_parallel_lines_yield_zero_degrees() { 354 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 355 + let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0)); 356 + let (s, p2) = add_point(s, Point2::from_mm(0.0, 5.0)); 357 + let (s, p3) = add_point(s, Point2::from_mm(10.0, 5.0)); 358 + let (s, l1) = add_line(s, p0, p1, false); 359 + let (s, l2) = add_line(s, p2, p3, false); 360 + let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else { 361 + panic!("expected eligible"); 362 + }; 363 + match req.proto { 364 + SketchDimension::Angular { value, .. } => { 365 + approx_eq(value.get::<degree>(), 0.0); 366 + } 367 + other => panic!("expected Angular, got {other:?}"), 368 + } 369 + } 370 + 371 + #[test] 372 + fn antiparallel_lines_collapse_to_acute_zero() { 373 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 374 + let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0)); 375 + let (s, p2) = add_point(s, Point2::from_mm(10.0, 5.0)); 376 + let (s, p3) = add_point(s, Point2::from_mm(0.0, 5.0)); 377 + let (s, l1) = add_line(s, p0, p1, false); 378 + let (s, l2) = add_line(s, p2, p3, false); 379 + let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else { 380 + panic!("expected eligible"); 381 + }; 382 + match req.proto { 383 + SketchDimension::Angular { value, .. } => { 384 + approx_eq(value.get::<degree>(), 0.0); 385 + } 386 + other => panic!("expected Angular, got {other:?}"), 387 + } 388 + } 389 + 390 + #[test] 391 + fn point_and_line_pair_disabled() { 392 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 393 + let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0)); 394 + let (s, line) = add_line(s, p0, p1, false); 395 + let (s, q) = add_point(s, Point2::from_mm(5.0, 5.0)); 396 + assert_eq!( 397 + eligibility(&s, &[q, line]), 398 + Eligibility::Disabled(strings::DIM_HINT_GENERIC), 399 + ); 400 + } 401 + 402 + #[test] 403 + fn three_entities_disabled() { 404 + let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 405 + let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0)); 406 + let (s, c) = add_point(s, Point2::from_mm(0.0, 1.0)); 407 + assert_eq!( 408 + eligibility(&s, &[a, b, c]), 409 + Eligibility::Disabled(strings::DIM_HINT_GENERIC), 410 + ); 411 + } 412 + 413 + #[test] 414 + fn pair_of_same_id_disabled() { 415 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 416 + let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0)); 417 + let (s, line) = add_line(s, p0, p1, false); 418 + assert_eq!( 419 + eligibility(&s, &[line, line]), 420 + Eligibility::Disabled(strings::DIM_HINT_GENERIC), 421 + ); 422 + } 423 + 424 + #[test] 425 + fn swap_radius_to_diameter_doubles_value() { 426 + let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 427 + let (s, _start) = add_point(s, Point2::from_mm(3.0, 0.0)); 428 + let (s, _end) = add_point(s, Point2::from_mm(0.0, 3.0)); 429 + let radius = SketchDimension::Radius { 430 + target: c, 431 + value: Length::new::<millimeter>(3.0), 432 + kind: DimensionKind::Driving, 433 + }; 434 + let Some(diameter) = swap_radius_diameter(radius) else { 435 + panic!("Radius must swap to Diameter"); 436 + }; 437 + let SketchDimension::Diameter { value, .. } = diameter else { 438 + panic!("expected Diameter"); 439 + }; 440 + approx_eq(value.get::<millimeter>(), 6.0); 441 + let _ = s; 442 + } 443 + 444 + #[test] 445 + fn swap_diameter_to_radius_halves_value() { 446 + let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 447 + let (s, _) = add_point(s, Point2::from_mm(3.0, 0.0)); 448 + let diameter = SketchDimension::Diameter { 449 + target: c, 450 + value: Length::new::<millimeter>(8.0), 451 + kind: DimensionKind::Driving, 452 + }; 453 + let Some(radius) = swap_radius_diameter(diameter) else { 454 + panic!("Diameter must swap to Radius"); 455 + }; 456 + let SketchDimension::Radius { value, .. } = radius else { 457 + panic!("expected Radius"); 458 + }; 459 + approx_eq(value.get::<millimeter>(), 4.0); 460 + let _ = s; 461 + } 462 + 463 + #[test] 464 + fn swap_does_not_affect_linear_or_angular() { 465 + let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0)); 466 + let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0)); 467 + let linear = SketchDimension::Linear { 468 + a: p0, 469 + b: p1, 470 + value: Length::new::<millimeter>(1.0), 471 + kind: DimensionKind::Driving, 472 + }; 473 + assert!(swap_radius_diameter(linear).is_none()); 474 + let _ = s; 475 + } 476 + }