Another project
0

Configure Feed

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

feat(app,render): dimension flow, drag pins, layered chrome

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

author
Lewis
date (May 16, 2026, 11:09 AM +0300) commit 3225f7b5 parent 4a463187 change-id usnusppt
+737 -192
+1
Cargo.lock
··· 220 220 "tracing", 221 221 "tracing-subscriber", 222 222 "uom", 223 + "wgpu", 223 224 "winit", 224 225 ] 225 226
+1
crates/bone-app/Cargo.toml
··· 12 12 bone-text = { workspace = true } 13 13 bone-ui = { workspace = true } 14 14 swash = { workspace = true } 15 + wgpu = { workspace = true } 15 16 16 17 pollster = { workspace = true } 17 18 thiserror = { workspace = true }
+697 -164
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::{Document, Sketch, SketchEdit, SketchEntity, SketchRelation, UndoStack}; 6 + use bone_document::{ 7 + DimensionKind, DimensionValue, Document, Sketch, SketchDimension, SketchEdit, SketchEntity, 8 + SketchRelation, SolverError, UndoStack, 9 + }; 7 10 use bone_render::{ 8 11 Camera2, ChromeInstance, ChromePipeline, ChromeTextPipeline, PickQuery, PickedItem, 9 12 PixelsPerMm, RenderTargets, SdfGlyphInstance, SketchPreview, SketchRenderer, SketchScene, ··· 33 36 dpi::{PhysicalPosition, PhysicalSize}, 34 37 event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}, 35 38 event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, 36 - keyboard::{KeyCode, ModifiersState, PhysicalKey}, 39 + keyboard::{Key, KeyCode, ModifiersState, NamedKey as WinitNamed, PhysicalKey}, 37 40 window::{Window, WindowId}, 38 41 }; 39 42 40 43 mod chrome; 44 + mod dimension_editor; 41 45 mod relation_tools; 42 46 mod selection; 43 47 mod shell; 44 48 mod sketch_mode; 49 + mod smart_dimension; 45 50 mod snap; 46 51 mod strings; 47 52 mod tools; 48 53 54 + use dimension_editor::{DimensionEditorAction, DimensionEditorOutcome, DimensionEditorState}; 49 55 use selection::Selection; 50 - use sketch_mode::{ClickAnchor, Mode, Pending, Plane, SketchSession, SketchTool}; 56 + use sketch_mode::{ 57 + ClickAnchor, DimensionFlow, DragPins, DragSession, Mode, Pending, PendingDimension, Plane, 58 + SketchTool, 59 + }; 51 60 use snap::{Anchor, SnapHit}; 52 61 53 62 #[derive(Debug, thiserror::Error)] ··· 91 100 const UNDO_CAPACITY: usize = 256; 92 101 const SNAP_TOLERANCE_PX: f64 = 8.0; 93 102 const SNAP_TOLERANCE_MAX_MM: f64 = 5.0; 103 + const REDRAW_KICKS_AFTER_INTERACTION: u8 = 2; 94 104 95 105 struct RenderState { 96 106 surface: SurfaceContext, ··· 116 126 viewport_rect: LayoutRect, 117 127 undo: UndoStack, 118 128 selection: Selection, 129 + dim_editor: DimensionEditorState, 130 + dim_editor_bounds: Option<LayoutRect>, 119 131 pending_exit: bool, 132 + redraw_kicks: u8, 120 133 } 121 134 122 135 #[derive(Default)] ··· 286 299 .with_pan(Vec2::from_mm(new_pan_x, new_pan_y)) 287 300 } 288 301 289 - const fn armed_sketch_tool(mode: &Mode) -> Option<SketchTool> { 302 + fn armed_sketch_tool(mode: &Mode) -> Option<SketchTool> { 290 303 match mode { 291 - Mode::Sketch { 292 - session: SketchSession { tool, .. }, 293 - .. 294 - } => *tool, 304 + Mode::Sketch { session, .. } => session.tool, 295 305 Mode::Idle => None, 296 306 } 297 307 } 298 308 299 - const fn dragging_in_sketch(mode: &Mode) -> bool { 309 + fn dragging_in_sketch(mode: &Mode) -> bool { 300 310 matches!( 301 311 mode, 302 - Mode::Sketch { 303 - session: SketchSession { drag: Some(_), .. }, 304 - .. 305 - } 312 + Mode::Sketch { session, .. } if session.drag.is_some() 313 + ) 314 + } 315 + 316 + fn dim_flow_active(mode: &Mode) -> bool { 317 + matches!( 318 + mode, 319 + Mode::Sketch { session, .. } if session.dim_flow.is_some() 306 320 ) 307 321 } 308 322 ··· 322 336 } 323 337 324 338 fn try_place(state: &mut RenderState, world: Point2) { 325 - let Mode::Sketch { 326 - sketch_id, 327 - session: 328 - SketchSession { 329 - tool: Some(tool), 330 - pending: prev_pending, 331 - drag: _, 332 - }, 333 - } = state.mode 334 - else { 339 + let Mode::Sketch { sketch_id, session } = &state.mode else { 340 + return; 341 + }; 342 + let Some(tool) = session.tool else { 335 343 return; 336 344 }; 345 + let prev_pending = session.pending; 346 + let sketch_id = *sketch_id; 337 347 let Some(sketch) = state.document.sketch(sketch_id).cloned() else { 338 348 return; 339 349 }; ··· 419 429 Some(_) | None => None, 420 430 }; 421 431 state.selection = std::mem::take(&mut state.selection).picked(picked_id, additive); 422 - if let Some(PickedItem::Point(entity)) = picked 423 - && !additive 424 - && state.mode.is_sketch() 425 - { 426 - state.undo.record(state.document.clone()); 427 - state.mode = state.mode.start_drag(entity); 432 + if additive || !state.mode.is_sketch() { 433 + return; 428 434 } 435 + let Some(entity_id) = picked_id else { 436 + return; 437 + }; 438 + let Mode::Sketch { sketch_id, .. } = state.mode else { 439 + return; 440 + }; 441 + let Some(sketch) = state.document.sketch(sketch_id) else { 442 + return; 443 + }; 444 + let Some(world) = cursor_to_world(state.camera, cursor) else { 445 + return; 446 + }; 447 + let Some(pins) = DragPins::from_sketch_entity(sketch, entity_id) else { 448 + return; 449 + }; 450 + let drag = DragSession { 451 + entity: entity_id, 452 + press: world, 453 + pins, 454 + }; 455 + state.undo.record(state.document.clone()); 456 + state.mode = core::mem::take(&mut state.mode).start_drag(drag); 429 457 } 430 458 431 459 fn try_drag_to(state: &mut RenderState, world: Point2) { 432 - let Mode::Sketch { 433 - sketch_id, 434 - session: SketchSession { 435 - drag: Some(entity), .. 436 - }, 437 - } = state.mode 438 - else { 460 + let Mode::Sketch { sketch_id, session } = &state.mode else { 439 461 return; 440 462 }; 463 + let Some(drag) = session.drag else { 464 + return; 465 + }; 466 + let sketch_id = *sketch_id; 441 467 let Some(sketch) = state.document.sketch(sketch_id).cloned() else { 442 468 return; 443 469 }; 444 - let next = match sketch.solve_with_drag(entity, world, BudgetCeiling::FRAME_16MS) { 470 + let pins = drag.pins.to_targets(drag.press, world); 471 + let next = match sketch.solve_with_drag_pins(&pins, BudgetCeiling::FRAME_16MS) { 445 472 Ok(solved) => solved, 446 473 Err(e) => { 447 474 tracing::trace!(error = %e, "drag solve failed; falling back to raw move"); 448 - let Ok((next, _)) = sketch.apply(SketchEdit::MovePoint { 449 - id: entity, 450 - position: world, 451 - }) else { 475 + let Some(baseline) = state.document.sketch(sketch_id).cloned() else { 452 476 return; 453 477 }; 454 - next 478 + let folded = pins.iter().try_fold(baseline, |acc, (id, target)| { 479 + acc.apply(SketchEdit::MovePoint { 480 + id: *id, 481 + position: *target, 482 + }) 483 + .map(|(s, _)| s) 484 + }); 485 + match folded { 486 + Ok(s) => s, 487 + Err(_) => return, 488 + } 455 489 } 456 490 }; 457 491 state.document.replace_sketch(sketch_id, next); ··· 484 518 cursor_world: Option<Point2>, 485 519 camera: &Camera2, 486 520 ) -> SketchPreview { 487 - let Mode::Sketch { 488 - sketch_id, 489 - session: 490 - SketchSession { 491 - tool: Some(tool), 492 - pending, 493 - drag: None, 494 - }, 495 - } = mode 496 - else { 521 + let Mode::Sketch { sketch_id, session } = mode else { 497 522 return SketchPreview::empty(); 498 523 }; 524 + if session.drag.is_some() || session.dim_flow.is_some() { 525 + return SketchPreview::empty(); 526 + } 527 + let Some(tool) = session.tool else { 528 + return SketchPreview::empty(); 529 + }; 530 + let pending = session.pending; 499 531 let Some(sketch) = document.sketch(*sketch_id) else { 500 532 return SketchPreview::empty(); 501 533 }; 502 534 let Some(cursor) = cursor_world else { 503 - return tools::preview_anchors_only(sketch, *pending); 535 + return tools::preview_anchors_only(sketch, pending); 504 536 }; 505 537 let snap = match tool { 506 - SketchTool::Line => compute_snap(sketch, camera, cursor, latest_anchor(*pending)), 538 + SketchTool::Line => compute_snap(sketch, camera, cursor, latest_anchor(pending)), 507 539 _ => compute_endpoint_snap(sketch, camera, cursor), 508 540 }; 509 - tools::preview(sketch, *tool, cursor, *pending, snap) 541 + tools::preview(sketch, tool, cursor, pending, snap) 510 542 } 511 543 512 544 fn snap_tolerance(camera: &Camera2) -> Option<Length> { ··· 695 727 actions: &[ActionId], 696 728 plane_sketches: &BTreeMap<Plane, SketchId>, 697 729 ) -> Mode { 698 - let after_pick = frame 730 + let pick = frame 699 731 .plane_picked 700 732 .filter(|_| !mode.is_sketch()) 701 - .and_then(|plane| plane_sketches.get(&plane).copied()) 702 - .map_or(mode, Mode::enter_sketch); 733 + .and_then(|plane| plane_sketches.get(&plane).copied()); 734 + let after_pick = pick.map_or(mode, Mode::enter_sketch); 703 735 let escape = actions.contains(&sketch_mode::EXIT_SKETCH_ACTION); 704 736 let after_escape = if escape { 705 737 cancel_pending_or_exit(after_pick) ··· 711 743 } else { 712 744 after_escape 713 745 }; 714 - frame 715 - .activated_tool 716 - .map_or(after_exit, |t| toggle_or_arm(after_exit, t)) 746 + match frame.activated_tool { 747 + Some(t) => toggle_or_arm(after_exit, t), 748 + None => after_exit, 749 + } 717 750 } 718 751 719 752 fn toggle_or_arm(mode: Mode, tool: SketchTool) -> Mode { 720 - match mode { 721 - Mode::Sketch { 722 - session: SketchSession { 723 - tool: Some(active), .. 724 - }, 725 - .. 726 - } if active == tool => mode.disarm_tool(), 727 - _ => mode.arm_tool(tool), 753 + let already_active = matches!( 754 + &mode, 755 + Mode::Sketch { session, .. } if session.tool == Some(tool) 756 + ); 757 + if already_active { 758 + mode.disarm_tool() 759 + } else { 760 + mode.arm_tool(tool) 728 761 } 729 762 } 730 763 731 764 fn cancel_pending_or_exit(mode: Mode) -> Mode { 732 - match mode { 733 - Mode::Sketch { 734 - session: SketchSession { 735 - pending: Some(_), .. 736 - }, 737 - .. 738 - } => mode.clear_pending(), 739 - Mode::Sketch { 740 - session: SketchSession { tool: Some(_), .. }, 741 - .. 742 - } => mode.disarm_tool(), 765 + match &mode { 766 + Mode::Sketch { session, .. } if session.pending.is_some() => mode.clear_pending(), 767 + Mode::Sketch { session, .. } if session.tool.is_some() => mode.disarm_tool(), 743 768 Mode::Sketch { .. } | Mode::Idle => Mode::Idle, 744 769 } 745 770 } ··· 797 822 }; 798 823 let (document, sketch_id) = initial_document(sketch); 799 824 let plane_sketches = BTreeMap::from([(Plane::Xy, sketch_id)]); 800 - let strings = strings::make_strings(bone_ui::strings::Locale::EnGb); 825 + let strings = strings::make_strings(bone_ui::strings::Locale::EnUs); 801 826 let viewport_rect = empty_rect(); 802 827 let Some(undo_capacity) = NonZeroUsize::new(UNDO_CAPACITY) else { 803 828 unreachable!("UNDO_CAPACITY constant is non-zero"); ··· 828 853 viewport_rect, 829 854 undo: UndoStack::with_capacity(undo_capacity), 830 855 selection: Selection::default(), 856 + dim_editor: DimensionEditorState::default(), 857 + dim_editor_bounds: None, 831 858 pending_exit: false, 859 + redraw_kicks: 0, 832 860 }); 833 861 } 834 862 ··· 883 911 } => { 884 912 if btn_state == ElementState::Pressed { 885 913 let in_viewport = self.input.cursor_in(state.viewport_rect); 886 - self.input.left_pan = in_viewport; 914 + let over_dim_editor = state 915 + .dim_editor_bounds 916 + .is_some_and(|r| self.input.cursor_in(r)); 917 + let dim_active = dim_flow_active(&state.mode); 918 + self.input.left_pan = in_viewport && !over_dim_editor && !dim_active; 887 919 self.input.pending_pressed = 888 920 self.input.pending_pressed.with(PointerButton::Primary); 889 921 if in_viewport 922 + && !over_dim_editor 923 + && !dim_active 890 924 && !self.input.modifiers.shift_key() 891 925 && let Some(cursor) = self.input.cursor_px 892 926 { ··· 902 936 } 903 937 } else { 904 938 self.input.left_pan = false; 905 - state.mode = state.mode.end_drag(); 939 + state.mode = core::mem::take(&mut state.mode).end_drag(); 906 940 self.input.pending_released = 907 941 self.input.pending_released.with(PointerButton::Primary); 908 942 } ··· 917 951 self.input.pending_pressed = 918 952 self.input.pending_pressed.with(PointerButton::Secondary); 919 953 if self.input.cursor_in(state.viewport_rect) { 920 - state.mode = state.mode.clear_pending(); 954 + state.mode = core::mem::take(&mut state.mode).clear_pending(); 921 955 } 922 956 } else { 923 957 self.input.pending_released = ··· 947 981 WindowEvent::KeyboardInput { 948 982 event: 949 983 KeyEvent { 950 - physical_key: PhysicalKey::Code(code), 984 + physical_key, 985 + logical_key, 951 986 state: ElementState::Pressed, 987 + text, 952 988 .. 953 989 }, 954 990 .. 955 991 } => { 956 - if let Some(named) = keycode_to_named(code) { 957 - self.input.pending_keys.push(UiKeyEvent::new( 958 - UiKeyCode::Named(named), 959 - self.input.modifier_mask(), 960 - )); 961 - } else if let Some(c) = keycode_to_char(code) { 992 + let physical_code = match physical_key { 993 + PhysicalKey::Code(c) => Some(c), 994 + PhysicalKey::Unidentified(_) => None, 995 + }; 996 + let logical_named = match &logical_key { 997 + Key::Named(nk) => winit_named_to_ui(*nk), 998 + _ => None, 999 + }; 1000 + let physical_named = physical_code.and_then(keycode_to_named); 1001 + let named = logical_named.or(physical_named); 1002 + let mods = self.input.modifier_mask(); 1003 + if let Some(named) = named { 1004 + self.input 1005 + .pending_keys 1006 + .push(UiKeyEvent::new(UiKeyCode::Named(named), mods)); 1007 + } else if let Some(c) = physical_code.and_then(keycode_to_char) { 962 1008 self.input.pending_keys.push(UiKeyEvent::new( 963 1009 UiKeyCode::Char(KeyChar::from_char(c)), 964 - self.input.modifier_mask(), 1010 + mods, 965 1011 )); 966 1012 } 967 - match keyboard_action(code, &self.input, state) { 968 - Some(KeyAction::Exit) => event_loop.exit(), 969 - Some(KeyAction::Camera(next)) => { 970 - state.camera = next; 1013 + if let Some(typed) = text.as_ref() { 1014 + let filtered: String = typed.chars().filter(|c| !c.is_control()).collect(); 1015 + if !filtered.is_empty() { 1016 + self.input.pending_text.push_str(&filtered); 971 1017 } 972 - None => {} 1018 + } 1019 + let suppress_camera = dim_flow_active(&state.mode); 1020 + if let Some(code) = physical_code { 1021 + match keyboard_action(code, &self.input, state) { 1022 + Some(KeyAction::Exit) => event_loop.exit(), 1023 + Some(KeyAction::Camera(next)) if !suppress_camera => { 1024 + state.camera = next; 1025 + } 1026 + Some(KeyAction::Camera(_)) | None => {} 1027 + } 973 1028 } 974 1029 window.request_redraw(); 975 1030 } ··· 990 1045 let mut hits = HitFrame::new(); 991 1046 let mut a11y = AccessTreeBuilder::new(); 992 1047 let scopes = scopes_for_mode(&state.mode); 993 - let (frame, hotkey_actions) = { 994 - let mut ctx = FrameCtx::new( 995 - theme, 996 - &mut input, 997 - &mut state.focus, 998 - &state.hotkeys, 999 - &state.strings, 1000 - &mut hits, 1001 - &state.hit_state, 1002 - &mut a11y, 1003 - ); 1004 - let frame = state.shell.render( 1005 - &mut ctx, 1006 - &state.document, 1007 - &state.mode, 1008 - state.selection.ids(), 1009 - layout_size, 1010 - ); 1011 - let actions = ctx.dispatch_hotkeys(&scopes); 1012 - (frame, actions) 1048 + let (mut frame, hotkey_actions, dim_outcome, conflict_outcome) = run_frame_ui( 1049 + state, 1050 + theme, 1051 + &mut input, 1052 + &mut hits, 1053 + &mut a11y, 1054 + &scopes, 1055 + layout_size, 1056 + ); 1057 + state.dim_editor_bounds = apply_popup_overlays( 1058 + &mut frame.overlay_paints, 1059 + dim_outcome.as_ref(), 1060 + conflict_outcome.as_ref(), 1061 + ); 1062 + let claimed_pointer = dim_outcome.as_ref().is_some_and(|o| o.claimed_pointer); 1063 + let frame = if claimed_pointer { 1064 + suppress_pointer_activations(frame) 1065 + } else { 1066 + frame 1013 1067 }; 1014 1068 state.viewport_rect = frame.viewport_rect; 1015 - state.hit_state = resolve(&state.hit_state, &hits, &input, state.focus.focused()); 1069 + apply_resolve_and_kick_redraws(state, &hits, &input); 1016 1070 if let Some(plane) = frame.plane_picked { 1017 1071 match ( 1018 1072 state.mode.is_sketch(), ··· 1027 1081 (false, true) => {} 1028 1082 } 1029 1083 } 1030 - state.mode = next_mode(state.mode, &frame, &hotkey_actions, &state.plane_sketches); 1084 + state.mode = next_mode( 1085 + core::mem::take(&mut state.mode), 1086 + &frame, 1087 + &hotkey_actions, 1088 + &state.plane_sketches, 1089 + ); 1090 + apply_dimension_outcome(state, dim_outcome); 1091 + apply_dim_conflict_outcome(state, conflict_outcome); 1092 + apply_dimension_request(state, frame.activated_dimension); 1031 1093 apply_undo_actions(state, &hotkey_actions); 1032 1094 apply_menu_action(state, frame.menu_action); 1033 1095 apply_relation_action(state, frame.activated_relation); ··· 1046 1108 ]; 1047 1109 let surface = &mut state.surface; 1048 1110 let renderer = &mut state.renderer; 1049 - let chrome_pipeline = &mut state.chrome_pipeline; 1050 - let text_pipeline = &mut state.text_pipeline; 1111 + let mut chrome_stage = ChromeStage { 1112 + chrome: &mut state.chrome_pipeline, 1113 + text: &mut state.text_pipeline, 1114 + atlas_pixels, 1115 + atlas_version, 1116 + viewport_px, 1117 + }; 1051 1118 let scene = &state.scene; 1052 1119 let camera = state.camera; 1053 1120 let style = &state.style; ··· 1062 1129 camera, 1063 1130 style, 1064 1131 ); 1065 - [&main_layer, &overlay_layer].into_iter().for_each(|layer| { 1066 - chrome_pipeline.draw(encoder, color, viewport_px, &layer.chrome); 1067 - text_pipeline.draw( 1068 - encoder, 1069 - color, 1070 - viewport_px, 1071 - atlas_pixels, 1072 - atlas_version, 1073 - &layer.glyphs, 1074 - ); 1075 - }); 1132 + chrome_stage.encode_layered(encoder, color, &main_layer, &overlay_layer); 1076 1133 }, 1077 1134 || window.pre_present_notify(), 1078 1135 ); 1136 + if state.redraw_kicks > 0 { 1137 + state.redraw_kicks -= 1; 1138 + window.request_redraw(); 1139 + } 1140 + } 1141 + 1142 + struct ChromeStage<'a> { 1143 + chrome: &'a mut ChromePipeline, 1144 + text: &'a mut ChromeTextPipeline, 1145 + atlas_pixels: &'a [u8], 1146 + atlas_version: u64, 1147 + viewport_px: [f32; 2], 1148 + } 1149 + 1150 + impl ChromeStage<'_> { 1151 + fn encode_layered( 1152 + &mut self, 1153 + encoder: &mut wgpu::CommandEncoder, 1154 + color: &wgpu::TextureView, 1155 + main: &ChromeLayer, 1156 + overlay: &ChromeLayer, 1157 + ) { 1158 + let combined_chrome: Vec<ChromeInstance> = main 1159 + .chrome 1160 + .iter() 1161 + .chain(overlay.chrome.iter()) 1162 + .copied() 1163 + .collect(); 1164 + let combined_glyphs: Vec<SdfGlyphInstance> = main 1165 + .glyphs 1166 + .iter() 1167 + .chain(overlay.glyphs.iter()) 1168 + .copied() 1169 + .collect(); 1170 + let Ok(main_chrome) = u32::try_from(main.chrome.len()) else { 1171 + unreachable!("chrome instance count fits in u32") 1172 + }; 1173 + let Ok(total_chrome) = u32::try_from(combined_chrome.len()) else { 1174 + unreachable!("chrome instance count fits in u32") 1175 + }; 1176 + let Ok(main_glyphs) = u32::try_from(main.glyphs.len()) else { 1177 + unreachable!("glyph instance count fits in u32") 1178 + }; 1179 + let Ok(total_glyphs) = u32::try_from(combined_glyphs.len()) else { 1180 + unreachable!("glyph instance count fits in u32") 1181 + }; 1182 + self.chrome.upload(self.viewport_px, &combined_chrome); 1183 + self.text.upload( 1184 + self.viewport_px, 1185 + self.atlas_pixels, 1186 + self.atlas_version, 1187 + &combined_glyphs, 1188 + ); 1189 + self.chrome.draw_range(encoder, color, 0..main_chrome); 1190 + self.text.draw_range(encoder, color, 0..main_glyphs); 1191 + self.chrome 1192 + .draw_range(encoder, color, main_chrome..total_chrome); 1193 + self.text 1194 + .draw_range(encoder, color, main_glyphs..total_glyphs); 1195 + } 1196 + } 1197 + 1198 + fn apply_resolve_and_kick_redraws(state: &mut RenderState, hits: &HitFrame, input: &InputSnapshot) { 1199 + state.hit_state = resolve(&state.hit_state, hits, input, state.focus.focused()); 1200 + if any_actionable_interaction(&state.hit_state) { 1201 + state.redraw_kicks = state.redraw_kicks.max(REDRAW_KICKS_AFTER_INTERACTION); 1202 + } 1203 + } 1204 + 1205 + fn any_actionable_interaction(hit_state: &HitState) -> bool { 1206 + use bone_ui::hit_test::InteractionState; 1207 + hit_state.interactions.values().any(|i| { 1208 + i.state.contains(InteractionState::CLICK) 1209 + || i.state.contains(InteractionState::DOUBLE_CLICK) 1210 + || i.state.contains(InteractionState::DRAG_START) 1211 + || i.state.contains(InteractionState::DRAG_RELEASE) 1212 + }) 1079 1213 } 1080 1214 1081 1215 struct ChromeLayer { ··· 1108 1242 } 1109 1243 } 1110 1244 1245 + fn suppress_pointer_activations(frame: shell::ShellFrame) -> shell::ShellFrame { 1246 + shell::ShellFrame { 1247 + paints: frame.paints, 1248 + overlay_paints: frame.overlay_paints, 1249 + viewport_rect: frame.viewport_rect, 1250 + activated_tool: None, 1251 + activated_relation: None, 1252 + activated_dimension: None, 1253 + plane_picked: None, 1254 + exit_sketch: false, 1255 + menu_action: None, 1256 + } 1257 + } 1258 + 1259 + fn apply_popup_overlays( 1260 + overlay: &mut Vec<bone_ui::widgets::WidgetPaint>, 1261 + dim_outcome: Option<&DimensionEditorOutcome>, 1262 + conflict_outcome: Option<&DimConflictOutcome>, 1263 + ) -> Option<LayoutRect> { 1264 + let dim_closing = matches!( 1265 + dim_outcome.map(|o| &o.action), 1266 + Some(DimensionEditorAction::Commit(_) | DimensionEditorAction::Cancel), 1267 + ); 1268 + extend_when_open( 1269 + overlay, 1270 + dim_outcome.map(|o| o.paints.as_slice()), 1271 + dim_closing, 1272 + ); 1273 + let conflict_closing = matches!( 1274 + conflict_outcome.map(|o| o.action), 1275 + Some(DimConflictAction::MakeDriven | DimConflictAction::Cancel), 1276 + ); 1277 + extend_when_open( 1278 + overlay, 1279 + conflict_outcome.map(|o| o.paints.as_slice()), 1280 + conflict_closing, 1281 + ); 1282 + if dim_closing { 1283 + None 1284 + } else { 1285 + dim_outcome.map(|o| o.bounds) 1286 + } 1287 + } 1288 + 1289 + fn extend_when_open( 1290 + overlay: &mut Vec<bone_ui::widgets::WidgetPaint>, 1291 + paints: Option<&[bone_ui::widgets::WidgetPaint]>, 1292 + closing: bool, 1293 + ) { 1294 + if let Some(p) = paints 1295 + && !closing 1296 + { 1297 + overlay.extend(p.iter().cloned()); 1298 + } 1299 + } 1300 + 1301 + fn run_frame_ui( 1302 + state: &mut RenderState, 1303 + theme: Arc<Theme>, 1304 + input: &mut InputSnapshot, 1305 + hits: &mut HitFrame, 1306 + a11y: &mut AccessTreeBuilder, 1307 + scopes: &HotkeyScopes, 1308 + layout_size: LayoutSize, 1309 + ) -> ( 1310 + shell::ShellFrame, 1311 + Vec<ActionId>, 1312 + Option<DimensionEditorOutcome>, 1313 + Option<DimConflictOutcome>, 1314 + ) { 1315 + let mut ctx = FrameCtx::new( 1316 + theme, 1317 + input, 1318 + &mut state.focus, 1319 + &state.hotkeys, 1320 + &state.strings, 1321 + hits, 1322 + &state.hit_state, 1323 + a11y, 1324 + &mut state.chrome_shaper, 1325 + ); 1326 + let frame = state.shell.render( 1327 + &mut ctx, 1328 + &state.document, 1329 + &state.mode, 1330 + state.selection.ids(), 1331 + layout_size, 1332 + ); 1333 + let dim_outcome = pending_dim(&state.mode).map(|pending| { 1334 + let live_anchor = match state.mode { 1335 + Mode::Sketch { sketch_id, .. } => state 1336 + .document 1337 + .sketch(sketch_id) 1338 + .and_then(|s| smart_dimension::live_anchor(s, pending.proto)) 1339 + .unwrap_or(pending.anchor), 1340 + Mode::Idle => pending.anchor, 1341 + }; 1342 + dimension_editor::render( 1343 + &mut ctx, 1344 + pending, 1345 + live_anchor, 1346 + &state.camera, 1347 + frame.viewport_rect, 1348 + &mut state.dim_editor, 1349 + ) 1350 + }); 1351 + let conflict_outcome = 1352 + dim_conflict_pending(&state.mode).map(|_| render_dim_conflict_modal(&mut ctx, layout_size)); 1353 + let actions = if conflict_outcome.is_some() || dim_outcome.is_some() { 1354 + Vec::new() 1355 + } else { 1356 + ctx.dispatch_hotkeys(scopes) 1357 + }; 1358 + (frame, actions, dim_outcome, conflict_outcome) 1359 + } 1360 + 1361 + fn dim_conflict_pending(mode: &Mode) -> Option<PendingDimension> { 1362 + match mode { 1363 + Mode::Sketch { session, .. } => match session.dim_flow { 1364 + Some(DimensionFlow::Conflict(p)) => Some(p), 1365 + Some(DimensionFlow::Editing(_)) | None => None, 1366 + }, 1367 + Mode::Idle => None, 1368 + } 1369 + } 1370 + 1371 + #[derive(Clone, Debug, PartialEq)] 1372 + struct DimConflictOutcome { 1373 + paints: Vec<bone_ui::widgets::WidgetPaint>, 1374 + action: DimConflictAction, 1375 + } 1376 + 1377 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 1378 + enum DimConflictAction { 1379 + Idle, 1380 + MakeDriven, 1381 + Cancel, 1382 + } 1383 + 1384 + fn render_dim_conflict_modal( 1385 + ctx: &mut FrameCtx<'_>, 1386 + layout_size: LayoutSize, 1387 + ) -> DimConflictOutcome { 1388 + use bone_ui::widgets::{ConfirmationDialog, ConfirmationOutcome, show_confirmation}; 1389 + use bone_ui::{WidgetId, WidgetKey}; 1390 + let viewport = LayoutRect::new(LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), layout_size); 1391 + let dialog_size = LayoutSize::new(LayoutPx::new(440.0), LayoutPx::new(180.0)); 1392 + let id = WidgetId::ROOT.child(WidgetKey::new("dim.conflict")); 1393 + let response = show_confirmation( 1394 + ctx, 1395 + ConfirmationDialog { 1396 + id, 1397 + viewport, 1398 + size: dialog_size, 1399 + title: strings::DIM_CONFLICT_TITLE, 1400 + message: strings::DIM_CONFLICT_MESSAGE, 1401 + confirm_label: strings::DIM_CONFLICT_MAKE_DRIVEN, 1402 + cancel_label: strings::DIM_CONFLICT_CANCEL, 1403 + destructive: false, 1404 + }, 1405 + ); 1406 + let action = match response.outcome { 1407 + Some(ConfirmationOutcome::Confirm) => DimConflictAction::MakeDriven, 1408 + Some(ConfirmationOutcome::Cancel) => DimConflictAction::Cancel, 1409 + None => DimConflictAction::Idle, 1410 + }; 1411 + DimConflictOutcome { 1412 + paints: response.paint, 1413 + action, 1414 + } 1415 + } 1416 + 1417 + fn pending_dim(mode: &Mode) -> Option<PendingDimension> { 1418 + match mode { 1419 + Mode::Sketch { session, .. } => match session.dim_flow { 1420 + Some(DimensionFlow::Editing(p)) => Some(p), 1421 + Some(DimensionFlow::Conflict(_)) | None => None, 1422 + }, 1423 + Mode::Idle => None, 1424 + } 1425 + } 1426 + 1427 + fn apply_dimension_request(state: &mut RenderState, request: Option<PendingDimension>) { 1428 + let Some(request) = request else { return }; 1429 + let Mode::Sketch { .. } = state.mode else { 1430 + return; 1431 + }; 1432 + state.mode = core::mem::take(&mut state.mode).start_dimension(request); 1433 + state.selection = Selection::default(); 1434 + } 1435 + 1436 + fn apply_dimension_outcome(state: &mut RenderState, outcome: Option<DimensionEditorOutcome>) { 1437 + let Some(outcome) = outcome else { return }; 1438 + let Some(pending) = pending_dim(&state.mode) else { 1439 + return; 1440 + }; 1441 + match outcome.action { 1442 + DimensionEditorAction::Idle => {} 1443 + DimensionEditorAction::Cancel => { 1444 + state.mode = core::mem::take(&mut state.mode).cancel_dimension(); 1445 + state.dim_editor.close(); 1446 + } 1447 + DimensionEditorAction::Swap(next_proto) => { 1448 + state.mode = core::mem::take(&mut state.mode).start_dimension(PendingDimension { 1449 + proto: next_proto, 1450 + anchor: pending.anchor, 1451 + }); 1452 + } 1453 + DimensionEditorAction::Commit(value) => { 1454 + commit_pending_dimension(state, pending, value); 1455 + } 1456 + } 1457 + } 1458 + 1459 + fn apply_dim_conflict_outcome(state: &mut RenderState, outcome: Option<DimConflictOutcome>) { 1460 + let Some(outcome) = outcome else { return }; 1461 + let Some(pending) = dim_conflict_pending(&state.mode) else { 1462 + return; 1463 + }; 1464 + match outcome.action { 1465 + DimConflictAction::Idle => {} 1466 + DimConflictAction::Cancel => { 1467 + state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict(); 1468 + } 1469 + DimConflictAction::MakeDriven => { 1470 + confirm_dim_conflict_make_driven(state, pending); 1471 + } 1472 + } 1473 + } 1474 + 1475 + fn commit_pending_dimension( 1476 + state: &mut RenderState, 1477 + pending: PendingDimension, 1478 + value: DimensionValue, 1479 + ) { 1480 + let Mode::Sketch { sketch_id, .. } = state.mode else { 1481 + return; 1482 + }; 1483 + let Some(sketch) = state.document.sketch(sketch_id).cloned() else { 1484 + return; 1485 + }; 1486 + let proto = match pending.proto.with_value(value) { 1487 + Ok(p) => p, 1488 + Err(e) => { 1489 + tracing::warn!(error = %e, ?pending.proto, ?value, "dimension value type mismatch"); 1490 + return; 1491 + } 1492 + }; 1493 + let after_add = match sketch.clone().apply(SketchEdit::AddDimension(proto)) { 1494 + Ok((next, _)) => next, 1495 + Err(e) => { 1496 + tracing::warn!(error = %e, ?proto, "add dimension failed"); 1497 + return; 1498 + } 1499 + }; 1500 + let solved = match after_add.solve() { 1501 + Ok(s) => s, 1502 + Err(SolverError::OverDefined { .. }) => { 1503 + state.mode = core::mem::take(&mut state.mode).start_dim_conflict(PendingDimension { 1504 + proto, 1505 + anchor: pending.anchor, 1506 + }); 1507 + state.dim_editor.close(); 1508 + return; 1509 + } 1510 + Err(e) => { 1511 + tracing::warn!(error = %e, "solve after add dim did not converge; rejecting"); 1512 + return; 1513 + } 1514 + }; 1515 + state.undo.record(state.document.clone()); 1516 + state.document.replace_sketch(sketch_id, solved); 1517 + state.mode = core::mem::take(&mut state.mode).cancel_dimension(); 1518 + state.dim_editor.close(); 1519 + refresh_active_scene(state); 1520 + } 1521 + 1522 + fn confirm_dim_conflict_make_driven(state: &mut RenderState, pending: PendingDimension) { 1523 + let Mode::Sketch { sketch_id, .. } = state.mode else { 1524 + return; 1525 + }; 1526 + let Some(sketch) = state.document.sketch(sketch_id).cloned() else { 1527 + return; 1528 + }; 1529 + let Some(measured) = sketch.measure(pending.proto) else { 1530 + tracing::warn!(?pending.proto, "measure failed for driven conversion; aborting"); 1531 + state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict(); 1532 + return; 1533 + }; 1534 + let Some(driven_proto) = driven_with_value(pending.proto, measured) else { 1535 + tracing::warn!(?pending.proto, "driven conversion failed"); 1536 + state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict(); 1537 + return; 1538 + }; 1539 + let after_add = match sketch.apply(SketchEdit::AddDimension(driven_proto)) { 1540 + Ok((next, _)) => next, 1541 + Err(e) => { 1542 + tracing::warn!(error = %e, ?driven_proto, "add driven dimension failed"); 1543 + state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict(); 1544 + return; 1545 + } 1546 + }; 1547 + let solved = after_add.clone().solve().unwrap_or(after_add); 1548 + state.undo.record(state.document.clone()); 1549 + state.document.replace_sketch(sketch_id, solved); 1550 + state.mode = core::mem::take(&mut state.mode).cancel_dim_conflict(); 1551 + refresh_active_scene(state); 1552 + } 1553 + 1554 + fn driven_with_value(proto: SketchDimension, value: DimensionValue) -> Option<SketchDimension> { 1555 + proto 1556 + .with_kind(DimensionKind::Driven) 1557 + .with_value(value) 1558 + .ok() 1559 + } 1560 + 1111 1561 fn apply_relation_action(state: &mut RenderState, relation: Option<SketchRelation>) { 1112 1562 let Some(relation) = relation else { return }; 1113 1563 let Mode::Sketch { sketch_id, .. } = state.mode else { ··· 1183 1633 .try_for_each(|(mode, theme)| -> Result<(), AppError> { 1184 1634 let mut state = GalleryState::new(); 1185 1635 let paint = render(Arc::new(theme.clone()), &mut state); 1186 - let rgba = rasterize(&theme, &paint, GALLERY_CANVAS); 1636 + let rgba = rasterize(&theme, &paint, GALLERY_CANVAS, StringTable::empty()); 1187 1637 let png = encode_png(&rgba, GALLERY_CANVAS) 1188 1638 .map_err(|source| AppError::GalleryEncode { mode, source })?; 1189 1639 let target = out_dir.join(format!("gallery_{mode}.png")); ··· 1229 1679 } 1230 1680 } 1231 1681 1682 + fn winit_named_to_ui(named: WinitNamed) -> Option<NamedKey> { 1683 + match named { 1684 + WinitNamed::Tab => Some(NamedKey::Tab), 1685 + WinitNamed::Enter => Some(NamedKey::Enter), 1686 + WinitNamed::Escape => Some(NamedKey::Escape), 1687 + WinitNamed::Backspace => Some(NamedKey::Backspace), 1688 + WinitNamed::Delete => Some(NamedKey::Delete), 1689 + WinitNamed::Space => Some(NamedKey::Space), 1690 + WinitNamed::ArrowUp => Some(NamedKey::ArrowUp), 1691 + WinitNamed::ArrowDown => Some(NamedKey::ArrowDown), 1692 + WinitNamed::ArrowLeft => Some(NamedKey::ArrowLeft), 1693 + WinitNamed::ArrowRight => Some(NamedKey::ArrowRight), 1694 + WinitNamed::Home => Some(NamedKey::Home), 1695 + WinitNamed::End => Some(NamedKey::End), 1696 + WinitNamed::PageUp => Some(NamedKey::PageUp), 1697 + WinitNamed::PageDown => Some(NamedKey::PageDown), 1698 + _ => None, 1699 + } 1700 + } 1701 + 1232 1702 fn keycode_to_char(code: KeyCode) -> Option<char> { 1233 1703 match code { 1234 1704 KeyCode::KeyA => Some('a'), ··· 1286 1756 #[cfg(test)] 1287 1757 mod tests { 1288 1758 use super::*; 1759 + use crate::sketch_mode::SketchSession; 1289 1760 1290 1761 #[test] 1291 1762 fn cursor_to_world_at_window_center_equals_camera_pan() { ··· 1330 1801 viewport_rect: empty_rect(), 1331 1802 activated_tool: None, 1332 1803 activated_relation: None, 1804 + activated_dimension: None, 1333 1805 plane_picked: None, 1334 1806 exit_sketch: false, 1335 1807 menu_action: None, ··· 1367 1839 plane_picked: Some(Plane::Xy), 1368 1840 ..empty_frame() 1369 1841 }; 1370 - assert_eq!(next_mode(prev, &frame, &[], &xy_only()), prev); 1842 + assert_eq!(next_mode(prev.clone(), &frame, &[], &xy_only()), prev); 1371 1843 } 1372 1844 1373 1845 #[test] ··· 1398 1870 fn escape_with_pending_clears_pending_keeps_sketch_and_tool() { 1399 1871 let prev = Mode::Sketch { 1400 1872 sketch_id: SketchId::default(), 1401 - session: SketchSession { 1873 + session: Box::new(SketchSession { 1402 1874 tool: Some(SketchTool::Line), 1403 1875 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1404 1876 1.0, 2.0, 1405 1877 )))), 1406 - drag: None, 1407 - }, 1878 + ..SketchSession::default() 1879 + }), 1408 1880 }; 1409 1881 let next = next_mode( 1410 1882 prev, ··· 1459 1931 let anchor = Point2::from_mm(2.0, 3.0); 1460 1932 let mode = Mode::Sketch { 1461 1933 sketch_id, 1462 - session: SketchSession { 1934 + session: Box::new(SketchSession { 1463 1935 tool: Some(SketchTool::Line), 1464 1936 pending: Some(Pending::First(ClickAnchor::Position(anchor))), 1465 - drag: None, 1466 - }, 1937 + ..SketchSession::default() 1938 + }), 1467 1939 }; 1468 1940 let cursor = Point2::from_mm(5.0, 7.0); 1469 1941 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1479 1951 let (document, sketch_id) = initial_document(sketch); 1480 1952 let mode = Mode::Sketch { 1481 1953 sketch_id, 1482 - session: SketchSession { 1954 + session: Box::new(SketchSession { 1483 1955 tool: Some(SketchTool::Line), 1484 1956 pending: Some(Pending::First(ClickAnchor::Endpoint(endpoint))), 1485 - drag: None, 1486 - }, 1957 + ..SketchSession::default() 1958 + }), 1487 1959 }; 1488 1960 let cursor = Point2::from_mm(0.0, 0.0); 1489 1961 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1498 1970 let anchor = Point2::from_mm(1.0, 1.0); 1499 1971 let mode = Mode::Sketch { 1500 1972 sketch_id, 1501 - session: SketchSession { 1973 + session: Box::new(SketchSession { 1502 1974 tool: Some(SketchTool::Line), 1503 1975 pending: Some(Pending::First(ClickAnchor::Position(anchor))), 1504 - drag: None, 1505 - }, 1976 + ..SketchSession::default() 1977 + }), 1506 1978 }; 1507 1979 let preview = build_preview(&mode, &document, None, &far_camera()); 1508 1980 assert_eq!(preview.anchors, vec![anchor]); ··· 1514 1986 let document = Document::new(DocumentId::default(), "doc".to_owned()); 1515 1987 let mode = Mode::Sketch { 1516 1988 sketch_id: SketchId::default(), 1517 - session: SketchSession { 1989 + session: Box::new(SketchSession { 1518 1990 tool: Some(SketchTool::Line), 1519 1991 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1520 1992 0.0, 0.0, 1521 1993 )))), 1522 - drag: Some(bone_types::SketchEntityId::default()), 1523 - }, 1994 + drag: Some(DragSession { 1995 + entity: bone_types::SketchEntityId::default(), 1996 + press: Point2::origin(), 1997 + pins: DragPins::from_array([ 1998 + Some((bone_types::SketchEntityId::default(), Point2::origin())), 1999 + None, 2000 + None, 2001 + ]), 2002 + }), 2003 + ..SketchSession::default() 2004 + }), 1524 2005 }; 1525 2006 let preview = build_preview( 1526 2007 &mode, ··· 1538 2019 let center = Point2::from_mm(0.0, 0.0); 1539 2020 let mode = Mode::Sketch { 1540 2021 sketch_id, 1541 - session: SketchSession { 2022 + session: Box::new(SketchSession { 1542 2023 tool: Some(SketchTool::Circle), 1543 2024 pending: Some(Pending::First(ClickAnchor::Position(center))), 1544 - drag: None, 1545 - }, 2025 + ..SketchSession::default() 2026 + }), 1546 2027 }; 1547 2028 let cursor = Point2::from_mm(3.0, 4.0); 1548 2029 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1559 2040 let corner = Point2::from_mm(0.0, 0.0); 1560 2041 let mode = Mode::Sketch { 1561 2042 sketch_id, 1562 - session: SketchSession { 2043 + session: Box::new(SketchSession { 1563 2044 tool: Some(SketchTool::CornerRectangle), 1564 2045 pending: Some(Pending::First(ClickAnchor::Position(corner))), 1565 - drag: None, 1566 - }, 2046 + ..SketchSession::default() 2047 + }), 1567 2048 }; 1568 2049 let cursor = Point2::from_mm(5.0, 3.0); 1569 2050 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1580 2061 let (document, sketch_id) = initial_document(sketch); 1581 2062 let mode = Mode::Sketch { 1582 2063 sketch_id, 1583 - session: SketchSession { 2064 + session: Box::new(SketchSession { 1584 2065 tool: Some(SketchTool::TangentArc), 1585 2066 pending: Some(Pending::First(ClickAnchor::Endpoint(b))), 1586 - drag: None, 1587 - }, 2067 + ..SketchSession::default() 2068 + }), 1588 2069 }; 1589 2070 let cursor = Point2::from_mm(10.0, 6.0); 1590 2071 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1600 2081 let start = Point2::from_mm(5.0, 0.0); 1601 2082 let mode = Mode::Sketch { 1602 2083 sketch_id, 1603 - session: SketchSession { 2084 + session: Box::new(SketchSession { 1604 2085 tool: Some(SketchTool::CenterpointArc), 1605 2086 pending: Some(Pending::Second( 1606 2087 ClickAnchor::Position(center), 1607 2088 ClickAnchor::Position(start), 1608 2089 )), 1609 - drag: None, 1610 - }, 2090 + ..SketchSession::default() 2091 + }), 1611 2092 }; 1612 2093 let cursor = Point2::from_mm(0.0, 5.0); 1613 2094 let preview = build_preview(&mode, &document, Some(cursor), &far_camera()); ··· 1663 2144 fn ribbon_exit_overrides_pending_chain() { 1664 2145 let prev = Mode::Sketch { 1665 2146 sketch_id: SketchId::default(), 1666 - session: SketchSession { 2147 + session: Box::new(SketchSession { 1667 2148 tool: Some(SketchTool::Line), 1668 2149 pending: Some(Pending::First(ClickAnchor::Position(Point2::from_mm( 1669 2150 0.0, 0.0, 1670 2151 )))), 1671 - drag: None, 1672 - }, 2152 + ..SketchSession::default() 2153 + }), 1673 2154 }; 1674 2155 let frame = shell::ShellFrame { 1675 2156 exit_sketch: true, ··· 1750 2231 ); 1751 2232 let in_idle = scopes_for_mode(&Mode::Idle); 1752 2233 assert_eq!(table.dispatch(chord, &in_idle), None); 2234 + } 2235 + 2236 + #[test] 2237 + fn driven_with_value_promotes_kind_and_overwrites_value() { 2238 + let proto = SketchDimension::Linear { 2239 + a: bone_types::SketchEntityId::default(), 2240 + b: bone_types::SketchEntityId::default(), 2241 + value: Length::new::<millimeter>(8.0), 2242 + kind: DimensionKind::Driving, 2243 + }; 2244 + let measured = DimensionValue::Length(Length::new::<millimeter>(10.0)); 2245 + let Some(driven) = driven_with_value(proto, measured) else { 2246 + panic!("driven_with_value rejected matched-kind value"); 2247 + }; 2248 + assert_eq!(driven.kind(), DimensionKind::Driven); 2249 + let DimensionValue::Length(length) = driven.value() else { 2250 + panic!("expected Length"); 2251 + }; 2252 + assert!((length.get::<millimeter>() - 10.0).abs() < 1e-9); 2253 + } 2254 + 2255 + #[test] 2256 + fn driven_with_value_rejects_kind_mismatch() { 2257 + let proto = SketchDimension::Linear { 2258 + a: bone_types::SketchEntityId::default(), 2259 + b: bone_types::SketchEntityId::default(), 2260 + value: Length::new::<millimeter>(1.0), 2261 + kind: DimensionKind::Driving, 2262 + }; 2263 + let bad = DimensionValue::Angle(bone_types::Angle::new::<uom::si::angle::radian>(1.0)); 2264 + assert!(driven_with_value(proto, bad).is_none()); 2265 + } 2266 + 2267 + #[test] 2268 + fn dim_conflict_pending_returns_proto_when_set() { 2269 + let proto = SketchDimension::Linear { 2270 + a: bone_types::SketchEntityId::default(), 2271 + b: bone_types::SketchEntityId::default(), 2272 + value: Length::new::<millimeter>(2.0), 2273 + kind: DimensionKind::Driving, 2274 + }; 2275 + let pending = PendingDimension { 2276 + proto, 2277 + anchor: Point2::origin(), 2278 + }; 2279 + let mode = Mode::enter_sketch(SketchId::default()).start_dim_conflict(pending); 2280 + assert_eq!(dim_conflict_pending(&mode), Some(pending)); 2281 + } 2282 + 2283 + #[test] 2284 + fn dim_conflict_pending_returns_none_in_idle() { 2285 + assert_eq!(dim_conflict_pending(&Mode::Idle), None); 1753 2286 } 1754 2287 }
+19 -16
crates/bone-render/src/pipelines/chrome.rs
··· 140 140 } 141 141 } 142 142 143 - pub fn draw( 144 - &mut self, 145 - encoder: &mut wgpu::CommandEncoder, 146 - color_view: &wgpu::TextureView, 147 - viewport_px: [f32; 2], 148 - instances: &[ChromeInstance], 149 - ) { 150 - if instances.is_empty() { 151 - return; 152 - } 143 + pub fn upload(&mut self, viewport_px: [f32; 2], instances: &[ChromeInstance]) { 153 144 let frame = ChromeFrame { 154 145 viewport_px, 155 146 pad: [0.0, 0.0], 156 147 }; 157 148 self.queue 158 149 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&frame)); 150 + if instances.is_empty() { 151 + return; 152 + } 159 153 let needed = instances.len() as u64; 160 154 if needed > self.instance_capacity { 161 155 let new_cap = needed.next_power_of_two().max(self.instance_capacity * 2); ··· 164 158 } 165 159 self.queue 166 160 .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances)); 161 + } 162 + 163 + pub fn draw_range( 164 + &self, 165 + encoder: &mut wgpu::CommandEncoder, 166 + color_view: &wgpu::TextureView, 167 + range: core::ops::Range<u32>, 168 + ) { 169 + if range.start >= range.end { 170 + return; 171 + } 167 172 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 168 173 label: Some("bone-render:chrome-pass"), 169 174 color_attachments: &[Some(wgpu::RenderPassColorAttachment { ··· 182 187 }); 183 188 pass.set_pipeline(&self.pipeline); 184 189 pass.set_bind_group(0, &self.bind_group, &[]); 185 - let used_bytes = needed * INSTANCE_STRIDE; 186 - pass.set_vertex_buffer(0, self.instance_buffer.slice(0..used_bytes)); 187 - let Ok(count) = u32::try_from(needed) else { 188 - panic!("chrome instance count {needed} exceeds u32::MAX"); 189 - }; 190 - pass.draw(0..6, 0..count); 190 + let start_bytes = u64::from(range.start) * INSTANCE_STRIDE; 191 + let end_bytes = u64::from(range.end) * INSTANCE_STRIDE; 192 + pass.set_vertex_buffer(0, self.instance_buffer.slice(start_bytes..end_bytes)); 193 + pass.draw(0..6, 0..(range.end - range.start)); 191 194 } 192 195 } 193 196
+19 -12
crates/bone-render/src/pipelines/chrome_text.rs
··· 101 101 } 102 102 } 103 103 104 - pub fn draw( 104 + pub fn upload( 105 105 &mut self, 106 - encoder: &mut wgpu::CommandEncoder, 107 - color_view: &wgpu::TextureView, 108 106 viewport_px: [f32; 2], 109 107 atlas_pixels: &[u8], 110 108 atlas_version: u64, 111 109 instances: &[SdfGlyphInstance], 112 110 ) { 113 - if instances.is_empty() { 114 - return; 115 - } 116 111 if self.atlas_version != Some(atlas_version) { 117 112 self.upload_atlas(atlas_pixels); 118 113 self.atlas_version = Some(atlas_version); ··· 123 118 }; 124 119 self.queue 125 120 .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&frame)); 121 + if instances.is_empty() { 122 + return; 123 + } 126 124 let needed = instances.len() as u64; 127 125 if needed > self.instance_capacity { 128 126 let new_cap = needed.next_power_of_two().max(self.instance_capacity * 2); ··· 131 129 } 132 130 self.queue 133 131 .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances)); 132 + } 133 + 134 + pub fn draw_range( 135 + &self, 136 + encoder: &mut wgpu::CommandEncoder, 137 + color_view: &wgpu::TextureView, 138 + range: core::ops::Range<u32>, 139 + ) { 140 + if range.start >= range.end { 141 + return; 142 + } 134 143 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 135 144 label: Some("bone-render:chrome-text-pass"), 136 145 color_attachments: &[Some(wgpu::RenderPassColorAttachment { ··· 149 158 }); 150 159 pass.set_pipeline(&self.pipeline); 151 160 pass.set_bind_group(0, &self.bind_group, &[]); 152 - let used_bytes = needed * INSTANCE_STRIDE; 153 - pass.set_vertex_buffer(0, self.instance_buffer.slice(0..used_bytes)); 154 - let Ok(count) = u32::try_from(needed) else { 155 - panic!("chrome text instance count {needed} exceeds u32::MAX"); 156 - }; 157 - pass.draw(0..6, 0..count); 161 + let start_bytes = u64::from(range.start) * INSTANCE_STRIDE; 162 + let end_bytes = u64::from(range.end) * INSTANCE_STRIDE; 163 + pass.set_vertex_buffer(0, self.instance_buffer.slice(start_bytes..end_bytes)); 164 + pass.draw(0..6, 0..(range.end - range.start)); 158 165 } 159 166 160 167 fn upload_atlas(&self, pixels: &[u8]) {