Another project
0

Configure Feed

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

feat(app): sketch session w/ drag pins

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

author
Lewis
date (May 12, 2026, 11:33 AM +0300) commit f408ab8f parent 384d58a2 change-id qkpqzqss
+319 -86
+277 -82
crates/bone-app/src/sketch_mode.rs
··· 1 1 use core::num::NonZeroU32; 2 2 3 + use bone_document::{Sketch, SketchDimension, SketchEntity}; 3 4 use bone_types::{Point2, Point3, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3}; 4 5 use bone_ui::hotkey::ActionId; 5 6 ··· 17 18 ThreePointCornerRectangle, 18 19 ThreePointCenterRectangle, 19 20 Parallelogram, 20 - SmartDimension, 21 21 } 22 22 23 23 impl SketchTool { ··· 86 86 Second(ClickAnchor, ClickAnchor), 87 87 } 88 88 89 + #[derive(Copy, Clone, Debug, PartialEq)] 90 + pub struct PendingDimension { 91 + pub proto: SketchDimension, 92 + pub anchor: Point2, 93 + } 94 + 95 + #[derive(Copy, Clone, Debug, Default, PartialEq)] 96 + pub struct DragPins([Option<(SketchEntityId, Point2)>; 3]); 97 + 98 + impl DragPins { 99 + #[cfg(test)] 100 + #[must_use] 101 + pub const fn from_array(arr: [Option<(SketchEntityId, Point2)>; 3]) -> Self { 102 + Self(arr) 103 + } 104 + 105 + #[must_use] 106 + pub fn from_sketch_entity(sketch: &Sketch, id: SketchEntityId) -> Option<Self> { 107 + let entity = sketch.entities().get(id).copied()?; 108 + let pin = |pid: SketchEntityId| -> Option<(SketchEntityId, Point2)> { 109 + match sketch.entities().get(pid)? { 110 + SketchEntity::Point(p) => Some((pid, p.at())), 111 + _ => None, 112 + } 113 + }; 114 + match entity { 115 + SketchEntity::Point(p) => Some(Self([Some((id, p.at())), None, None])), 116 + SketchEntity::Line(l) => Some(Self([Some(pin(l.a())?), Some(pin(l.b())?), None])), 117 + SketchEntity::Arc(a) => Some(Self([ 118 + Some(pin(a.center())?), 119 + Some(pin(a.start())?), 120 + Some(pin(a.end())?), 121 + ])), 122 + SketchEntity::Circle(c) => Some(Self([Some(pin(c.center())?), None, None])), 123 + } 124 + } 125 + 126 + pub fn iter(&self) -> impl Iterator<Item = (SketchEntityId, Point2)> + '_ { 127 + self.0.iter().filter_map(|p| *p) 128 + } 129 + 130 + #[must_use] 131 + pub fn to_targets(self, press: Point2, cursor: Point2) -> Vec<(SketchEntityId, Point2)> { 132 + let (px, py) = press.coords_mm(); 133 + let (cx, cy) = cursor.coords_mm(); 134 + let (dx, dy) = (cx - px, cy - py); 135 + self.iter() 136 + .map(|(id, original)| { 137 + let (ox, oy) = original.coords_mm(); 138 + (id, Point2::from_mm(ox + dx, oy + dy)) 139 + }) 140 + .collect() 141 + } 142 + } 143 + 144 + #[derive(Copy, Clone, Debug, PartialEq)] 145 + pub struct DragSession { 146 + pub entity: SketchEntityId, 147 + pub press: Point2, 148 + pub pins: DragPins, 149 + } 150 + 151 + #[derive(Copy, Clone, Debug, PartialEq)] 152 + pub enum DimensionFlow { 153 + Editing(PendingDimension), 154 + Conflict(PendingDimension), 155 + } 156 + 89 157 #[derive(Copy, Clone, Debug, Default, PartialEq)] 90 158 pub struct SketchSession { 91 159 pub tool: Option<SketchTool>, 92 160 pub pending: Option<Pending>, 93 - pub drag: Option<SketchEntityId>, 161 + pub drag: Option<DragSession>, 162 + pub dim_flow: Option<DimensionFlow>, 94 163 } 95 164 96 - #[derive(Copy, Clone, Debug, Default, PartialEq)] 165 + #[derive(Clone, Debug, Default, PartialEq)] 97 166 pub enum Mode { 98 167 #[default] 99 168 Idle, 100 169 Sketch { 101 170 sketch_id: SketchId, 102 - session: SketchSession, 171 + session: Box<SketchSession>, 103 172 }, 104 173 } 105 174 106 175 impl Mode { 107 176 #[must_use] 108 - pub const fn enter_sketch(sketch_id: SketchId) -> Self { 177 + pub fn enter_sketch(sketch_id: SketchId) -> Self { 109 178 Self::Sketch { 110 179 sketch_id, 111 - session: SketchSession { 112 - tool: None, 113 - pending: None, 114 - drag: None, 115 - }, 180 + session: Box::default(), 116 181 } 117 182 } 118 183 119 184 #[must_use] 120 - pub fn arm_tool(self, kind: SketchTool) -> Self { 185 + fn map_session(self, f: impl FnOnce(SketchSession) -> SketchSession) -> Self { 121 186 match self { 122 - Self::Sketch { sketch_id, .. } => Self::Sketch { 187 + Self::Sketch { sketch_id, session } => Self::Sketch { 123 188 sketch_id, 124 - session: SketchSession { 125 - tool: Some(kind), 126 - pending: None, 127 - drag: None, 128 - }, 189 + session: Box::new(f(*session)), 129 190 }, 130 191 Self::Idle => Self::Idle, 131 192 } 132 193 } 133 194 134 195 #[must_use] 135 - pub const fn disarm_tool(self) -> Self { 136 - match self { 137 - Self::Sketch { sketch_id, .. } => Self::Sketch { 138 - sketch_id, 139 - session: SketchSession { 140 - tool: None, 141 - pending: None, 142 - drag: None, 143 - }, 144 - }, 145 - Self::Idle => Self::Idle, 146 - } 196 + pub fn arm_tool(self, kind: SketchTool) -> Self { 197 + self.map_session(|_| SketchSession { 198 + tool: Some(kind), 199 + ..SketchSession::default() 200 + }) 147 201 } 148 202 149 203 #[must_use] 150 - pub const fn clear_pending(self) -> Self { 151 - match self { 152 - Self::Sketch { sketch_id, session } => Self::Sketch { 153 - sketch_id, 154 - session: SketchSession { 155 - tool: session.tool, 156 - pending: None, 157 - drag: session.drag, 158 - }, 159 - }, 160 - Self::Idle => Self::Idle, 161 - } 204 + pub fn disarm_tool(self) -> Self { 205 + self.map_session(|_| SketchSession::default()) 162 206 } 163 207 164 208 #[must_use] 165 - pub const fn start_drag(self, entity: SketchEntityId) -> Self { 166 - match self { 167 - Self::Sketch { sketch_id, session } => Self::Sketch { 168 - sketch_id, 169 - session: SketchSession { 170 - tool: session.tool, 171 - pending: session.pending, 172 - drag: Some(entity), 173 - }, 209 + pub fn clear_pending(self) -> Self { 210 + self.map_session(|s| SketchSession { pending: None, ..s }) 211 + } 212 + 213 + #[must_use] 214 + pub fn start_drag(self, drag: DragSession) -> Self { 215 + self.map_session(|s| SketchSession { 216 + drag: Some(drag), 217 + ..s 218 + }) 219 + } 220 + 221 + #[must_use] 222 + pub fn end_drag(self) -> Self { 223 + self.map_session(|s| SketchSession { drag: None, ..s }) 224 + } 225 + 226 + #[must_use] 227 + pub fn start_dimension(self, pending: PendingDimension) -> Self { 228 + self.map_session(|s| SketchSession { 229 + tool: None, 230 + pending: None, 231 + dim_flow: Some(DimensionFlow::Editing(pending)), 232 + ..s 233 + }) 234 + } 235 + 236 + #[must_use] 237 + pub fn cancel_dimension(self) -> Self { 238 + self.map_session(|s| match s.dim_flow { 239 + Some(DimensionFlow::Editing(_)) => SketchSession { 240 + dim_flow: None, 241 + ..s 174 242 }, 175 - Self::Idle => Self::Idle, 176 - } 243 + _ => s, 244 + }) 245 + } 246 + 247 + #[must_use] 248 + pub fn start_dim_conflict(self, pending: PendingDimension) -> Self { 249 + self.map_session(|s| SketchSession { 250 + dim_flow: Some(DimensionFlow::Conflict(pending)), 251 + ..s 252 + }) 177 253 } 178 254 179 255 #[must_use] 180 - pub const fn end_drag(self) -> Self { 181 - match self { 182 - Self::Sketch { sketch_id, session } => Self::Sketch { 183 - sketch_id, 184 - session: SketchSession { 185 - tool: session.tool, 186 - pending: session.pending, 187 - drag: None, 188 - }, 256 + pub fn cancel_dim_conflict(self) -> Self { 257 + self.map_session(|s| match s.dim_flow { 258 + Some(DimensionFlow::Conflict(_)) => SketchSession { 259 + dim_flow: None, 260 + ..s 189 261 }, 190 - Self::Idle => Self::Idle, 191 - } 262 + _ => s, 263 + }) 192 264 } 193 265 194 266 #[must_use] ··· 199 271 200 272 #[cfg(test)] 201 273 mod tests { 202 - use super::{ClickAnchor, Mode, Pending, Plane, SketchSession, SketchTool}; 274 + use super::{ 275 + ClickAnchor, DimensionFlow, DragPins, DragSession, Mode, Pending, Plane, SketchSession, 276 + SketchTool, 277 + }; 203 278 use bone_types::{Point2, SketchEntityId, SketchId}; 204 279 205 280 #[test] ··· 223 298 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 224 299 1.0, 2.0, 225 300 )))), 226 - drag: None, 301 + ..SketchSession::default() 227 302 }; 228 303 let mode = Mode::Sketch { 229 304 sketch_id: SketchId::default(), 230 - session, 305 + session: Box::new(session), 231 306 } 232 307 .arm_tool(SketchTool::Point); 233 308 let Mode::Sketch { session, .. } = mode else { ··· 244 319 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 245 320 3.0, 4.0, 246 321 )))), 247 - drag: None, 322 + ..SketchSession::default() 248 323 }; 249 324 let mode = Mode::Sketch { 250 325 sketch_id: SketchId::default(), 251 - session, 326 + session: Box::new(session), 252 327 } 253 328 .clear_pending(); 254 329 let Mode::Sketch { session, .. } = mode else { ··· 263 338 assert_eq!(Mode::Idle.clear_pending(), Mode::Idle); 264 339 } 265 340 341 + fn sample_drag_session() -> DragSession { 342 + let entity = SketchEntityId::default(); 343 + DragSession { 344 + entity, 345 + press: Point2::origin(), 346 + pins: DragPins::from_array([Some((entity, Point2::origin())), None, None]), 347 + } 348 + } 349 + 266 350 #[test] 267 351 fn start_drag_records_entity_keeps_tool_and_pending() { 268 - let entity = SketchEntityId::default(); 352 + let drag = sample_drag_session(); 269 353 let mode = Mode::Sketch { 270 354 sketch_id: SketchId::default(), 271 - session: SketchSession { 355 + session: Box::new(SketchSession { 272 356 tool: Some(SketchTool::Line), 273 357 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 274 358 0.0, 0.0, 275 359 )))), 276 - drag: None, 277 - }, 360 + ..SketchSession::default() 361 + }), 278 362 } 279 - .start_drag(entity); 363 + .start_drag(drag); 280 364 let Mode::Sketch { session, .. } = mode else { 281 365 panic!("expected sketch mode"); 282 366 }; 283 - assert_eq!(session.drag, Some(entity)); 367 + assert_eq!(session.drag, Some(drag)); 284 368 assert_eq!(session.tool, Some(SketchTool::Line)); 285 369 assert!(session.pending.is_some()); 286 370 } 287 371 288 372 #[test] 289 373 fn end_drag_clears_drag_only() { 290 - let entity = SketchEntityId::default(); 291 374 let mode = Mode::Sketch { 292 375 sketch_id: SketchId::default(), 293 - session: SketchSession { 376 + session: Box::new(SketchSession { 294 377 tool: Some(SketchTool::Line), 295 - pending: None, 296 - drag: Some(entity), 297 - }, 378 + drag: Some(sample_drag_session()), 379 + ..SketchSession::default() 380 + }), 298 381 } 299 382 .end_drag(); 300 383 let Mode::Sketch { session, .. } = mode else { ··· 306 389 307 390 #[test] 308 391 fn start_drag_in_idle_is_noop() { 309 - assert_eq!(Mode::Idle.start_drag(SketchEntityId::default()), Mode::Idle); 392 + assert_eq!(Mode::Idle.start_drag(sample_drag_session()), Mode::Idle); 310 393 } 311 394 312 395 #[test] ··· 315 398 } 316 399 317 400 #[test] 318 - fn entities_table_excludes_smart_dimension() { 319 - assert!(!SketchTool::ENTITIES.contains(&SketchTool::SmartDimension)); 401 + fn entities_table_holds_day_one_set() { 320 402 assert_eq!( 321 403 SketchTool::ENTITIES.len(), 322 404 12, ··· 334 416 fn is_sketch_distinguishes_states() { 335 417 assert!(!Mode::Idle.is_sketch()); 336 418 assert!(Mode::enter_sketch(SketchId::default()).is_sketch()); 419 + } 420 + 421 + #[test] 422 + fn start_dimension_records_pending_and_clears_tool() { 423 + use bone_document::{DimensionKind, SketchDimension}; 424 + use bone_types::Length; 425 + use uom::si::length::millimeter; 426 + let proto = SketchDimension::Linear { 427 + a: SketchEntityId::default(), 428 + b: SketchEntityId::default(), 429 + value: Length::new::<millimeter>(5.0), 430 + kind: DimensionKind::Driving, 431 + }; 432 + let pending = super::PendingDimension { 433 + proto, 434 + anchor: Point2::origin(), 435 + }; 436 + let mode = Mode::enter_sketch(SketchId::default()) 437 + .arm_tool(SketchTool::Line) 438 + .start_dimension(pending); 439 + let Mode::Sketch { session, .. } = mode else { 440 + panic!("expected sketch mode"); 441 + }; 442 + assert_eq!(session.tool, None); 443 + assert_eq!(session.pending, None); 444 + assert_eq!(session.dim_flow, Some(DimensionFlow::Editing(pending))); 445 + } 446 + 447 + #[test] 448 + fn cancel_dimension_clears_editing_only() { 449 + use bone_document::{DimensionKind, SketchDimension}; 450 + use bone_types::Length; 451 + use uom::si::length::millimeter; 452 + let proto = SketchDimension::Linear { 453 + a: SketchEntityId::default(), 454 + b: SketchEntityId::default(), 455 + value: Length::new::<millimeter>(5.0), 456 + kind: DimensionKind::Driving, 457 + }; 458 + let pending = super::PendingDimension { 459 + proto, 460 + anchor: Point2::origin(), 461 + }; 462 + let mode = Mode::enter_sketch(SketchId::default()) 463 + .start_dimension(pending) 464 + .cancel_dimension(); 465 + let Mode::Sketch { session, .. } = mode else { 466 + panic!("expected sketch mode"); 467 + }; 468 + assert_eq!(session.dim_flow, None); 469 + } 470 + 471 + #[test] 472 + fn cancel_dimension_leaves_conflict_intact() { 473 + use bone_document::{DimensionKind, SketchDimension}; 474 + use bone_types::Length; 475 + use uom::si::length::millimeter; 476 + let proto = SketchDimension::Linear { 477 + a: SketchEntityId::default(), 478 + b: SketchEntityId::default(), 479 + value: Length::new::<millimeter>(5.0), 480 + kind: DimensionKind::Driving, 481 + }; 482 + let pending = super::PendingDimension { 483 + proto, 484 + anchor: Point2::origin(), 485 + }; 486 + let mode = Mode::enter_sketch(SketchId::default()) 487 + .start_dim_conflict(pending) 488 + .cancel_dimension(); 489 + let Mode::Sketch { session, .. } = mode else { 490 + panic!("expected sketch mode"); 491 + }; 492 + assert_eq!(session.dim_flow, Some(DimensionFlow::Conflict(pending))); 493 + } 494 + 495 + #[test] 496 + fn start_dimension_after_existing_keeps_drag_replaces_proto() { 497 + use bone_document::{DimensionKind, SketchDimension}; 498 + use bone_types::Length; 499 + use uom::si::length::millimeter; 500 + let radius = SketchDimension::Radius { 501 + target: SketchEntityId::default(), 502 + value: Length::new::<millimeter>(3.0), 503 + kind: DimensionKind::Driving, 504 + }; 505 + let diameter = SketchDimension::Diameter { 506 + target: SketchEntityId::default(), 507 + value: Length::new::<millimeter>(6.0), 508 + kind: DimensionKind::Driving, 509 + }; 510 + let initial = super::PendingDimension { 511 + proto: radius, 512 + anchor: Point2::origin(), 513 + }; 514 + let next = super::PendingDimension { 515 + proto: diameter, 516 + anchor: Point2::from_mm(1.0, 0.0), 517 + }; 518 + let mode = Mode::Sketch { 519 + sketch_id: SketchId::default(), 520 + session: Box::new(SketchSession { 521 + drag: Some(sample_drag_session()), 522 + dim_flow: Some(DimensionFlow::Editing(initial)), 523 + ..SketchSession::default() 524 + }), 525 + } 526 + .start_dimension(next); 527 + let Mode::Sketch { session, .. } = mode else { 528 + panic!("expected sketch mode"); 529 + }; 530 + assert_eq!(session.dim_flow, Some(DimensionFlow::Editing(next))); 531 + assert!(session.drag.is_some(), "drag preserved across swap"); 337 532 } 338 533 339 534 #[test]
+42 -2
crates/bone-app/src/strings.rs
··· 46 46 pub const REL_HINT_ENTITY: StringKey = StringKey::new("rel.hint.entity"); 47 47 48 48 pub const TOOL_SMART_DIMENSION: StringKey = StringKey::new("tool.smart_dimension"); 49 + pub const TOOL_RADIUS: StringKey = StringKey::new("tool.radius"); 50 + pub const TOOL_DIAMETER: StringKey = StringKey::new("tool.diameter"); 51 + 52 + pub const DIM_HINT_GENERIC: StringKey = StringKey::new("dim.hint.generic"); 53 + pub const DIM_PLACEHOLDER_LENGTH: StringKey = StringKey::new("dim.placeholder.length"); 54 + pub const DIM_PLACEHOLDER_ANGLE: StringKey = StringKey::new("dim.placeholder.angle"); 55 + pub const DIM_CONFLICT_TITLE: StringKey = StringKey::new("dim.conflict.title"); 56 + pub const DIM_CONFLICT_MESSAGE: StringKey = StringKey::new("dim.conflict.message"); 57 + pub const DIM_CONFLICT_MAKE_DRIVEN: StringKey = StringKey::new("dim.conflict.make_driven"); 58 + pub const DIM_CONFLICT_CANCEL: StringKey = StringKey::new("dim.conflict.cancel"); 49 59 50 60 pub const FEATURE_TREE_LABEL: StringKey = StringKey::new("shell.feature_tree"); 51 61 pub const FEATURE_ORIGIN: StringKey = StringKey::new("feature.origin"); ··· 103 113 pub fn make_strings(locale: Locale) -> StringTable { 104 114 let mut table = StringTable::for_locale(locale); 105 115 let entries: &[(StringKey, &str)] = match locale { 106 - Locale::EnGb => EN_GB, 116 + Locale::EnUs => EN_US, 107 117 Locale::ArXb => AR_XB, 108 118 }; 109 119 entries.iter().for_each(|(key, value)| { ··· 112 122 table 113 123 } 114 124 115 - const EN_GB: &[(StringKey, &str)] = &[ 125 + const EN_US: &[(StringKey, &str)] = &[ 116 126 (APP_TITLE, "Bone"), 117 127 (RIBBON_LABEL, "Ribbon"), 118 128 (RIBBON_TAB_SKETCH, "Sketch"), ··· 158 168 (REL_HINT_MIDPOINT, "Select a point and a line"), 159 169 (REL_HINT_ENTITY, "Select an entity"), 160 170 (TOOL_SMART_DIMENSION, "Smart Dimension"), 171 + (TOOL_RADIUS, "Radius"), 172 + (TOOL_DIAMETER, "Diameter"), 173 + ( 174 + DIM_HINT_GENERIC, 175 + "Select a line, two points, an arc, a circle, or two lines", 176 + ), 177 + (DIM_PLACEHOLDER_LENGTH, "value (mm)"), 178 + (DIM_PLACEHOLDER_ANGLE, "value (deg)"), 179 + (DIM_CONFLICT_TITLE, "Make Dimension Driven?"), 180 + ( 181 + DIM_CONFLICT_MESSAGE, 182 + "Adding this dimension will over-define the sketch.", 183 + ), 184 + (DIM_CONFLICT_MAKE_DRIVEN, "Make driven"), 185 + (DIM_CONFLICT_CANCEL, "Cancel"), 161 186 (FEATURE_TREE_LABEL, "Feature Tree"), 162 187 (FEATURE_ORIGIN, "Origin"), 163 188 (FEATURE_PLANE_XY, "Front Plane"), ··· 259 284 (REL_HINT_MIDPOINT, "[!! Sêlect a pôint ând a lîne !!]"), 260 285 (REL_HINT_ENTITY, "[!! Sêlect an êntity !!]"), 261 286 (TOOL_SMART_DIMENSION, "[!! Smârt Dimensiôn !!]"), 287 + (TOOL_RADIUS, "[!! Râdius !!]"), 288 + (TOOL_DIAMETER, "[!! Dîameter !!]"), 289 + ( 290 + DIM_HINT_GENERIC, 291 + "[!! Sêlect a lîne, twô pôints, an ârc, a cîrcle, ôr twô lînes !!]", 292 + ), 293 + (DIM_PLACEHOLDER_LENGTH, "[!! valûe (mm) !!]"), 294 + (DIM_PLACEHOLDER_ANGLE, "[!! valûe (dêg) !!]"), 295 + (DIM_CONFLICT_TITLE, "[!! Mâke Dîmension Drîven? !!]"), 296 + ( 297 + DIM_CONFLICT_MESSAGE, 298 + "[!! Âdding this dîmension will over-defîne the skêtch. !!]", 299 + ), 300 + (DIM_CONFLICT_MAKE_DRIVEN, "[!! Mâke drîven !!]"), 301 + (DIM_CONFLICT_CANCEL, "[!! Cancêl !!]"), 262 302 (FEATURE_TREE_LABEL, "[!! Featûre Tree !!]"), 263 303 (FEATURE_ORIGIN, "[!! Orîgin !!]"), 264 304 (FEATURE_PLANE_XY, "[!! Front Plàne !!]"),
-1
crates/bone-app/src/tools/mod.rs
··· 135 135 place::place_three_point_center_rectangle(sketch, world, pending, snap) 136 136 } 137 137 SketchTool::Parallelogram => place::place_parallelogram(sketch, world, pending, snap), 138 - SketchTool::SmartDimension => (None, pending), 139 138 } 140 139 } 141 140
-1
crates/bone-app/src/tools/preview.rs
··· 46 46 preview_three_point_center_rectangle(sketch, effective, pending, snap_dot) 47 47 } 48 48 SketchTool::Parallelogram => preview_parallelogram(sketch, effective, pending, snap_dot), 49 - SketchTool::SmartDimension => SketchPreview::empty(), 50 49 } 51 50 } 52 51