From 1300e4f300ce2b6744fc2e0fd95fdc09016b92b2 Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 8 Jun 2026 22:58:32 +0300 Subject: [PATCH] feat(app): features ribbon tab arms extrude profile Lewis: May this revision serve well! --- crates/bone-app/src/hotkeys.rs | 42 ++- crates/bone-app/src/main.rs | 233 ++++++++++--- crates/bone-app/src/shell.rs | 314 ++++++++++++++++-- crates/bone-app/src/sketch_mode.rs | 90 ++++- ..._hotkeys__tests__default_hotkey_table.snap | 1 + crates/bone-app/src/strings.rs | 31 ++ crates/bone-ui/src/hotkey.rs | 1 + 7 files changed, 617 insertions(+), 95 deletions(-) diff --git a/crates/bone-app/src/hotkeys.rs b/crates/bone-app/src/hotkeys.rs index 3e2cd0a..c29b526 100644 --- a/crates/bone-app/src/hotkeys.rs +++ b/crates/bone-app/src/hotkeys.rs @@ -8,7 +8,7 @@ use bone_ui::input::{KeyChar, KeyCode, ModifierMask, NamedKey}; use bone_ui::strings::StringKey; use serde::{Deserialize, Serialize}; -use crate::sketch_mode::{EXIT_SKETCH_ACTION, REDO_ACTION, UNDO_ACTION}; +use crate::sketch_mode::{ESCAPE_ACTION, REDO_ACTION, UNDO_ACTION}; use crate::strings as s; const fn action_id(value: u32) -> ActionId { @@ -86,12 +86,19 @@ pub struct Command { pub const COMMANDS: &[Command] = &[ Command { - action: EXIT_SKETCH_ACTION, + action: ESCAPE_ACTION, kind: None, scope: HotkeyScope::Sketch, label: s::HOTKEY_LABEL_ESCAPE, defaults: &[ESC], }, + Command { + action: ESCAPE_ACTION, + kind: None, + scope: HotkeyScope::Extrude, + label: s::HOTKEY_LABEL_ESCAPE, + defaults: &[ESC], + }, Command { action: UNDO_ACTION, kind: Some(HotkeyCommand::Undo), @@ -258,15 +265,20 @@ pub struct RemapEntry { #[must_use] pub fn remap_entries() -> Vec { - COMMANDS - .iter() - .map(|cmd| RemapEntry { - action: cmd.action, - scope: cmd.scope, - label: cmd.label, - default_chord: cmd.defaults.first().copied(), - }) - .collect() + COMMANDS.iter().fold(Vec::new(), |mut acc, cmd| { + if !acc + .iter() + .any(|entry: &RemapEntry| entry.action == cmd.action) + { + acc.push(RemapEntry { + action: cmd.action, + scope: cmd.scope, + label: cmd.label, + default_chord: cmd.defaults.first().copied(), + }); + } + acc + }) } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -303,7 +315,7 @@ pub fn compose_table(overrides: &HotkeyOverrides) -> Result Result<(), HotkeyTableError> { bindings.iter().try_for_each(|inner| { - if inner.scope != HotkeyScope::Sketch { + if !matches!(inner.scope, HotkeyScope::Sketch | HotkeyScope::Extrude) { return Ok(()); } let outer = bindings @@ -324,7 +336,7 @@ fn reject_cross_scope_shadow(bindings: &[HotkeyBinding]) -> Result<(), HotkeyTab #[cfg(test)] mod tests { use super::{ - DELETE_SELECTION_ACTION, ENTER_SKETCH_ACTION, EXIT_SKETCH_ACTION, EXTEND_ACTION, + DELETE_SELECTION_ACTION, ENTER_SKETCH_ACTION, ESCAPE_ACTION, EXTEND_ACTION, HotkeyOverrides, MIRROR_ACTION, NEW_DOCUMENT_ACTION, OPEN_DOCUMENT_ACTION, OPEN_SHORTCUT_BAR_ACTION, QUIT_ACTION, REDO_ACTION, SAVE_DOCUMENT_ACTION, SELECT_ALL_ACTION, SMART_DIMENSION_ACTION, TOGGLE_CONSTRUCTION_ACTION, TRIM_ACTION, @@ -378,7 +390,7 @@ mod tests { panic!("defaults must compose"); }; let chord = KeyChord::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE); - assert_eq!(table.dispatch(chord, &scopes()), Some(EXIT_SKETCH_ACTION)); + assert_eq!(table.dispatch(chord, &scopes()), Some(ESCAPE_ACTION)); } #[test] @@ -397,7 +409,7 @@ mod tests { .map(|e| e.action) .collect::>(); [ - EXIT_SKETCH_ACTION, + ESCAPE_ACTION, UNDO_ACTION, REDO_ACTION, ENTER_SKETCH_ACTION, diff --git a/crates/bone-app/src/main.rs b/crates/bone-app/src/main.rs index d1ead46..69666c6 100644 --- a/crates/bone-app/src/main.rs +++ b/crates/bone-app/src/main.rs @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use bone_document::{ - DimensionKind, DimensionValue, Document, DocumentFolder, EditOutcome, LineData, Sketch, - SketchDimension, SketchEdit, SketchEntity, SketchRelation, SolverError, UndoStack, + DimensionKind, DimensionValue, Document, DocumentFolder, EditOutcome, FeatureNode, LineData, + Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation, SolverError, UndoStack, }; use bone_render::{ Camera2, ChromeInstance, ChromePipeline, ChromeTextPipeline, PickQuery, PickedItem, @@ -62,8 +62,8 @@ mod tools; use dimension_editor::{DimensionEditorAction, DimensionEditorOutcome, DimensionEditorState}; use selection::Selection; use sketch_mode::{ - ClickAnchor, DimensionFlow, DragPins, DragSession, Mode, Pending, PendingDimension, Plane, - SketchTool, + ClickAnchor, DimensionFlow, DragPins, DragSession, ExtrudeArming, FeatureTool, Mode, Pending, + PendingDimension, Plane, SketchTool, }; use snap::{Anchor, SnapHit}; @@ -359,13 +359,6 @@ fn zoom_about(camera: Camera2, cursor: Option>, factor: f6 .with_pan(Vec2::from_mm(new_pan_x, new_pan_y)) } -fn armed_sketch_tool(mode: &Mode) -> Option { - match mode { - Mode::Sketch { session, .. } => session.tool, - Mode::Idle => None, - } -} - fn dragging_in_sketch(mode: &Mode) -> bool { matches!( mode, @@ -550,7 +543,47 @@ fn refresh_active_scene(state: &mut RenderState) { fn active_sketch_id(mode: &Mode, plane_sketches: &BTreeMap) -> Option { match mode { Mode::Sketch { sketch_id, .. } => Some(*sketch_id), - Mode::Idle => plane_sketches.get(&Plane::Xy).copied(), + Mode::Extrude(ExtrudeArming::Profile(id)) => Some(*id), + Mode::Extrude(ExtrudeArming::AwaitingSketch) | Mode::Idle => { + plane_sketches.get(&Plane::Xy).copied() + } + } +} + +enum ProfileChoice { + NoSketch, + Unique(SketchId), + Ambiguous, +} + +fn classify_extrude_profile(document: &Document) -> ProfileChoice { + let mut sketches = document + .feature_tree() + .iter() + .filter_map(|(_, node)| match node { + FeatureNode::Sketch(id) => Some(id), + _ => None, + }); + match (sketches.next(), sketches.next()) { + (None, _) => ProfileChoice::NoSketch, + (Some(id), None) => ProfileChoice::Unique(id), + (Some(_), Some(_)) => ProfileChoice::Ambiguous, + } +} + +fn apply_feature_tool(state: &mut RenderState, tool: Option) { + match tool { + Some(FeatureTool::ExtrudedBossBase) => arm_extruded_boss_base(state), + Some(FeatureTool::ExtrudedCut) => notify_stub(state, strings::TOOL_EXTRUDED_CUT), + None => {} + } +} + +fn arm_extruded_boss_base(state: &mut RenderState) { + match classify_extrude_profile(&state.document) { + ProfileChoice::NoSketch => notify_info(state, strings::NOTIFY_EXTRUDE_NO_SKETCH, None), + ProfileChoice::Unique(id) => state.mode = Mode::Extrude(ExtrudeArming::Profile(id)), + ProfileChoice::Ambiguous => state.mode = Mode::Extrude(ExtrudeArming::AwaitingSketch), } } @@ -722,23 +755,20 @@ fn scopes_for_mode(mode: &Mode) -> HotkeyScopes { if mode.is_sketch() { scopes.push(HotkeyScope::Sketch); } + if mode.is_extrude() { + scopes.push(HotkeyScope::Extrude); + } scopes } fn next_mode( mode: Mode, frame: &shell::ShellFrame, - exit_sketch_requested: bool, + escape_requested: bool, plane_sketches: &BTreeMap, ) -> Mode { - let plane_pick = frame - .plane_picked - .filter(|_| !mode.is_sketch()) - .and_then(|plane| plane_sketches.get(&plane).copied()); - let sketch_pick = frame.sketch_activated.filter(|_| !mode.is_sketch()); - let pick = sketch_pick.or(plane_pick); - let after_pick = pick.map_or(mode, Mode::enter_sketch); - let after_escape = if exit_sketch_requested { + let after_pick = resolve_pick(mode, frame, plane_sketches); + let after_escape = if escape_requested { cancel_pending_or_exit(after_pick) } else { after_pick @@ -754,6 +784,25 @@ fn next_mode( } } +fn resolve_pick( + mode: Mode, + frame: &shell::ShellFrame, + plane_sketches: &BTreeMap, +) -> Mode { + if mode.is_extrude() { + return match frame.sketch_activated { + Some(id) => Mode::Extrude(ExtrudeArming::Profile(id)), + None => mode, + }; + } + let plane_pick = frame + .plane_picked + .filter(|_| !mode.is_sketch()) + .and_then(|plane| plane_sketches.get(&plane).copied()); + let sketch_pick = frame.sketch_activated.filter(|_| !mode.is_sketch()); + sketch_pick.or(plane_pick).map_or(mode, Mode::enter_sketch) +} + fn toggle_or_arm(mode: Mode, tool: SketchTool) -> Mode { let already_active = matches!( &mode, @@ -770,7 +819,7 @@ fn cancel_pending_or_exit(mode: Mode) -> Mode { match &mode { Mode::Sketch { session, .. } if session.pending.is_some() => mode.clear_pending(), Mode::Sketch { session, .. } if session.tool.is_some() => mode.disarm_tool(), - Mode::Sketch { .. } | Mode::Idle => Mode::Idle, + Mode::Sketch { .. } | Mode::Idle | Mode::Extrude(_) => Mode::Idle, } } @@ -991,7 +1040,7 @@ impl App { && !self.input.modifiers.shift_key() && let Some(cursor) = self.input.cursor_px { - if armed_sketch_tool(&state.mode).is_some() { + if state.mode.active_tool().is_some() { if let Some(world) = cursor_to_world(state.camera, cursor) { try_place(state, world); } @@ -1205,13 +1254,18 @@ fn render_frame( (false, true) => {} } } - let exit_sketch_requested = hotkey_actions.contains(&sketch_mode::EXIT_SKETCH_ACTION); + let escape_requested = hotkey_actions.contains(&sketch_mode::ESCAPE_ACTION); + let prev_active_sketch = active_sketch_id(&state.mode, &state.plane_sketches); state.mode = next_mode( core::mem::take(&mut state.mode), &frame, - exit_sketch_requested, + escape_requested, &state.plane_sketches, ); + apply_feature_tool(state, frame.activated_feature_tool); + if active_sketch_id(&state.mode, &state.plane_sketches) != prev_active_sketch { + refresh_active_scene(state); + } apply_dimension_outcome(state, dim_outcome); apply_dim_conflict_outcome(state, conflict_outcome); apply_dimension_request(state, frame.activated_dimension); @@ -1922,6 +1976,7 @@ fn suppress_pointer_activations(frame: shell::ShellFrame) -> shell::ShellFrame { overlay_paints: frame.overlay_paints, viewport_rect: frame.viewport_rect, activated_tool: None, + activated_feature_tool: None, activated_relation: None, activated_dimension: None, dimension_edit: None, @@ -2073,14 +2128,12 @@ fn run_frame_ui( cursor_world, ); let dim_outcome = pending_dim(&state.mode).map(|pending| { - let live_anchor = match state.mode { - Mode::Sketch { sketch_id, .. } => state - .document - .sketch(sketch_id) - .and_then(|s| smart_dimension::live_anchor(s, pending.proto)) - .unwrap_or(pending.anchor), - Mode::Idle => pending.anchor, - }; + let live_anchor = state + .mode + .sketch_id() + .and_then(|id| state.document.sketch(id)) + .and_then(|s| smart_dimension::live_anchor(s, pending.proto)) + .unwrap_or(pending.anchor); dimension_editor::render( &mut ctx, pending, @@ -2144,12 +2197,9 @@ fn run_frame_ui( } fn dim_conflict_pending(mode: &Mode) -> Option { - match mode { - Mode::Sketch { session, .. } => match session.dim_flow { - Some(DimensionFlow::Conflict(p)) => Some(p), - Some(DimensionFlow::Editing(_)) | None => None, - }, - Mode::Idle => None, + match mode.dim_flow() { + Some(DimensionFlow::Conflict(p)) => Some(p), + Some(DimensionFlow::Editing(_)) | None => None, } } @@ -2426,12 +2476,9 @@ fn apply_overwrite_outcome(state: &mut RenderState, outcome: Option Option { - match mode { - Mode::Sketch { session, .. } => match session.dim_flow { - Some(DimensionFlow::Editing(p)) => Some(p), - Some(DimensionFlow::Conflict(_)) | None => None, - }, - Mode::Idle => None, + match mode.dim_flow() { + Some(DimensionFlow::Editing(p)) => Some(p), + Some(DimensionFlow::Conflict(_)) | None => None, } } @@ -2926,12 +2973,8 @@ fn notify_info( fn notify_stub(state: &mut RenderState, label: bone_ui::strings::StringKey) { let detail = state.strings.resolve(label).to_owned(); - tracing::info!(label = %detail, "hotkey action stub"); - state.notification = Some(Notification { - kind: NotificationKind::Info, - headline: strings::NOTIFY_COMING_SOON, - detail: Some(detail), - }); + tracing::info!(label = %detail, "stub action invoked"); + notify_info(state, strings::NOTIFY_COMING_SOON, Some(detail)); } fn apply_shortcut_bar_outcome( @@ -3263,6 +3306,7 @@ mod tests { overlay_paints: Vec::new(), viewport_rect: empty_rect(), activated_tool: None, + activated_feature_tool: None, activated_relation: None, activated_dimension: None, dimension_edit: None, @@ -3280,6 +3324,21 @@ mod tests { BTreeMap::from([(Plane::Xy, SketchId::default())]) } + #[test] + fn classify_extrude_profile_separates_no_sketch_from_unique() { + let empty = Document::new(DocumentId::default(), "Untitled".to_owned()); + assert!(matches!( + super::classify_extrude_profile(&empty), + super::ProfileChoice::NoSketch + )); + + let (one, id) = super::initial_document(Sketch::new(Plane::Xy.basis())); + assert!(matches!( + super::classify_extrude_profile(&one), + super::ProfileChoice::Unique(found) if found == id + )); + } + #[test] fn plane_pick_from_idle_enters_sketch_for_known_plane() { let frame = shell::ShellFrame { @@ -3722,6 +3781,34 @@ mod tests { assert_eq!(next, Mode::enter_sketch(sketch_id)); } + #[test] + fn sketch_pick_in_extrude_sets_profile_instead_of_editing() { + let sketch_id = SketchId::default(); + let frame = shell::ShellFrame { + sketch_activated: Some(sketch_id), + ..empty_frame() + }; + assert_eq!( + next_mode( + Mode::Extrude(ExtrudeArming::AwaitingSketch), + &frame, + false, + &xy_only(), + ), + Mode::Extrude(ExtrudeArming::Profile(sketch_id)), + ); + assert_eq!( + next_mode( + Mode::Extrude(ExtrudeArming::Profile(sketch_id)), + &frame, + false, + &xy_only(), + ), + Mode::Extrude(ExtrudeArming::Profile(sketch_id)), + "a pick while armed re-targets the profile, never drops into sketch editing", + ); + } + #[test] fn sketch_activated_while_in_sketch_is_ignored() { let prev = Mode::enter_sketch(SketchId::default()); @@ -3765,12 +3852,54 @@ mod tests { let in_sketch = scopes_for_mode(&Mode::enter_sketch(SketchId::default())); assert_eq!( table.dispatch(chord, &in_sketch), - Some(sketch_mode::EXIT_SKETCH_ACTION) + Some(sketch_mode::ESCAPE_ACTION) ); let in_idle = scopes_for_mode(&Mode::Idle); assert_eq!(table.dispatch(chord, &in_idle), None); } + #[test] + fn extrude_scope_isolates_escape_from_sketch_tools() { + let extrude = scopes_for_mode(&Mode::Extrude(ExtrudeArming::AwaitingSketch)); + let in_extrude: Vec<_> = extrude.innermost_first().copied().collect(); + assert!(in_extrude.contains(&HotkeyScope::Extrude)); + assert!( + !in_extrude.contains(&HotkeyScope::Sketch), + "extrude must not activate the sketch-tool scope", + ); + let sketch = scopes_for_mode(&Mode::enter_sketch(SketchId::default())); + let in_sketch: Vec<_> = sketch.innermost_first().copied().collect(); + assert!(!in_sketch.contains(&HotkeyScope::Extrude)); + } + + #[test] + fn hotkey_table_binds_escape_under_extrude_scope() { + let table = build_hotkey_table(); + let chord = KeyChord::new(UiKeyCode::Named(NamedKey::Escape), ModifierMask::NONE); + let in_extrude = scopes_for_mode(&Mode::Extrude(ExtrudeArming::AwaitingSketch)); + assert_eq!( + table.dispatch(chord, &in_extrude), + Some(sketch_mode::ESCAPE_ACTION) + ); + } + + #[test] + fn escape_exits_extrude_to_idle() { + let frame = empty_frame(); + let awaiting = Mode::Extrude(ExtrudeArming::AwaitingSketch); + assert_eq!(next_mode(awaiting, &frame, true, &xy_only()), Mode::Idle); + let profiled = Mode::Extrude(ExtrudeArming::Profile(SketchId::default())); + assert_eq!(next_mode(profiled, &frame, true, &xy_only()), Mode::Idle); + } + + #[test] + fn cancel_pending_or_exit_drops_extrude_arming() { + assert_eq!( + cancel_pending_or_exit(Mode::Extrude(ExtrudeArming::AwaitingSketch)), + Mode::Idle + ); + } + #[test] fn driven_with_value_promotes_kind_and_overwrites_value() { let proto = SketchDimension::Linear { diff --git a/crates/bone-app/src/shell.rs b/crates/bone-app/src/shell.rs index f6df013..3dd51ca 100644 --- a/crates/bone-app/src/shell.rs +++ b/crates/bone-app/src/shell.rs @@ -36,7 +36,7 @@ use crate::relation_tools::{Eligibility, RelationKind, eligibility}; use crate::selection::Selection; use crate::settings::Settings; use crate::sketch_mode::PendingDimension; -use crate::sketch_mode::{Mode, Plane, SketchTool}; +use crate::sketch_mode::{ExtrudeArming, FeatureTool, Mode, Plane, SketchTool}; use crate::smart_dimension; use crate::status_badge::{ render_status_panel, status_badge_widget_id, status_color, status_label_key, @@ -240,6 +240,7 @@ pub struct Shell { retained_layout: RetainedLayout, dock_state: Arc, tool_index: BTreeMap, + feature_tool_index: BTreeMap, relation_index: BTreeMap, pub state: ShellState, } @@ -259,10 +260,12 @@ pub struct ShellState { pub hotkey_capture: BTreeMap, pub left_pane: LeftPane, last_left_pane_interesting: bool, + last_mode_was_sketch: bool, pub status_panel_open: bool, pub status_panel: PanelState, status_cache: Option<(SketchVersion, SketchStatusReport)>, pub ribbon_overflow_open: BTreeMap, + pub ribbon_active_tab: Option, } #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -291,6 +294,7 @@ pub struct ShellFrame { pub overlay_paints: Vec, pub viewport_rect: LayoutRect, pub activated_tool: Option, + pub activated_feature_tool: Option, pub activated_relation: Option, pub activated_dimension: Option, pub dimension_edit: Option, @@ -316,6 +320,7 @@ impl ShellFrame { overlay_paints: Vec::new(), viewport_rect: zero_rect(), activated_tool: None, + activated_feature_tool: None, activated_relation: None, activated_dimension: None, dimension_edit: None, @@ -362,6 +367,7 @@ impl Shell { let ids = ShellIds::standard(); let dock_state = Arc::new(DockState::new(build_dock_main(panels))); let tool_index = build_tool_index(ids.ribbon); + let feature_tool_index = build_feature_tool_index(ids.ribbon); let relation_index = build_relation_index(ids.ribbon); let mut state = ShellState::default(); state.feature_tree.expanded.insert(ids.feature_part); @@ -371,6 +377,7 @@ impl Shell { retained_layout: RetainedLayout::default(), dock_state, tool_index, + feature_tool_index, relation_index, state, } @@ -424,6 +431,7 @@ impl Shell { ); let active_sketch = active_sketch(document, mode); let entity_ids = selection.entity_ids(); + sync_ribbon_tab_to_mode(&mut self.state, self.ids.ribbon, mode); let activated_widget = render_ribbon( ctx, RibbonInputs { @@ -437,12 +445,14 @@ impl Shell { &mut paints, &mut popover_paints, &mut self.state.ribbon_overflow_open, + &mut self.state.ribbon_active_tab, + ); + update_left_pane_auto( + &mut self.state, + selection, + mode.active_tool(), + mode.is_extrude(), ); - let active_tool = match mode { - Mode::Sketch { session, .. } => session.tool, - Mode::Idle => None, - }; - update_left_pane_auto(&mut self.state, selection, active_tool); let tab_clicked = render_left_pane_tabs( ctx, tab_strip_rect, @@ -534,6 +544,8 @@ impl Shell { let confirm_action = confirm; let exit_sketch = confirm_action.is_some() || menu_action == Some(MenuAction::ExitSketch); let activated_tool = activated_widget.and_then(|id| self.tool_index.get(&id).copied()); + let activated_feature_tool = + activated_widget.and_then(|id| self.feature_tool_index.get(&id).copied()); let activated_relation = resolve_activated_relation( activated_widget, &self.relation_index, @@ -577,6 +589,7 @@ impl Shell { overlay_paints, viewport_rect, activated_tool, + activated_feature_tool, activated_relation, activated_dimension, dimension_edit, @@ -1031,10 +1044,7 @@ fn partition_overlay( } fn active_sketch<'a>(document: &'a Document, mode: &Mode) -> Option<&'a Sketch> { - match mode { - Mode::Sketch { sketch_id, .. } => document.sketch(*sketch_id), - Mode::Idle => None, - } + mode.sketch_id().and_then(|id| document.sketch(id)) } fn resolve_activated_relation( @@ -1218,7 +1228,7 @@ fn build_menu_entries( items: vec![action_with_accel( ids.menu_sketch_exit, strings::MENU_SKETCH_EXIT, - Some(crate::sketch_mode::EXIT_SKETCH_ACTION), + Some(crate::sketch_mode::ESCAPE_ACTION), )], }); } @@ -1247,12 +1257,29 @@ struct RibbonInputs<'a> { selection: &'a [SketchEntityId], } +fn sketch_tab_id(ribbon: WidgetId) -> WidgetId { + ribbon.child(WidgetKey::new("tab.sketch")) +} + +fn features_tab_id(ribbon: WidgetId) -> WidgetId { + ribbon.child(WidgetKey::new("tab.features")) +} + +fn sync_ribbon_tab_to_mode(state: &mut ShellState, ribbon: WidgetId, mode: &Mode) { + let in_sketch = mode.is_sketch(); + if in_sketch && !state.last_mode_was_sketch { + state.ribbon_active_tab = Some(sketch_tab_id(ribbon)); + } + state.last_mode_was_sketch = in_sketch; +} + fn render_ribbon( ctx: &mut FrameCtx<'_>, inputs: RibbonInputs<'_>, paints: &mut Vec, popover_paints: &mut Vec, overflow_open: &mut BTreeMap, + active_tab: &mut Option, ) -> Option { let RibbonInputs { rect, @@ -1265,10 +1292,7 @@ fn render_ribbon( if rect.size.width.value() <= 0.0 || rect.size.height.value() <= 0.0 { return None; } - let active_tool = match mode { - Mode::Sketch { session, .. } => session.tool, - Mode::Idle => None, - }; + let active_tool = mode.active_tool(); let tools_disabled = !mode.is_sketch(); let label_font_size_px = ctx.theme().typography.caption.size.as_px_f32(); let size_item = |item: ToolbarItem, min_width: LayoutPx| -> ToolbarItem { @@ -1299,7 +1323,9 @@ fn render_ribbon( .into_iter() .map(|item| size_item(item, small_min)) .collect(); - let tab_id = ribbon.child(WidgetKey::new("tab.sketch")); + let feature_items = feature_tool_items(ctx, ribbon, mode, large_min); + let sketch_tab_id = sketch_tab_id(ribbon); + let features_tab_id = features_tab_id(ribbon); let groups = build_sketch_groups( ribbon, entity_items, @@ -1309,20 +1335,29 @@ fn render_ribbon( small_min, overflow_open, ); + let feature_groups = build_feature_groups(ribbon, feature_items, large_min, overflow_open); let placeholder_tab = |key: &'static str, label: StringKey| { RibbonTab::new(ribbon.child(WidgetKey::new(key)), label, Vec::new()).disabled(true) }; let tabs = [ - placeholder_tab("tab.features", strings::RIBBON_TAB_FEATURES), - RibbonTab::new(tab_id, strings::RIBBON_TAB_SKETCH, groups), + RibbonTab::new( + features_tab_id, + strings::RIBBON_TAB_FEATURES, + feature_groups, + ), + RibbonTab::new(sketch_tab_id, strings::RIBBON_TAB_SKETCH, groups), placeholder_tab("tab.surfaces", strings::RIBBON_TAB_SURFACES), placeholder_tab("tab.evaluate", strings::RIBBON_TAB_EVALUATE), ]; + let selected_tab = active_tab.unwrap_or(sketch_tab_id); let pointer_pressed = !ctx.input.buttons_pressed.is_empty(); let response = show_ribbon( ctx, - Ribbon::new(ribbon, rect, strings::RIBBON_LABEL, &tabs, tab_id), + Ribbon::new(ribbon, rect, strings::RIBBON_LABEL, &tabs, selected_tab), ); + if let Some(tab) = response.activated_tab { + *active_tab = Some(tab); + } process_ribbon_response( response, paints, @@ -1380,6 +1415,25 @@ fn build_sketch_groups( ] } +fn build_feature_groups( + ribbon: WidgetId, + feature_items: Vec, + large_min: LayoutPx, + overflow_open: &BTreeMap, +) -> Vec { + let extrude_id = ribbon.child(WidgetKey::new("group.extrude")); + vec![RibbonGroup { + id: extrude_id, + label: strings::RIBBON_GROUP_EXTRUDE, + min_width: group_min_width(large_min, feature_items.len()), + width: group_width_for(&feature_items, large_min), + items: feature_items, + icon_size: RibbonIconSize::Large, + overflow_open: overflow_open.get(&extrude_id).copied().unwrap_or(false), + overflow_label: Some(strings::TOOLBAR_OVERFLOW), + }] +} + fn process_ribbon_response( response: bone_ui::widgets::RibbonResponse, paints: &mut Vec, @@ -1525,10 +1579,7 @@ fn render_property_pane( paints: &mut Vec, ) -> Option { let in_sketch = matches!(state.mode, Mode::Sketch { .. }); - let active_sketch_id = match state.mode { - Mode::Sketch { sketch_id, .. } => Some(*sketch_id), - Mode::Idle => None, - }; + let active_sketch_id = state.mode.sketch_id(); let resolved = state .sketch .filter(|_| in_sketch) @@ -1539,6 +1590,15 @@ fn render_property_pane( if rect.size.width.value() <= 0.0 || rect.size.height.value() <= 0.0 { return None; } + if let Mode::Extrude(arming) = state.mode { + let prompt = match arming { + ExtrudeArming::Profile(_) => strings::EXTRUDE_PROFILE_SELECTED, + ExtrudeArming::AwaitingSketch => strings::EXTRUDE_PROMPT_SELECT_SKETCH, + }; + let mut editors = vec![row_editor(prompt, "")]; + render_static_rows(ctx, rect, id, clipboard, &mut editors, paints); + return None; + } match resolved { Some((sketch, SelectionTarget::Entity(entity))) => { let mut editors = entity_editors(ctx.strings, entity, sketch); @@ -2130,6 +2190,7 @@ fn render_status_bar( fn mode_status_label(strings_table: &StringTable, mode: &Mode, document: &Document) -> LabelText { match mode { Mode::Idle => LabelText::Key(strings::STATUS_READY), + Mode::Extrude(_) => LabelText::Key(strings::STATUS_EXTRUDE_ACTIVE), Mode::Sketch { sketch_id, .. } => { let Some(label) = document.sketch_label(*sketch_id) else { tracing::warn!(?sketch_id, "active sketch missing from document"); @@ -2264,6 +2325,61 @@ fn tool_label(tool: SketchTool) -> StringKey { } } +fn build_feature_tool_index(ribbon: WidgetId) -> BTreeMap { + FeatureTool::ALL + .iter() + .copied() + .map(|t| (feature_tool_widget_id(ribbon, t), t)) + .collect() +} + +fn feature_tool_widget_id(ribbon: WidgetId, tool: FeatureTool) -> WidgetId { + ribbon.child(WidgetKey::new(feature_tool_key(tool))) +} + +fn feature_tool_key(tool: FeatureTool) -> &'static str { + match tool { + FeatureTool::ExtrudedBossBase => "tool.extruded_boss_base", + FeatureTool::ExtrudedCut => "tool.extruded_cut", + } +} + +fn feature_tool_label(tool: FeatureTool) -> StringKey { + match tool { + FeatureTool::ExtrudedBossBase => strings::TOOL_EXTRUDED_BOSS_BASE, + FeatureTool::ExtrudedCut => strings::TOOL_EXTRUDED_CUT, + } +} + +fn feature_tool_items( + ctx: &FrameCtx<'_>, + ribbon: WidgetId, + mode: &Mode, + large_min: LayoutPx, +) -> Vec { + let active = match mode { + Mode::Extrude(_) => Some(FeatureTool::ExtrudedBossBase), + Mode::Idle | Mode::Sketch { .. } => None, + }; + let font = ctx.theme().typography.caption.size.as_px_f32(); + FeatureTool::ALL + .iter() + .copied() + .map(|t| { + let base = ToolbarItem::new(feature_tool_widget_id(ribbon, t), feature_tool_label(t)) + .active(active == Some(t)); + let item = if mode.is_sketch() { + base.disabled(true) + .with_tooltip(strings::FEATURE_HINT_EXIT_SKETCH) + } else { + base + }; + let width = estimate_label_width(ctx.strings.resolve(item.label), font, large_min); + item.with_width(width) + }) + .collect() +} + fn paint_walk( layout: &SolvedLayout, node: &SolvedNode, @@ -2477,8 +2593,9 @@ fn update_left_pane_auto( state: &mut ShellState, selection: &Selection, active_tool: Option, + armed_extrude: bool, ) { - let interesting = !selection.is_empty() || active_tool.is_some(); + let interesting = !selection.is_empty() || active_tool.is_some() || armed_extrude; if interesting && !state.last_left_pane_interesting { state.left_pane = LeftPane::Properties; } else if !interesting && state.last_left_pane_interesting { @@ -3021,6 +3138,155 @@ mod tests { }); } + #[test] + fn feature_tool_index_round_trips_both_extrude_buttons() { + let ids = ShellIds::standard(); + let index = build_feature_tool_index(ids.ribbon); + FeatureTool::ALL.iter().copied().for_each(|t| { + let id = feature_tool_widget_id(ids.ribbon, t); + assert_eq!(index.get(&id).copied(), Some(t)); + }); + } + + #[test] + fn features_tab_renders_extrude_group_only_when_selected() { + let document = sample_document(); + let size = layout_size(1280.0, 800.0); + let sketch_view = render_with(Theme::light(), size, &document, &Mode::Idle); + assert!( + label_rect(&sketch_view.paints, strings::RIBBON_GROUP_EXTRUDE).is_none(), + "extrude group hidden while the sketch tab is active", + ); + assert!( + label_rect(&sketch_view.paints, strings::RIBBON_GROUP_ENTITIES).is_some(), + "sketch tab shows its entity group", + ); + + let mut shell = Shell::new(); + shell.state.ribbon_active_tab = Some(features_tab_id(shell.ids.ribbon)); + let features_view = render_into_shell( + &mut shell, + Theme::light(), + size, + &document, + &Mode::Idle, + &Selection::default(), + ); + assert!( + label_rect(&features_view.paints, strings::RIBBON_GROUP_EXTRUDE).is_some(), + "selecting the features tab reveals the extrude group", + ); + } + + #[test] + fn awaiting_sketch_extrude_prompts_in_property_pane() { + let frame = render_with( + Theme::light(), + layout_size(1280.0, 800.0), + &sample_document(), + &Mode::Extrude(ExtrudeArming::AwaitingSketch), + ); + assert!( + label_rect(&frame.paints, strings::EXTRUDE_PROMPT_SELECT_SKETCH).is_some(), + "awaiting-sketch arm prompts via the property pane", + ); + } + + #[test] + fn feature_tools_disabled_while_sketching() { + let theme = Arc::new(Theme::light()); + let table = HotkeyTable::new(); + let mut focus = FocusManager::new(); + let mut hits = HitFrame::new(); + let prev = HitState::new(); + let mut input = InputSnapshot::idle(FrameInstant::ZERO); + let mut shaper = bone_text::Shaper::new(); + let mut a11y = AccessTreeBuilder::new(); + let ctx = FrameCtx::new( + theme, + &mut input, + &mut focus, + &table, + StringTable::empty(), + &mut hits, + &prev, + &mut a11y, + &mut shaper, + ); + let ribbon = ShellIds::standard().ribbon; + let large = RibbonIconSize::Large.item_px(); + + let sketching = feature_tool_items( + &ctx, + ribbon, + &Mode::enter_sketch(SketchId::default()), + large, + ); + assert!( + sketching.iter().all(|item| item.disabled), + "feature tools disable mid-sketch so a ribbon click can't drop the session", + ); + assert!( + sketching + .iter() + .all(|item| item.tooltip == Some(strings::FEATURE_HINT_EXIT_SKETCH)), + "disabled feature tools explain why through a tooltip", + ); + + let idle = feature_tool_items(&ctx, ribbon, &Mode::Idle, large); + assert!( + idle.iter().all(|item| !item.disabled), + "feature tools stay live from idle", + ); + assert!( + idle.iter().all(|item| item.tooltip.is_none()), + "live feature tools carry no disabled-reason tooltip", + ); + } + + #[test] + fn entering_sketch_snaps_ribbon_to_sketch_tab() { + let ids = ShellIds::standard(); + let mut state = ShellState { + ribbon_active_tab: Some(features_tab_id(ids.ribbon)), + ..ShellState::default() + }; + sync_ribbon_tab_to_mode( + &mut state, + ids.ribbon, + &Mode::enter_sketch(SketchId::default()), + ); + assert_eq!(state.ribbon_active_tab, Some(sketch_tab_id(ids.ribbon))); + } + + #[test] + fn ribbon_tab_sync_only_fires_on_the_sketch_rising_edge() { + let ids = ShellIds::standard(); + let features = Some(features_tab_id(ids.ribbon)); + + let mut already_sketching = ShellState { + last_mode_was_sketch: true, + ribbon_active_tab: features, + ..ShellState::default() + }; + sync_ribbon_tab_to_mode( + &mut already_sketching, + ids.ribbon, + &Mode::enter_sketch(SketchId::default()), + ); + assert_eq!( + already_sketching.ribbon_active_tab, features, + "no rising edge, the user's tab choice stands", + ); + + let mut idle = ShellState { + ribbon_active_tab: features, + ..ShellState::default() + }; + sync_ribbon_tab_to_mode(&mut idle, ids.ribbon, &Mode::Idle); + assert_eq!(idle.ribbon_active_tab, features, "idle never forces a tab"); + } + #[test] fn tool_index_omits_smart_dimension() { let ids = ShellIds::standard(); diff --git a/crates/bone-app/src/sketch_mode.rs b/crates/bone-app/src/sketch_mode.rs index 82b7ab5..4c5e2fd 100644 --- a/crates/bone-app/src/sketch_mode.rs +++ b/crates/bone-app/src/sketch_mode.rs @@ -37,6 +37,22 @@ impl SketchTool { ]; } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FeatureTool { + ExtrudedBossBase, + ExtrudedCut, +} + +impl FeatureTool { + pub const ALL: &'static [Self] = &[Self::ExtrudedBossBase, Self::ExtrudedCut]; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ExtrudeArming { + Profile(SketchId), + AwaitingSketch, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Plane { Xy, @@ -66,7 +82,7 @@ const fn action_id(value: u32) -> ActionId { ActionId::new(nz) } -pub const EXIT_SKETCH_ACTION: ActionId = action_id(1); +pub const ESCAPE_ACTION: ActionId = action_id(1); pub const UNDO_ACTION: ActionId = action_id(2); pub const REDO_ACTION: ActionId = action_id(3); @@ -170,6 +186,7 @@ pub enum Mode { sketch_id: SketchId, session: Box, }, + Extrude(ExtrudeArming), } impl Mode { @@ -188,7 +205,7 @@ impl Mode { sketch_id, session: Box::new(f(*session)), }, - Self::Idle => Self::Idle, + Self::Idle | Self::Extrude(_) => self, } } @@ -267,16 +284,81 @@ impl Mode { pub const fn is_sketch(&self) -> bool { matches!(self, Self::Sketch { .. }) } + + #[must_use] + pub const fn is_extrude(&self) -> bool { + matches!(self, Self::Extrude(_)) + } + + #[must_use] + pub fn sketch_id(&self) -> Option { + match self { + Self::Sketch { sketch_id, .. } => Some(*sketch_id), + Self::Idle | Self::Extrude(_) => None, + } + } + + #[must_use] + pub fn active_tool(&self) -> Option { + match self { + Self::Sketch { session, .. } => session.tool, + Self::Idle | Self::Extrude(_) => None, + } + } + + #[must_use] + pub fn dim_flow(&self) -> Option { + match self { + Self::Sketch { session, .. } => session.dim_flow, + Self::Idle | Self::Extrude(_) => None, + } + } } #[cfg(test)] mod tests { use super::{ - ClickAnchor, DimensionFlow, DragPins, DragSession, Mode, Pending, Plane, SketchSession, - SketchTool, + ClickAnchor, DimensionFlow, DragPins, DragSession, ExtrudeArming, FeatureTool, Mode, + Pending, Plane, SketchSession, SketchTool, }; use bone_types::{Point2, SketchEntityId, SketchId}; + #[test] + fn sketch_mode_projections_read_session() { + let id = SketchId::default(); + let mode = Mode::enter_sketch(id).arm_tool(SketchTool::Line); + assert_eq!(mode.sketch_id(), Some(id)); + assert_eq!(mode.active_tool(), Some(SketchTool::Line)); + assert!(!mode.is_extrude()); + } + + #[test] + fn off_sketch_projections_are_empty() { + [Mode::Idle, Mode::Extrude(ExtrudeArming::AwaitingSketch)] + .into_iter() + .for_each(|mode| { + assert_eq!(mode.sketch_id(), None); + assert_eq!(mode.active_tool(), None); + assert_eq!(mode.dim_flow(), None); + }); + } + + #[test] + fn session_transforms_leave_extrude_untouched() { + let mode = Mode::Extrude(ExtrudeArming::AwaitingSketch); + assert_eq!(mode.clone().arm_tool(SketchTool::Line), mode); + assert_eq!(mode.clone().disarm_tool(), mode); + assert_eq!(mode.clone().clear_pending(), mode); + } + + #[test] + fn feature_tool_all_lists_both_extrude_buttons() { + assert_eq!( + FeatureTool::ALL, + &[FeatureTool::ExtrudedBossBase, FeatureTool::ExtrudedCut], + ); + } + #[test] fn arm_tool_in_sketch_records_kind() { let mode = Mode::enter_sketch(SketchId::default()).arm_tool(SketchTool::Line); diff --git a/crates/bone-app/src/snapshots/bone_app__hotkeys__tests__default_hotkey_table.snap b/crates/bone-app/src/snapshots/bone_app__hotkeys__tests__default_hotkey_table.snap index 24d41ff..5aaca83 100644 --- a/crates/bone-app/src/snapshots/bone_app__hotkeys__tests__default_hotkey_table.snap +++ b/crates/bone-app/src/snapshots/bone_app__hotkeys__tests__default_hotkey_table.snap @@ -2,6 +2,7 @@ source: crates/bone-app/src/hotkeys.rs expression: "rendered.join(\"\\n\")" --- +action=1 chord=Esc scope=Extrude action=1 chord=Esc scope=Sketch action=10 chord=Ctrl+N scope=Global action=11 chord=Ctrl+O scope=Global diff --git a/crates/bone-app/src/strings.rs b/crates/bone-app/src/strings.rs index 75ef71b..6b95402 100644 --- a/crates/bone-app/src/strings.rs +++ b/crates/bone-app/src/strings.rs @@ -10,6 +10,7 @@ pub const RIBBON_TAB_EVALUATE: StringKey = StringKey::new("shell.ribbon.tab.eval pub const RIBBON_GROUP_ENTITIES: StringKey = StringKey::new("shell.ribbon.group.entities"); pub const RIBBON_GROUP_RELATIONS: StringKey = StringKey::new("shell.ribbon.group.relations"); pub const RIBBON_GROUP_DIMENSIONS: StringKey = StringKey::new("shell.ribbon.group.dimensions"); +pub const RIBBON_GROUP_EXTRUDE: StringKey = StringKey::new("shell.ribbon.group.extrude"); pub const CONFIRM_ACCEPT: StringKey = StringKey::new("confirm.accept"); pub const CONFIRM_CANCEL: StringKey = StringKey::new("confirm.cancel"); @@ -28,6 +29,9 @@ pub const TOOL_THREE_POINT_CENTER_RECTANGLE: StringKey = StringKey::new("tool.three_point_center_rectangle"); pub const TOOL_PARALLELOGRAM: StringKey = StringKey::new("tool.parallelogram"); +pub const TOOL_EXTRUDED_BOSS_BASE: StringKey = StringKey::new("tool.extruded_boss_base"); +pub const TOOL_EXTRUDED_CUT: StringKey = StringKey::new("tool.extruded_cut"); + pub const TOOL_COINCIDENT: StringKey = StringKey::new("tool.coincident"); pub const TOOL_HORIZONTAL: StringKey = StringKey::new("tool.horizontal"); pub const TOOL_VERTICAL: StringKey = StringKey::new("tool.vertical"); @@ -88,6 +92,7 @@ pub const DOC_TAB_MODEL: StringKey = StringKey::new("shell.doc_tabs.model"); pub const STATUS_BAR_LABEL: StringKey = StringKey::new("shell.status_bar"); pub const STATUS_READY: StringKey = StringKey::new("status.ready"); pub const STATUS_SKETCH_ACTIVE: StringKey = StringKey::new("status.sketch_active"); +pub const STATUS_EXTRUDE_ACTIVE: StringKey = StringKey::new("status.extrude_active"); pub const STATUS_UNITS_MM: StringKey = StringKey::new("status.units.mm"); pub const STATUS_SKETCH_UNDER_DEFINED: StringKey = StringKey::new("status.sketch.under_defined"); pub const STATUS_SKETCH_FULLY_DEFINED: StringKey = StringKey::new("status.sketch.fully_defined"); @@ -188,6 +193,10 @@ pub const NOTIFY_HOTKEY_CONFLICT: StringKey = StringKey::new("notify.hotkey.conf pub const SHORTCUT_BAR_TITLE: StringKey = StringKey::new("shortcut_bar.title"); pub const PROPERTY_PANE_NO_SELECTION: StringKey = StringKey::new("property.no_selection"); +pub const EXTRUDE_PROMPT_SELECT_SKETCH: StringKey = StringKey::new("extrude.prompt.select_sketch"); +pub const EXTRUDE_PROFILE_SELECTED: StringKey = StringKey::new("extrude.profile.selected"); +pub const FEATURE_HINT_EXIT_SKETCH: StringKey = StringKey::new("feature.hint.exit_sketch"); +pub const NOTIFY_EXTRUDE_NO_SKETCH: StringKey = StringKey::new("notify.extrude.no_sketch"); pub const PROPERTY_ROW_KIND: StringKey = StringKey::new("property.row.kind"); pub const PROPERTY_ROW_X: StringKey = StringKey::new("property.row.x"); pub const PROPERTY_ROW_Y: StringKey = StringKey::new("property.row.y"); @@ -244,6 +253,7 @@ const EN_US: &[(StringKey, &str)] = &[ (RIBBON_GROUP_ENTITIES, "Entities"), (RIBBON_GROUP_RELATIONS, "Relations"), (RIBBON_GROUP_DIMENSIONS, "Dimensions"), + (RIBBON_GROUP_EXTRUDE, "Extrude"), (CONFIRM_ACCEPT, "Accept"), (CONFIRM_CANCEL, "Cancel"), (TOOL_POINT, "Point"), @@ -264,6 +274,8 @@ const EN_US: &[(StringKey, &str)] = &[ "3 Point Center Rectangle", ), (TOOL_PARALLELOGRAM, "Parallelogram"), + (TOOL_EXTRUDED_BOSS_BASE, "Extruded Boss/Base"), + (TOOL_EXTRUDED_CUT, "Extruded Cut"), (TOOL_COINCIDENT, "Coincident"), (TOOL_HORIZONTAL, "Horizontal"), (TOOL_VERTICAL, "Vertical"), @@ -321,6 +333,7 @@ const EN_US: &[(StringKey, &str)] = &[ (STATUS_BAR_LABEL, "Status Bar"), (STATUS_READY, "Ready"), (STATUS_SKETCH_ACTIVE, "Editing"), + (STATUS_EXTRUDE_ACTIVE, "Extrude"), (STATUS_UNITS_MM, "MMGS"), (STATUS_SKETCH_UNDER_DEFINED, "Under Defined"), (STATUS_SKETCH_FULLY_DEFINED, "Fully Defined"), @@ -431,6 +444,10 @@ const EN_US: &[(StringKey, &str)] = &[ ), (SHORTCUT_BAR_TITLE, "Shortcut Bar"), (PROPERTY_PANE_NO_SELECTION, "Nothing selected"), + (EXTRUDE_PROMPT_SELECT_SKETCH, "Select a sketch to extrude"), + (EXTRUDE_PROFILE_SELECTED, "Sketch selected"), + (FEATURE_HINT_EXIT_SKETCH, "Exit the sketch to add a feature"), + (NOTIFY_EXTRUDE_NO_SKETCH, "Create a sketch first"), (PROPERTY_ROW_KIND, "Type"), (PROPERTY_ROW_X, "X"), (PROPERTY_ROW_Y, "Y"), @@ -475,6 +492,7 @@ const AR_XB: &[(StringKey, &str)] = &[ (RIBBON_GROUP_ENTITIES, "[!! Entîtîes !!]"), (RIBBON_GROUP_RELATIONS, "[!! Relâtions !!]"), (RIBBON_GROUP_DIMENSIONS, "[!! Dîmensions !!]"), + (RIBBON_GROUP_EXTRUDE, "[!! Êxtrude !!]"), (CONFIRM_ACCEPT, "[!! Accêpt !!]"), (CONFIRM_CANCEL, "[!! Cancêl !!]"), (TOOL_POINT, "[!! Pôint !!]"), @@ -495,6 +513,8 @@ const AR_XB: &[(StringKey, &str)] = &[ "[!! 3 Pôint Cênter Rectàngle !!]", ), (TOOL_PARALLELOGRAM, "[!! Parâllelôgram !!]"), + (TOOL_EXTRUDED_BOSS_BASE, "[!! Êxtruded Bôss/Base !!]"), + (TOOL_EXTRUDED_CUT, "[!! Êxtruded Cût !!]"), (TOOL_COINCIDENT, "[!! Coincîdent !!]"), (TOOL_HORIZONTAL, "[!! Horîzontal !!]"), (TOOL_VERTICAL, "[!! Vêrtical !!]"), @@ -558,6 +578,7 @@ const AR_XB: &[(StringKey, &str)] = &[ (STATUS_BAR_LABEL, "[!! Statûs Bar !!]"), (STATUS_READY, "[!! Réady !!]"), (STATUS_SKETCH_ACTIVE, "[!! Edîting !!]"), + (STATUS_EXTRUDE_ACTIVE, "[!! Êxtrude !!]"), (STATUS_UNITS_MM, "[!! MMGS !!]"), (STATUS_SKETCH_UNDER_DEFINED, "[!! Ûnder Defîned !!]"), (STATUS_SKETCH_FULLY_DEFINED, "[!! Fûlly Defîned !!]"), @@ -677,6 +698,16 @@ const AR_XB: &[(StringKey, &str)] = &[ ), (SHORTCUT_BAR_TITLE, "[!! Shortcût Bâr !!]"), (PROPERTY_PANE_NO_SELECTION, "[!! Nôthing sêlected !!]"), + ( + EXTRUDE_PROMPT_SELECT_SKETCH, + "[!! Sêlect a skêtch to êxtrude !!]", + ), + (EXTRUDE_PROFILE_SELECTED, "[!! Skêtch sêlected !!]"), + ( + FEATURE_HINT_EXIT_SKETCH, + "[!! Êxit the skêtch to add a fêature !!]", + ), + (NOTIFY_EXTRUDE_NO_SKETCH, "[!! Crêate a skêtch fîrst !!]"), (PROPERTY_ROW_KIND, "[!! Týpe !!]"), (PROPERTY_ROW_X, "X"), (PROPERTY_ROW_Y, "Y"), diff --git a/crates/bone-ui/src/hotkey.rs b/crates/bone-ui/src/hotkey.rs index 93c4675..810bf94 100644 --- a/crates/bone-ui/src/hotkey.rs +++ b/crates/bone-ui/src/hotkey.rs @@ -41,6 +41,7 @@ pub enum HotkeyScope { Viewport, FeatureTree, Sketch, + Extrude, Modal, TextInput, } -- 2.51.2