From dce875c4fc00a0d4060f4a7f8a6c46d156bb6220 Mon Sep 17 00:00:00 2001 From: Lewis Date: Sun, 5 Jul 2026 17:15:35 +0300 Subject: [PATCH] sweep tool ui shell arming, sketch mode, hotkeys, strings, icon id Lewis: May this revision serve well! --- crates/bone-app/src/app.rs | 161 +++- crates/bone-app/src/hotkeys.rs | 7 + crates/bone-app/src/shell.rs | 754 ++++++++++++++++-- crates/bone-app/src/sketch_mode.rs | 73 +- ..._hotkeys__tests__default_hotkey_table.snap | 1 + crates/bone-app/src/status_badge.rs | 5 + crates/bone-app/src/strings.rs | 109 +++ crates/bone-app/src/tools/geometry.rs | 21 +- crates/bone-types/src/icon.rs | 2 + crates/bone-types/src/lib.rs | 2 + crates/bone-ui/src/hotkey.rs | 1 + 11 files changed, 1032 insertions(+), 104 deletions(-) diff --git a/crates/bone-app/src/app.rs b/crates/bone-app/src/app.rs index 49a5b85..f7cd9ac 100644 --- a/crates/bone-app/src/app.rs +++ b/crates/bone-app/src/app.rs @@ -9,7 +9,7 @@ use bone_document::{ LineData, PrincipalPlane, ProjectedCurve, RebuildBudget, RebuildCost, RebuildPass, RecomputeScope, RevolveAxis, RevolveFeature, Sketch, Sketch3Relation, SketchDimension, SketchEdit, SketchEditError, SketchEntity, SketchRelation, SketchVersion, SolverError, - UndoStack, evaluate_extrude_reaching, evaluate_revolve_reaching, evaluate_sketch, + SweepPath, UndoStack, evaluate_extrude_reaching, evaluate_revolve_reaching, evaluate_sketch, }; use bone_render::{ BodyKey, Camera2, CameraTween, ChromeInstance, ChromePipeline, ChromeTextPipeline, @@ -56,7 +56,7 @@ use crate::selection::Selection; use crate::sketch_mode::{ ClickAnchor, DatumArming, DimensionFlow, DragPins, DragSession, EndConditionKind, ExtrudeArming, FeatureTool, Mode, ModeExit, Pending, PendingDimension, Plane, ReferenceKind, - RevolveArming, SketchTool, + RevolveArming, SketchTool, SweepArming, }; use crate::snap::{Anchor, SnapHit}; use crate::status_badge::ExtrudeStatus; @@ -87,6 +87,12 @@ enum RevolveArm { Contour, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum SweepArm { + Idle, + Path, +} + struct AppState { extent: ViewportExtent, renderer: SketchRenderer, @@ -115,6 +121,7 @@ struct AppState { pending_reattach: Option, extrude_reference_arm: Option, revolve_arm: RevolveArm, + sweep_arm: SweepArm, brep_selection: Vec, solid_renderer: SolidRenderer, solid_view: Option, @@ -604,6 +611,7 @@ fn active_pick_index(state: &AppState) -> Option { } let solid_mode = state.extrude_reference_arm.is_some() || state.revolve_arm == RevolveArm::Axis + || state.sweep_arm == SweepArm::Path || matches!(state.mode, Mode::Idle); if solid_mode && state.solid_view.is_some() { return solid_pick_index(&state.model); @@ -681,6 +689,9 @@ fn handle_viewport_click( if state.revolve_arm == RevolveArm::Contour && capture_revolve_contour(state, picked) { return; } + if state.sweep_arm == SweepArm::Path && capture_sweep_path(state, picked) { + return; + } if !additive && matches!(state.mode, Mode::Idle) && let Some(PickedItem::BrepFace(face)) = picked @@ -831,8 +842,10 @@ fn active_sketch_id(mode: &Mode, plane_sketches: &BTreeMap) -> Mode::Sketch { sketch_id, .. } => Some(*sketch_id), Mode::Extrude(ExtrudeArming::Profile { feature, .. }) => Some(feature.sketch), Mode::Revolve(RevolveArming::Profile { feature, .. }) => Some(feature.sketch), + Mode::Sweep(SweepArming::Profile { feature, .. }) => Some(feature.profile), Mode::Extrude(ExtrudeArming::AwaitingSketch(_)) | Mode::Revolve(RevolveArming::AwaitingSketch(_)) + | Mode::Sweep(SweepArming::AwaitingSketch(_)) | Mode::Idle | Mode::Datum(_) => plane_sketches.get(&Plane::Xy).copied(), } @@ -877,6 +890,8 @@ fn apply_feature_tool(state: &mut AppState, tool: Option) { FeatureTool::ExtrudedCut => arm_extruded_cut(state), FeatureTool::RevolvedBossBase => arm_revolve(state, ExtrudeOperation::default()), FeatureTool::RevolvedCut => arm_revolve(state, ExtrudeOperation::Cut), + FeatureTool::SweptBossBase => arm_sweep(state, ExtrudeOperation::default()), + FeatureTool::SweptCut => arm_sweep(state, ExtrudeOperation::Cut), FeatureTool::Sketch3D => arm_sketch3(state), datum if datum.is_datum() => arm_datum(state, datum), _ => {} @@ -1334,6 +1349,8 @@ fn arm_datum(state: &mut AppState, tool: FeatureTool) { | FeatureTool::ExtrudedCut | FeatureTool::RevolvedBossBase | FeatureTool::RevolvedCut + | FeatureTool::SweptBossBase + | FeatureTool::SweptCut | FeatureTool::Sketch3D => None, }; if let Some(feature) = feature { @@ -1393,15 +1410,24 @@ fn apply_revolve_edit(state: &mut AppState, edit: Option) { }); } -fn apply_revolve_axis_arm(state: &mut AppState, arm: bool) { - if arm && matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) { - state.revolve_arm = RevolveArm::Axis; - } -} - -fn apply_revolve_contour_arm(state: &mut AppState, arm: bool) { - if arm && matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) { - state.revolve_arm = RevolveArm::Contour; +fn apply_feature_arm(state: &mut AppState, arm: Option) { + match arm { + Some(shell::FeatureArm::RevolveAxis) + if matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) => + { + state.revolve_arm = RevolveArm::Axis; + } + Some(shell::FeatureArm::RevolveContour) + if matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) => + { + state.revolve_arm = RevolveArm::Contour; + } + Some(shell::FeatureArm::SweepPath) + if matches!(state.mode, Mode::Sweep(SweepArming::Profile { .. })) => + { + state.sweep_arm = SweepArm::Path; + } + _ => {} } } @@ -1433,6 +1459,84 @@ fn commit_armed_revolve( Some(committed) } +fn arm_sweep(state: &mut AppState, operation: ExtrudeOperation) { + match classify_extrude_profile(&state.document) { + ProfileChoice::NoSketch => notify_info(state, strings::NOTIFY_EXTRUDE_NO_SKETCH, None), + ProfileChoice::Unique(id) => { + state.mode = Mode::Sweep(SweepArming::armed(id, operation)); + state.sweep_arm = SweepArm::Path; + } + ProfileChoice::Ambiguous => { + state.mode = Mode::Sweep(SweepArming::AwaitingSketch(operation)); + } + } +} + +fn apply_sweep_edit(state: &mut AppState, edit: Option) { + let Some(edit) = edit else { return }; + let Mode::Sweep(SweepArming::Profile { feature, target }) = &state.mode else { + return; + }; + let next = edit.apply(feature); + let target = *target; + state.mode = Mode::Sweep(SweepArming::Profile { + feature: Box::new(next), + target, + }); +} + +fn apply_sweep_confirm(state: &mut AppState, confirm: Option) { + commit_armed_sweep(&mut state.document, &mut state.undo, &state.mode, confirm); +} + +fn commit_armed_sweep( + document: &mut Document, + undo: &mut UndoStack, + mode: &Mode, + confirm: Option, +) -> Option { + let Some(shell::ConfirmAction::Accept) = confirm else { + return None; + }; + let Mode::Sweep(SweepArming::Profile { feature, target }) = mode else { + return None; + }; + let snapshot = document.clone(); + let committed = match target { + Some(id) => { + document.insert_sweep(*id, **feature); + *id + } + None => document.commit_sweep(**feature), + }; + undo.record(snapshot); + Some(committed) +} + +fn capture_sweep_path(state: &mut AppState, picked: Option) -> bool { + let Mode::Sweep(SweepArming::Profile { feature, target }) = &state.mode else { + return false; + }; + let Some(path) = picked_sweep_path(&state.model, picked) else { + return false; + }; + let next = shell::SweepEdit::Path(path).apply(feature); + let target = *target; + state.mode = Mode::Sweep(SweepArming::Profile { + feature: Box::new(next), + target, + }); + state.sweep_arm = SweepArm::Idle; + true +} + +fn picked_sweep_path(model: &EvaluatedModel, picked: Option) -> Option { + match picked? { + PickedItem::BrepEdge(id) => model_edge_label(model, id).map(SweepPath::Edge), + _ => None, + } +} + fn apply_extrude_edit(state: &mut AppState, edit: Option) { let Some(edit) = edit else { return }; let Mode::Extrude(ExtrudeArming::Profile { feature, target }) = &state.mode else { @@ -1738,10 +1842,11 @@ fn active_solid_feature( Mode::Revolve(RevolveArming::Profile { feature, .. }) => { Some(PreviewFeature::Revolve(**feature)) } - Mode::Sketch { .. } | Mode::Datum(_) => None, + Mode::Sketch { .. } | Mode::Datum(_) | Mode::Sweep(SweepArming::Profile { .. }) => None, Mode::Idle | Mode::Extrude(ExtrudeArming::AwaitingSketch(_)) - | Mode::Revolve(RevolveArming::AwaitingSketch(_)) => framed + | Mode::Revolve(RevolveArming::AwaitingSketch(_)) + | Mode::Sweep(SweepArming::AwaitingSketch(_)) => framed .and_then(|id| document.extrude(id).copied()) .or_else(|| { document @@ -1826,7 +1931,7 @@ fn sketch3_overlay_view(state: &AppState) -> Option { edges: a.edges, aabb: a.aabb.union(b.aabb), }), - Mode::Extrude(_) | Mode::Revolve(_) | Mode::Datum(_) => None, + Mode::Extrude(_) | Mode::Revolve(_) | Mode::Sweep(_) | Mode::Datum(_) => None, } } @@ -2249,6 +2354,7 @@ fn feature_node_label(document: &Document, node: FeatureNode) -> Option match node { FeatureNode::Extrude(id) => Some(document.extrude_label(id).unwrap_or("").to_owned()), FeatureNode::Revolve(id) => Some(document.revolve_label(id).unwrap_or("").to_owned()), + FeatureNode::Sweep(id) => Some(document.sweep_label(id).unwrap_or("").to_owned()), FeatureNode::Datum(_) | FeatureNode::Sketch(_) | FeatureNode::Origin @@ -3085,6 +3191,9 @@ fn scopes_for_mode(mode: &Mode) -> HotkeyScopes { if mode.is_revolve() { scopes.push(HotkeyScope::Revolve); } + if mode.is_sweep() { + scopes.push(HotkeyScope::Sweep); + } if mode.is_datum() { scopes.push(HotkeyScope::Datum); } @@ -3163,9 +3272,12 @@ fn cancel_pending_or_exit(mode: Mode) -> 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::Extrude(_) | Mode::Revolve(_) | Mode::Datum(_) => { - Mode::Idle - } + Mode::Sketch { .. } + | Mode::Idle + | Mode::Extrude(_) + | Mode::Revolve(_) + | Mode::Sweep(_) + | Mode::Datum(_) => Mode::Idle, } } @@ -3236,6 +3348,7 @@ impl AppCore { pending_reattach: None, extrude_reference_arm: None, revolve_arm: RevolveArm::Idle, + sweep_arm: SweepArm::Idle, brep_selection: Vec::new(), solid_renderer, solid_view: None, @@ -3697,14 +3810,16 @@ fn render_frame( state.pending_reattach = None; state.extrude_reference_arm = None; state.revolve_arm = RevolveArm::Idle; + state.sweep_arm = SweepArm::Idle; } apply_extrude_edit(state, frame.extrude_edit); apply_extrude_arm(state, frame.extrude_arm_reference); apply_extrude_confirm(state, frame.confirm_action); apply_revolve_edit(state, frame.revolve_edit); - apply_revolve_axis_arm(state, frame.revolve_arm_axis); - apply_revolve_contour_arm(state, frame.revolve_arm_contour); apply_revolve_confirm(state, frame.confirm_action); + apply_sweep_edit(state, frame.sweep_edit); + apply_feature_arm(state, frame.feature_arm); + apply_sweep_confirm(state, frame.confirm_action); apply_datum_confirm(state, frame.confirm_action); let prev_spline_sketch = state.mode.sketch_id(); let prev_spline_fresh = state.mode.spline_draft_fresh(); @@ -4827,8 +4942,8 @@ fn suppress_pointer_activations(frame: shell::ShellFrame) -> shell::ShellFrame { extrude_edit: frame.extrude_edit, extrude_arm_reference: frame.extrude_arm_reference, revolve_edit: frame.revolve_edit, - revolve_arm_axis: frame.revolve_arm_axis, - revolve_arm_contour: frame.revolve_arm_contour, + sweep_edit: frame.sweep_edit, + feature_arm: frame.feature_arm, plane_picked: None, sketch_activated: None, sketch_rename: None, @@ -6794,8 +6909,8 @@ mod tests { extrude_edit: None, extrude_arm_reference: None, revolve_edit: None, - revolve_arm_axis: false, - revolve_arm_contour: false, + sweep_edit: None, + feature_arm: None, plane_picked: None, sketch_activated: None, sketch_rename: None, diff --git a/crates/bone-app/src/hotkeys.rs b/crates/bone-app/src/hotkeys.rs index bc0fc22..f191f4d 100644 --- a/crates/bone-app/src/hotkeys.rs +++ b/crates/bone-app/src/hotkeys.rs @@ -162,6 +162,13 @@ pub const COMMANDS: &[Command] = &[ label: s::HOTKEY_LABEL_ESCAPE, defaults: &[ESC], }, + Command { + action: ESCAPE_ACTION, + kind: None, + scope: HotkeyScope::Sweep, + label: s::HOTKEY_LABEL_ESCAPE, + defaults: &[ESC], + }, Command { action: ESCAPE_ACTION, kind: None, diff --git a/crates/bone-app/src/shell.rs b/crates/bone-app/src/shell.rs index 1f99ea8..0f9218d 100644 --- a/crates/bone-app/src/shell.rs +++ b/crates/bone-app/src/shell.rs @@ -8,11 +8,11 @@ use bone_document::{ ExtrudeOperation, ExtrudeSense, FeatureEdge, FeatureNode, FeatureScope, FromCondition, MergeResult, RevolveAngle, RevolveAxis, RevolveDirection2, RevolveEndCondition, RevolveFeature, Sketch, Sketch3Version, SketchDimension, SketchEntity, SketchRelation, SketchStatusReport, - ThinWall, ThinWallDirection, + SweepFeature, SweepOrientation, SweepPath, SweepProfileKind, ThinWall, ThinWallDirection, }; use bone_types::{ Angle, Camera3, DatumId, ExtrudeId, FaceLabel, FeatureId, IconId, Length, Point2, - PositiveLength, RevolveId, RollbackMarker, SketchDimensionId, SketchEntityId, SketchId, + PositiveLength, RevolveId, RollbackMarker, SketchDimensionId, SketchEntityId, SketchId, SweepId, VertexLabel, }; use bone_ui::a11y::{AccessNode, Role}; @@ -52,7 +52,7 @@ use crate::settings::Settings; use crate::sketch_mode::PendingDimension; use crate::sketch_mode::{ EndConditionKind, ExtrudeArming, FeatureTool, Mode, ModeExit, Plane, ReferenceKind, - RevolveArming, SketchTool, default_extrude_depth, end_condition_depth, + RevolveArming, SketchTool, SweepArming, default_extrude_depth, end_condition_depth, }; use crate::smart_dimension; use crate::status_badge::{ @@ -369,6 +369,7 @@ pub struct ShellState { pub dim_property: Option, pub extrude_property: Option, pub revolve_property: Option, + pub sweep_property: Option, pub settings_dialog_open: bool, pub keyboard_dialog_open: bool, pub hotkey_capture: BTreeMap, @@ -837,6 +838,50 @@ impl RevolveEdit { } } +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum SweepEdit { + Path(SweepPath), + Orientation(SweepOrientation), + ProfileKind(SweepProfileKind), + Thin(Option), + FlipSide(bool), + Scope(FeatureScope), + Merge(MergeResult), +} + +impl SweepEdit { + #[must_use] + pub fn apply(self, feature: &SweepFeature) -> SweepFeature { + match self { + Self::Path(path) => SweepFeature { path, ..*feature }, + Self::Orientation(orientation) => SweepFeature { + orientation, + ..*feature + }, + Self::ProfileKind(profile_kind) => SweepFeature { + profile_kind, + ..*feature + }, + Self::Thin(thin_wall) => SweepFeature { + thin_wall, + ..*feature + }, + Self::FlipSide(flip_side) => SweepFeature { + flip_side, + ..*feature + }, + Self::Scope(scope) => SweepFeature { scope, ..*feature }, + Self::Merge(merge) => match feature.operation { + ExtrudeOperation::Boss(_) => SweepFeature { + operation: ExtrudeOperation::Boss(merge), + ..*feature + }, + ExtrudeOperation::Cut => *feature, + }, + } + } +} + pub struct RevolvePropertyEditor { sketch: SketchId, kind: SelectionEditor, @@ -898,6 +943,164 @@ impl RevolvePropertyEditor { } } +const DEFAULT_SWEEP_DIAMETER_MM: f64 = 10.0; + +fn default_sweep_diameter() -> PositiveLength { + let Ok(diameter) = PositiveLength::new(Length::new::(DEFAULT_SWEEP_DIAMETER_MM)) + else { + unreachable!("constant sweep diameter is positive and finite") + }; + diameter +} + +fn sweep_merge_on(feature: &SweepFeature) -> bool { + matches!( + feature.operation, + ExtrudeOperation::Boss(MergeResult::Merge) + ) +} + +fn sweep_profile_display_editor() -> SelectionEditor { + SelectionEditor::new( + vec![PropertyOption { + label: strings::SWEEP_PROFILE_SKETCH, + }], + Some(0), + ) +} + +fn sweep_path_label(path: SweepPath) -> StringKey { + match path { + SweepPath::Sketch(sketch) if sketch == SketchId::default() => strings::SWEEP_PATH_SELECT, + SweepPath::Sketch(_) => strings::SWEEP_PATH_SKETCH, + SweepPath::Edge(_) => strings::SWEEP_PATH_EDGE, + } +} + +fn sweep_path_editor(path: SweepPath) -> SelectionEditor { + SelectionEditor::new( + vec![ + PropertyOption { + label: sweep_path_label(path), + }, + PropertyOption { + label: strings::SWEEP_PATH_SELECT, + }, + ], + Some(0), + ) +} + +fn sweep_orientation_options() -> Vec { + vec![ + PropertyOption { + label: strings::SWEEP_ORIENTATION_FOLLOW, + }, + PropertyOption { + label: strings::SWEEP_ORIENTATION_KEEP_NORMAL, + }, + ] +} + +fn sweep_orientation_index(orientation: SweepOrientation) -> usize { + match orientation { + SweepOrientation::FollowPath => 0, + SweepOrientation::KeepNormalConstant => 1, + } +} + +fn sweep_orientation_from_index(index: Option) -> SweepOrientation { + match index { + Some(1) => SweepOrientation::KeepNormalConstant, + _ => SweepOrientation::FollowPath, + } +} + +fn sweep_profile_kind_options() -> Vec { + vec![ + PropertyOption { + label: strings::SWEEP_PROFILE_KIND_SKETCH, + }, + PropertyOption { + label: strings::SWEEP_PROFILE_KIND_CIRCULAR, + }, + ] +} + +fn sweep_profile_kind_index(kind: SweepProfileKind) -> usize { + match kind { + SweepProfileKind::Sketch => 0, + SweepProfileKind::Circular { .. } => 1, + } +} + +fn sweep_diameter_value(kind: SweepProfileKind) -> Length { + match kind { + SweepProfileKind::Circular { diameter } => diameter.get(), + SweepProfileKind::Sketch => default_sweep_diameter().get(), + } +} + +pub struct SweepPropertyEditor { + profile: SketchId, + profile_display: SelectionEditor, + path: SelectionEditor, + orientation: SelectionEditor, + profile_kind: SelectionEditor, + diameter: LengthEditor, + thin: BoolEditor, + thin_direction: SelectionEditor, + thickness: LengthEditor, + thickness2: LengthEditor, + flip_side: BoolEditor, + scope: SelectionEditor, + merge: BoolEditor, + is_cut: bool, +} + +impl SweepPropertyEditor { + fn new(feature: &SweepFeature) -> Self { + Self { + profile: feature.profile, + profile_display: sweep_profile_display_editor(), + path: sweep_path_editor(feature.path), + orientation: SelectionEditor::new( + sweep_orientation_options(), + Some(sweep_orientation_index(feature.orientation)), + ), + profile_kind: SelectionEditor::new( + sweep_profile_kind_options(), + Some(sweep_profile_kind_index(feature.profile_kind)), + ), + diameter: LengthEditor::new(sweep_diameter_value(feature.profile_kind)), + thin: BoolEditor::new(feature.thin_wall.is_some()), + thin_direction: thin_direction_editor(feature.thin_wall), + thickness: LengthEditor::new(thin_thickness_value(feature.thin_wall)), + thickness2: LengthEditor::new(thin_thickness2_value(feature.thin_wall)), + flip_side: BoolEditor::new(feature.flip_side), + scope: SelectionEditor::new(scope_options(), Some(scope_index(feature.scope))), + merge: BoolEditor::new(sweep_merge_on(feature)), + is_cut: feature.operation.is_cut(), + } + } + + fn synced(mut self, feature: &SweepFeature) -> Self { + self.path = sweep_path_editor(feature.path); + self.orientation.current = Some(sweep_orientation_index(feature.orientation)); + self.profile_kind.current = Some(sweep_profile_kind_index(feature.profile_kind)); + self.diameter.value = sweep_diameter_value(feature.profile_kind); + self.thin.value = feature.thin_wall.is_some(); + self.thin_direction.current = Some(thin_direction_index(feature.thin_wall)); + self.thickness.value = thin_thickness_value(feature.thin_wall); + self.thickness2.value = thin_thickness2_value(feature.thin_wall); + self.flip_side.value = feature.flip_side; + self.scope.current = Some(scope_index(feature.scope)); + self.merge.value = sweep_merge_on(feature); + self.is_cut = feature.operation.is_cut(); + self + } +} + const REVOLVE_PICK_INDEX: usize = 1; fn revolve_axis_label(axis: RevolveAxis) -> StringKey { @@ -1303,6 +1506,13 @@ fn thin_cap_ends(thin: Option) -> bool { thin.is_some_and(|t| t.cap_ends) } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum FeatureArm { + RevolveAxis, + RevolveContour, + SweepPath, +} + #[derive(Clone, Debug, PartialEq)] pub struct ShellFrame { pub paints: Vec, @@ -1316,8 +1526,8 @@ pub struct ShellFrame { pub extrude_edit: Option, pub extrude_arm_reference: Option, pub revolve_edit: Option, - pub revolve_arm_axis: bool, - pub revolve_arm_contour: bool, + pub sweep_edit: Option, + pub feature_arm: Option, pub plane_picked: Option, pub sketch_activated: Option, pub sketch_rename: Option, @@ -1355,8 +1565,8 @@ impl ShellFrame { extrude_edit: None, extrude_arm_reference: None, revolve_edit: None, - revolve_arm_axis: false, - revolve_arm_contour: false, + sweep_edit: None, + feature_arm: None, plane_picked: None, sketch_activated: None, sketch_rename: None, @@ -1499,7 +1709,7 @@ impl Shell { &mut self.state, selection, mode.active_tool(), - mode.is_extrude() || mode.is_revolve(), + mode.is_extrude() || mode.is_revolve() || mode.is_sweep(), ); let tab_clicked = render_left_pane_tabs( ctx, @@ -1551,6 +1761,7 @@ impl Shell { dim: &mut self.state.dim_property, extrude: &mut self.state.extrude_property, revolve: &mut self.state.revolve_property, + sweep: &mut self.state.sweep_property, groups: &mut self.state.property_groups, }, PropertyState { @@ -1564,8 +1775,8 @@ impl Shell { let extrude_edit = pane.extrude_edit; let extrude_arm_reference = pane.extrude_arm_reference; let revolve_edit = pane.revolve_edit; - let revolve_arm_axis = pane.revolve_arm_axis; - let revolve_arm_contour = pane.revolve_arm_contour; + let sweep_edit = pane.sweep_edit; + let feature_arm = pane.feature_arm; render_doc_tabs(ctx, doc_tabs_rect, &self.ids, &mut paints); let status_report: Option<&SketchStatusReport> = if let Some(s3) = mode.sketch_id().and_then(|id| document.sketch3(id)) @@ -1643,6 +1854,7 @@ impl Shell { ModeExit::Dialog => { matches!(mode, Mode::Extrude(ExtrudeArming::Profile { .. })) || matches!(mode, Mode::Revolve(RevolveArming::Profile { .. })) + || matches!(mode, Mode::Sweep(SweepArming::Profile { .. })) } ModeExit::None => false, }; @@ -1777,8 +1989,8 @@ impl Shell { extrude_edit, extrude_arm_reference, revolve_edit, - revolve_arm_axis, - revolve_arm_contour, + sweep_edit, + feature_arm, plane_picked, sketch_activated, sketch_rename, @@ -2617,10 +2829,6 @@ fn render_ribbon( .into_iter() .map(|item| size_item(item, small_min)) .collect(); - let extrude_items = feature_tool_items(ctx, ribbon, mode, FeatureTool::EXTRUDE, large_min); - let revolve_items = feature_tool_items(ctx, ribbon, mode, FeatureTool::REVOLVE, large_min); - let datum_items = feature_tool_items(ctx, ribbon, mode, FeatureTool::DATUM, large_min); - let sketch3_items = feature_tool_items(ctx, ribbon, mode, FeatureTool::SKETCH3, large_min); let sketch_tab_id = sketch_tab_id(ribbon); let features_tab_id = features_tab_id(ribbon); let groups = build_sketch_groups( @@ -2632,15 +2840,7 @@ fn render_ribbon( small_min, overflow_open, ); - let feature_groups = build_feature_groups( - ribbon, - extrude_items, - revolve_items, - datum_items, - sketch3_items, - large_min, - overflow_open, - ); + let feature_groups = feature_ribbon_groups(ctx, ribbon, mode, large_min, overflow_open); let placeholder_tab = |key: &'static str, label: StringKey| { RibbonTab::new(ribbon.child(WidgetKey::new(key)), label, Vec::new()).disabled(true) }; @@ -2723,12 +2923,17 @@ fn build_sketch_groups( ] } +struct FeatureGroupItems { + extrude: Vec, + revolve: Vec, + sweep: Vec, + datum: Vec, + sketch3: Vec, +} + fn build_feature_groups( ribbon: WidgetId, - extrude_items: Vec, - revolve_items: Vec, - datum_items: Vec, - sketch3_items: Vec, + items: FeatureGroupItems, large_min: LayoutPx, overflow_open: &BTreeMap, ) -> Vec { @@ -2746,25 +2951,31 @@ fn build_feature_groups( } }; vec![ - feature_group( - "group.extrude", - strings::RIBBON_GROUP_EXTRUDE, - extrude_items, - ), - feature_group( - "group.revolve", - strings::RIBBON_GROUP_REVOLVE, - revolve_items, - ), - feature_group("group.datum", strings::RIBBON_GROUP_DATUM, datum_items), - feature_group( - "group.sketch3", - strings::RIBBON_GROUP_SKETCH3, - sketch3_items, - ), + feature_group("group.extrude", strings::RIBBON_GROUP_EXTRUDE, items.extrude), + feature_group("group.revolve", strings::RIBBON_GROUP_REVOLVE, items.revolve), + feature_group("group.sweep", strings::RIBBON_GROUP_SWEEP, items.sweep), + feature_group("group.datum", strings::RIBBON_GROUP_DATUM, items.datum), + feature_group("group.sketch3", strings::RIBBON_GROUP_SKETCH3, items.sketch3), ] } +fn feature_ribbon_groups( + ctx: &FrameCtx<'_>, + ribbon: WidgetId, + mode: &Mode, + large_min: LayoutPx, + overflow_open: &BTreeMap, +) -> Vec { + let items = FeatureGroupItems { + extrude: feature_tool_items(ctx, ribbon, mode, FeatureTool::EXTRUDE, large_min), + revolve: feature_tool_items(ctx, ribbon, mode, FeatureTool::REVOLVE, large_min), + sweep: feature_tool_items(ctx, ribbon, mode, FeatureTool::SWEEP, large_min), + datum: feature_tool_items(ctx, ribbon, mode, FeatureTool::DATUM, large_min), + sketch3: feature_tool_items(ctx, ribbon, mode, FeatureTool::SKETCH3, large_min), + }; + build_feature_groups(ribbon, items, large_min, overflow_open) +} + fn process_ribbon_response( response: bone_ui::widgets::RibbonResponse, paints: &mut Vec, @@ -2890,6 +3101,10 @@ fn consumed_sketches(document: &Document) -> BTreeSet { Some(FeatureNode::Sketch(sketch_id)) => Some(sketch_id), _ => None, }, + FeatureEdge::PathToSweep { path, .. } => match tree.node(*path) { + Some(FeatureNode::Sketch(sketch_id)) => Some(sketch_id), + _ => None, + }, FeatureEdge::FaceToSketch { .. } | FeatureEdge::ReferenceToDatum { .. } | FeatureEdge::AxisToRevolve { .. } => None, @@ -2979,6 +3194,9 @@ fn feature_rows( FeatureNode::Revolve(revolve_id) => { Some(revolve_feature_node(document, part_id, revolve_id, badges)) } + FeatureNode::Sweep(sweep_id) => { + Some(sweep_feature_node(document, part_id, sweep_id, badges)) + } FeatureNode::Datum(datum_id) => { Some(datum_feature_node(document, part_id, datum_id, badges)) } @@ -3022,6 +3240,34 @@ fn revolve_feature_node( .disabled(rolled_back) } +fn sweep_widget_id(part_id: WidgetId, sweep_id: SweepId) -> WidgetId { + part_id.child_indexed(WidgetKey::new("sweep"), sweep_id.as_u64()) +} + +fn sweep_icon(feature: Option<&SweepFeature>) -> IconId { + match feature.map(|feature| feature.operation) { + Some(ExtrudeOperation::Cut) => IconId::SweptCut, + _ => IconId::SweptBossBase, + } +} + +fn sweep_feature_node( + document: &Document, + part_id: WidgetId, + sweep_id: SweepId, + badges: &BTreeMap, +) -> TreeNode { + let widget_id = sweep_widget_id(part_id, sweep_id); + let label = document.sweep_label(sweep_id).unwrap_or("").to_owned(); + let feature = document.feature_tree().feature_of_sweep(sweep_id); + let badge = feature.and_then(|feature| badges.get(&feature).copied()); + let rolled_back = feature.is_some_and(|feature| document.is_rolled_back(feature)); + TreeNode::leaf_owned(widget_id, label) + .with_icon(sweep_icon(document.sweep(sweep_id))) + .with_badge(badge) + .disabled(rolled_back) +} + fn datum_icon(feature: Option<&DatumFeature>) -> IconId { match feature { Some(DatumFeature::Plane(_)) => IconId::TreePlane, @@ -3864,8 +4110,8 @@ struct PropertyPaneOutcome { extrude_edit: Option, extrude_arm_reference: Option, revolve_edit: Option, - revolve_arm_axis: bool, - revolve_arm_contour: bool, + sweep_edit: Option, + feature_arm: Option, confirm: Option, } @@ -3873,9 +4119,37 @@ struct PaneEditors<'a> { dim: &'a mut Option, extrude: &'a mut Option, revolve: &'a mut Option, + sweep: &'a mut Option, groups: &'a mut BTreeMap, } +fn clear_inactive_pane_editors(mode: &Mode, editors: &mut PaneEditors<'_>, keep_dim: bool) { + if !keep_dim { + *editors.dim = None; + } + if !matches!(mode, Mode::Extrude(ExtrudeArming::Profile { .. })) { + *editors.extrude = None; + } + if !matches!(mode, Mode::Revolve(RevolveArming::Profile { .. })) { + *editors.revolve = None; + } + if !matches!(mode, Mode::Sweep(SweepArming::Profile { .. })) { + *editors.sweep = None; + } +} + +fn awaiting_sketch_prompt( + ctx: &mut FrameCtx<'_>, + rect: LayoutRect, + id: WidgetId, + clipboard: &mut MemoryClipboard, + paints: &mut Vec, +) -> PropertyPaneOutcome { + let mut editors = vec![row_editor(strings::EXTRUDE_PROMPT_SELECT_SKETCH, "")]; + render_static_rows(ctx, rect, id, clipboard, &mut editors, paints); + PropertyPaneOutcome::default() +} + fn render_property_pane( ctx: &mut FrameCtx<'_>, rect: LayoutRect, @@ -3891,15 +4165,8 @@ fn render_property_pane( .sketch .filter(|_| in_sketch) .and_then(|s| resolve_selection_target(s, state.selection).map(|t| (s, t))); - if !matches!(resolved, Some((_, SelectionTarget::Dimension(_, _)))) { - *editors.dim = None; - } - if !matches!(state.mode, Mode::Extrude(ExtrudeArming::Profile { .. })) { - *editors.extrude = None; - } - if !matches!(state.mode, Mode::Revolve(RevolveArming::Profile { .. })) { - *editors.revolve = None; - } + let keep_dim = matches!(resolved, Some((_, SelectionTarget::Dimension(_, _)))); + clear_inactive_pane_editors(state.mode, editors, keep_dim); if rect.size.width.value() <= 0.0 || rect.size.height.value() <= 0.0 { return PropertyPaneOutcome::default(); } @@ -3916,18 +4183,14 @@ fn render_property_pane( let outcome = render_extrude_rows(ctx, rect, id, clipboard, editors, feature, paints); PropertyPaneOutcome { - dimension_edit: None, extrude_edit: outcome.edit, extrude_arm_reference: outcome.arm_reference, - revolve_edit: None, confirm: outcome.confirm, ..PropertyPaneOutcome::default() } } ExtrudeArming::AwaitingSketch(_) => { - let mut editors = vec![row_editor(strings::EXTRUDE_PROMPT_SELECT_SKETCH, "")]; - render_static_rows(ctx, rect, id, clipboard, &mut editors, paints); - PropertyPaneOutcome::default() + awaiting_sketch_prompt(ctx, rect, id, clipboard, paints) } }; } @@ -3937,9 +4200,17 @@ fn render_property_pane( render_revolve_rows(ctx, rect, id, clipboard, editors, feature, paints).into_pane() } RevolveArming::AwaitingSketch(_) => { - let mut editors = vec![row_editor(strings::EXTRUDE_PROMPT_SELECT_SKETCH, "")]; - render_static_rows(ctx, rect, id, clipboard, &mut editors, paints); - PropertyPaneOutcome::default() + awaiting_sketch_prompt(ctx, rect, id, clipboard, paints) + } + }; + } + if let Mode::Sweep(arming) = state.mode { + return match arming { + SweepArming::Profile { feature, .. } => { + render_sweep_rows(ctx, rect, id, clipboard, editors, feature, paints).into_pane() + } + SweepArming::AwaitingSketch(_) => { + awaiting_sketch_prompt(ctx, rect, id, clipboard, paints) } }; } @@ -4362,10 +4633,16 @@ struct RevolveRowsOutcome { impl RevolveRowsOutcome { fn into_pane(self) -> PropertyPaneOutcome { + let feature_arm = if self.arm_axis { + Some(FeatureArm::RevolveAxis) + } else if self.arm_contour { + Some(FeatureArm::RevolveContour) + } else { + None + }; PropertyPaneOutcome { revolve_edit: self.edit, - revolve_arm_axis: self.arm_axis, - revolve_arm_contour: self.arm_contour, + feature_arm, confirm: self.confirm, ..PropertyPaneOutcome::default() } @@ -4672,6 +4949,342 @@ fn render_revolve_rows( } } +const SWEEP_PICK_INDEX: usize = 1; +const SWEEP_CIRCULAR_INDEX: usize = 1; + +fn sync_sweep_editor<'slot>( + slot: &'slot mut Option, + feature: &SweepFeature, +) -> &'slot mut SweepPropertyEditor { + let editor = match slot.take() { + Some(editor) if editor.profile == feature.profile => editor.synced(feature), + _ => SweepPropertyEditor::new(feature), + }; + slot.insert(editor) +} + +fn sweep_row_id(key: &'static str) -> WidgetId { + WidgetId::ROOT + .child(WidgetKey::new("props.sweep")) + .child(WidgetKey::new(key)) +} + +struct SweepRowsOutcome { + edit: Option, + arm_path: bool, + confirm: Option, +} + +impl SweepRowsOutcome { + fn into_pane(self) -> PropertyPaneOutcome { + PropertyPaneOutcome { + sweep_edit: self.edit, + feature_arm: self.arm_path.then_some(FeatureArm::SweepPath), + confirm: self.confirm, + ..PropertyPaneOutcome::default() + } + } +} + +fn sweep_pane_header( + ctx: &mut FrameCtx<'_>, + id: WidgetId, + rect: LayoutRect, +) -> (Option, Vec) { + let header_id = id.child(WidgetKey::new("header")); + let header = show_property_pane_header( + ctx, + PropertyPaneHeader { + id: header_id, + rect: LayoutRect::new( + rect.origin, + LayoutSize::new(rect.size.width, LayoutPx::new(PM_HEADER_HEIGHT)), + ), + title: strings::PROPERTY_PANE_SWEEP_TITLE, + accept_id: header_id.child(WidgetKey::new("accept")), + cancel_id: header_id.child(WidgetKey::new("cancel")), + }, + ); + let confirm = match header.action { + Some(PropertyPaneAction::Accept) => Some(ConfirmAction::Accept), + Some(PropertyPaneAction::Cancel) => Some(ConfirmAction::Cancel), + None => None, + }; + (confirm, header.paint) +} + +fn sweep_profile_path_group_rows(editor: &mut SweepPropertyEditor) -> Vec> { + let SweepPropertyEditor { + profile_display, + path, + .. + } = editor; + vec![ + PropertyRow { + id: sweep_row_id("profile"), + label: strings::PROPERTY_ROW_SWEEP_PROFILE, + editor: profile_display, + read_only: true, + }, + PropertyRow { + id: sweep_row_id("path"), + label: strings::PROPERTY_ROW_SWEEP_PATH, + editor: path, + read_only: false, + }, + ] +} + +fn sweep_options_group_rows(editor: &mut SweepPropertyEditor) -> Vec> { + let show_diameter = editor.profile_kind.current == Some(SWEEP_CIRCULAR_INDEX); + let SweepPropertyEditor { + orientation, + profile_kind, + diameter, + .. + } = editor; + let mut rows = vec![ + PropertyRow { + id: sweep_row_id("orientation"), + label: strings::PROPERTY_ROW_SWEEP_ORIENTATION, + editor: orientation, + read_only: false, + }, + PropertyRow { + id: sweep_row_id("profile_kind"), + label: strings::PROPERTY_ROW_SWEEP_PROFILE_KIND, + editor: profile_kind, + read_only: false, + }, + ]; + if show_diameter { + rows.push(PropertyRow { + id: sweep_row_id("diameter"), + label: strings::PROPERTY_ROW_SWEEP_DIAMETER, + editor: diameter, + read_only: false, + }); + } + rows +} + +fn sweep_scope_group_rows(editor: &mut SweepPropertyEditor) -> Vec> { + let is_cut = editor.is_cut; + let thin_on = editor.thin.value; + let two_direction = matches!( + thin_direction_from_index(editor.thin_direction.current), + ThinWallDirection::TwoDirection + ); + let SweepPropertyEditor { + scope, + thin, + thin_direction, + thickness, + thickness2, + flip_side, + merge, + .. + } = editor; + let mut rows = vec![ + PropertyRow { + id: sweep_row_id("scope"), + label: strings::PROPERTY_ROW_EXTRUDE_SCOPE, + editor: scope, + read_only: false, + }, + PropertyRow { + id: sweep_row_id("thin"), + label: strings::PROPERTY_ROW_EXTRUDE_THIN, + editor: thin, + read_only: false, + }, + ]; + if thin_on { + rows.push(PropertyRow { + id: sweep_row_id("thin_direction"), + label: strings::PROPERTY_ROW_EXTRUDE_DIRECTION, + editor: thin_direction, + read_only: false, + }); + rows.push(PropertyRow { + id: sweep_row_id("thickness"), + label: strings::PROPERTY_ROW_EXTRUDE_THICKNESS, + editor: thickness, + read_only: false, + }); + if two_direction { + rows.push(PropertyRow { + id: sweep_row_id("thickness2"), + label: strings::PROPERTY_ROW_EXTRUDE_THICKNESS_TWO, + editor: thickness2, + read_only: false, + }); + } + } + if is_cut { + rows.push(PropertyRow { + id: sweep_row_id("flip_side"), + label: strings::PROPERTY_ROW_EXTRUDE_FLIP_SIDE, + editor: flip_side, + read_only: false, + }); + } else { + rows.push(PropertyRow { + id: sweep_row_id("merge"), + label: strings::PROPERTY_ROW_EXTRUDE_MERGE, + editor: merge, + read_only: false, + }); + } + rows +} + +fn current_thin_from_sweep_editor(editor: &SweepPropertyEditor) -> Option { + if !editor.thin.value { + return None; + } + let thickness = PositiveLength::new(editor.thickness.value).ok()?; + let direction = thin_direction_from_index(editor.thin_direction.current); + let thickness2 = match direction { + ThinWallDirection::TwoDirection => PositiveLength::new(editor.thickness2.value).ok(), + _ => None, + }; + Some(ThinWall { + thickness, + direction, + thickness2, + cap_ends: false, + auto_fillet: None, + }) +} + +fn sweep_profile_kind_from_index(index: Option, diameter: Length) -> SweepProfileKind { + match index { + Some(SWEEP_CIRCULAR_INDEX) => SweepProfileKind::Circular { + diameter: PositiveLength::new(diameter).unwrap_or_else(|_| default_sweep_diameter()), + }, + _ => SweepProfileKind::Sketch, + } +} + +fn resolve_sweep_edit(editor: &SweepPropertyEditor, changed: &[WidgetId]) -> Option { + let on = |key: &'static str| changed.contains(&sweep_row_id(key)); + if on("orientation") { + return Some(SweepEdit::Orientation(sweep_orientation_from_index( + editor.orientation.current, + ))); + } + if on("profile_kind") || on("diameter") { + return Some(SweepEdit::ProfileKind(sweep_profile_kind_from_index( + editor.profile_kind.current, + editor.diameter.value, + ))); + } + if on("thin") || on("thin_direction") || on("thickness") || on("thickness2") { + return Some(SweepEdit::Thin(current_thin_from_sweep_editor(editor))); + } + if on("flip_side") { + return Some(SweepEdit::FlipSide(editor.flip_side.value)); + } + if on("scope") { + return Some(SweepEdit::Scope(scope_from_index(editor.scope.current))); + } + if on("merge") { + return Some(SweepEdit::Merge(if editor.merge.value { + MergeResult::Merge + } else { + MergeResult::Separate + })); + } + None +} + +fn resolve_sweep_arm(editor: &SweepPropertyEditor, changed: &[WidgetId]) -> bool { + changed.contains(&sweep_row_id("path")) && editor.path.current == Some(SWEEP_PICK_INDEX) +} + +fn render_sweep_rows( + ctx: &mut FrameCtx<'_>, + rect: LayoutRect, + id: WidgetId, + clipboard: &mut MemoryClipboard, + editors: &mut PaneEditors<'_>, + feature: &SweepFeature, + paints: &mut Vec, +) -> SweepRowsOutcome { + ctx.a11y.push( + id, + rect, + AccessNode::new(Role::Form).with_label(strings::PROPERTY_PANE_LABEL), + ); + let (confirm, header_paint) = sweep_pane_header(ctx, id, rect); + paints.extend(header_paint); + + let groups = &mut *editors.groups; + let editor = sync_sweep_editor(editors.sweep, feature); + let groups_top = LayoutPx::new(rect.origin.y.value() + PM_HEADER_HEIGHT + PM_GROUP_GAP); + let mut changed: Vec = Vec::new(); + let mut render_group = |ctx: &mut FrameCtx<'_>, + rows: &mut Vec>, + key: &'static str, + title: StringKey, + top: LayoutPx| { + let (next_y, ch) = render_property_group( + ctx, + clipboard, + groups, + PropertyGroupSpec { + id: id.child(WidgetKey::new(key)), + title, + top_left: LayoutPos::new(rect.origin.x, top), + width: rect.size.width, + }, + rows, + paints, + ); + changed.extend(ch); + next_y + }; + let options_top = { + let mut rows = sweep_profile_path_group_rows(editor); + render_group( + ctx, + &mut rows, + "group.profile_path", + strings::PROPERTY_GROUP_SWEEP_PROFILE_PATH, + groups_top, + ) + }; + let scope_top = { + let mut rows = sweep_options_group_rows(editor); + render_group( + ctx, + &mut rows, + "group.options", + strings::PROPERTY_GROUP_SWEEP_OPTIONS, + options_top, + ) + }; + { + let mut rows = sweep_scope_group_rows(editor); + let _ = render_group( + ctx, + &mut rows, + "group.feature_scope", + strings::PROPERTY_GROUP_FEATURE_SCOPE, + scope_top, + ); + } + + let edit = resolve_sweep_edit(editor, &changed); + let arm_path = resolve_sweep_arm(editor, &changed); + SweepRowsOutcome { + edit, + arm_path, + confirm, + } +} + fn render_extrude_rows( ctx: &mut FrameCtx<'_>, rect: LayoutRect, @@ -5601,6 +6214,7 @@ fn mode_status_label(strings_table: &StringTable, mode: &Mode, document: &Docume Mode::Idle => LabelText::Key(strings::STATUS_READY), Mode::Extrude(_) => LabelText::Key(strings::STATUS_EXTRUDE_ACTIVE), Mode::Revolve(_) => LabelText::Key(strings::STATUS_REVOLVE_ACTIVE), + Mode::Sweep(_) => LabelText::Key(strings::STATUS_SWEEP_ACTIVE), Mode::Datum(_) => LabelText::Key(strings::STATUS_DATUM_ACTIVE), Mode::Sketch { session, .. } if session.spatial.is_some() => { LabelText::Key(strings::STATUS_SKETCH3_ACTIVE) @@ -5810,6 +6424,8 @@ fn feature_tool_key(tool: FeatureTool) -> &'static str { FeatureTool::ExtrudedCut => "tool.extruded_cut", FeatureTool::RevolvedBossBase => "tool.revolved_boss_base", FeatureTool::RevolvedCut => "tool.revolved_cut", + FeatureTool::SweptBossBase => "tool.swept_boss_base", + FeatureTool::SweptCut => "tool.swept_cut", FeatureTool::DatumPlane => "tool.datum_plane", FeatureTool::DatumAxis => "tool.datum_axis", FeatureTool::DatumPoint => "tool.datum_point", @@ -5824,6 +6440,8 @@ fn feature_tool_label(tool: FeatureTool) -> StringKey { FeatureTool::ExtrudedCut => strings::TOOL_EXTRUDED_CUT, FeatureTool::RevolvedBossBase => strings::TOOL_REVOLVED_BOSS_BASE, FeatureTool::RevolvedCut => strings::TOOL_REVOLVED_CUT, + FeatureTool::SweptBossBase => strings::TOOL_SWEPT_BOSS_BASE, + FeatureTool::SweptCut => strings::TOOL_SWEPT_CUT, FeatureTool::DatumPlane => strings::TOOL_DATUM_PLANE, FeatureTool::DatumAxis => strings::TOOL_DATUM_AXIS, FeatureTool::DatumPoint => strings::TOOL_DATUM_POINT, @@ -5838,6 +6456,8 @@ const fn feature_tool_icon(tool: FeatureTool) -> IconId { FeatureTool::ExtrudedCut => IconId::ExtrudedCut, FeatureTool::RevolvedBossBase => IconId::RevolvedBossBase, FeatureTool::RevolvedCut => IconId::RevolvedCut, + FeatureTool::SweptBossBase => IconId::SweptBossBase, + FeatureTool::SweptCut => IconId::SweptCut, FeatureTool::DatumPlane => IconId::TreePlane, FeatureTool::DatumAxis => IconId::Line, FeatureTool::DatumPoint => IconId::Point, @@ -7014,7 +7634,7 @@ mod tests { #[test] fn features_tab_renders_extrude_group_only_when_selected() { let document = sample_document(); - let size = layout_size(1280.0, 800.0); + let size = layout_size(1920.0, 800.0); let sketch_view = render_with(Theme::light(), size, &document, &Mode::Idle); assert!( label_rect(&sketch_view.paints, strings::TOOL_EXTRUDED_BOSS_BASE).is_none(), diff --git a/crates/bone-app/src/sketch_mode.rs b/crates/bone-app/src/sketch_mode.rs index d3bdfa6..df84812 100644 --- a/crates/bone-app/src/sketch_mode.rs +++ b/crates/bone-app/src/sketch_mode.rs @@ -3,11 +3,11 @@ use core::num::NonZeroU32; use bone_document::{ AxisRecipe, CsysAxisRef, CsysRecipe, DatumFeature, ExtrudeEndCondition, ExtrudeFeature, ExtrudeOperation, PlaneRecipe, PointRecipe, RevolveAngle, RevolveAxis, RevolveFeature, Sketch3, - Sketch3Entity, Sketch3Relation, SketchDimension, + Sketch3Entity, Sketch3Relation, SketchDimension, SweepFeature, SweepPath, }; use bone_types::{ DatumId, DatumTarget, ExtrudeId, FeatureId, Length, Point2, Point3, PositiveLength, RevolveId, - SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, + SketchEntityId, SketchId, SketchPlaneBasis, SweepId, Tolerance, UnitVec3, }; use bone_ui::hotkey::ActionId; use uom::si::length::millimeter; @@ -57,6 +57,8 @@ pub enum FeatureTool { ExtrudedCut, RevolvedBossBase, RevolvedCut, + SweptBossBase, + SweptCut, DatumPlane, DatumAxis, DatumPoint, @@ -70,6 +72,8 @@ impl FeatureTool { Self::ExtrudedCut, Self::RevolvedBossBase, Self::RevolvedCut, + Self::SweptBossBase, + Self::SweptCut, Self::DatumPlane, Self::DatumAxis, Self::DatumPoint, @@ -81,6 +85,8 @@ impl FeatureTool { pub const REVOLVE: &'static [Self] = &[Self::RevolvedBossBase, Self::RevolvedCut]; + pub const SWEEP: &'static [Self] = &[Self::SweptBossBase, Self::SweptCut]; + pub const DATUM: &'static [Self] = &[ Self::DatumPlane, Self::DatumAxis, @@ -220,6 +226,41 @@ pub fn revolve_feature( } } +#[derive(Clone, Debug, PartialEq)] +pub enum SweepArming { + Profile { + feature: Box, + target: Option, + }, + AwaitingSketch(ExtrudeOperation), +} + +impl SweepArming { + #[must_use] + pub fn armed(profile: SketchId, operation: ExtrudeOperation) -> Self { + Self::Profile { + feature: Box::new(sweep_feature(profile, operation)), + target: None, + } + } + + #[must_use] + pub fn operation(&self) -> ExtrudeOperation { + match self { + Self::Profile { feature, .. } => feature.operation, + Self::AwaitingSketch(operation) => *operation, + } + } +} + +#[must_use] +pub fn sweep_feature(profile: SketchId, operation: ExtrudeOperation) -> SweepFeature { + SweepFeature { + operation, + ..SweepFeature::boss(profile, SweepPath::Sketch(SketchId::default())) + } +} + const DEFAULT_DATUM_OFFSET_MM: f64 = 10.0; #[must_use] @@ -557,6 +598,7 @@ pub enum Mode { }, Extrude(ExtrudeArming), Revolve(RevolveArming), + Sweep(SweepArming), Datum(DatumArming), } @@ -611,6 +653,14 @@ impl Mode { }), exit: ModeExit::Dialog, }, + Self::Sweep(arming) => ModeLifecycle { + button: Some(if arming.operation().is_cut() { + FeatureTool::SweptCut + } else { + FeatureTool::SweptBossBase + }), + exit: ModeExit::Dialog, + }, Self::Datum(arming) => ModeLifecycle { button: Some(datum_button(&arming.feature)), exit: ModeExit::Dialog, @@ -659,7 +709,7 @@ impl Mode { sketch_id, session: Box::new(f(*session)), }, - Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Datum(_) => self, + Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Sweep(_) | Self::Datum(_) => self, } } @@ -710,7 +760,7 @@ impl Mode { pub fn spline_draft(&self) -> &[SplineDraftPoint] { match self { Self::Sketch { session, .. } => &session.spline_draft, - Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Datum(_) => &[], + Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Sweep(_) | Self::Datum(_) => &[], } } @@ -807,6 +857,11 @@ impl Mode { matches!(self, Self::Revolve(_)) } + #[must_use] + pub const fn is_sweep(&self) -> bool { + matches!(self, Self::Sweep(_)) + } + #[must_use] pub const fn is_datum(&self) -> bool { matches!(self, Self::Datum(_)) @@ -816,7 +871,7 @@ impl Mode { pub fn sketch_id(&self) -> Option { match self { Self::Sketch { sketch_id, .. } => Some(*sketch_id), - Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Datum(_) => None, + Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Sweep(_) | Self::Datum(_) => None, } } @@ -840,7 +895,7 @@ impl Mode { pub fn dim_flow(&self) -> Option { match self { Self::Sketch { session, .. } => session.dim_flow, - Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Datum(_) => None, + Self::Idle | Self::Extrude(_) | Self::Revolve(_) | Self::Sweep(_) | Self::Datum(_) => None, } } @@ -957,6 +1012,8 @@ mod tests { FeatureTool::ExtrudedCut, FeatureTool::RevolvedBossBase, FeatureTool::RevolvedCut, + FeatureTool::SweptBossBase, + FeatureTool::SweptCut, FeatureTool::DatumPlane, FeatureTool::DatumAxis, FeatureTool::DatumPoint, @@ -972,6 +1029,10 @@ mod tests { FeatureTool::REVOLVE, &[FeatureTool::RevolvedBossBase, FeatureTool::RevolvedCut], ); + assert_eq!( + FeatureTool::SWEEP, + &[FeatureTool::SweptBossBase, FeatureTool::SweptCut], + ); assert!(FeatureTool::DATUM.iter().all(|tool| tool.is_datum())); assert!( FeatureTool::EXTRUDE.iter().all(|tool| !tool.is_datum()), 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 866a6b3..5c1a6a6 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 @@ -6,6 +6,7 @@ action=1 chord=Esc scope=Datum action=1 chord=Esc scope=Extrude action=1 chord=Esc scope=Revolve action=1 chord=Esc scope=Sketch +action=1 chord=Esc scope=Sweep action=10 chord=Ctrl+N scope=Global action=11 chord=Ctrl+O scope=Global action=12 chord=Ctrl+S scope=Global diff --git a/crates/bone-app/src/status_badge.rs b/crates/bone-app/src/status_badge.rs index 3a7162a..0a0080b 100644 --- a/crates/bone-app/src/status_badge.rs +++ b/crates/bone-app/src/status_badge.rs @@ -202,6 +202,11 @@ fn truck_gap_key(gap: TruckGap) -> StringKey { TruckGap::RevolvePolePartial | TruckGap::RevolvePoleThin => { strings::REVOLVE_PANEL_UNSUPPORTED_POLE } + TruckGap::SweepGeneralPath => strings::SWEEP_PANEL_GENERAL_PATH, + TruckGap::SweepProfileKind => strings::SWEEP_PANEL_PROFILE_KIND, + TruckGap::SweepOrientation => strings::SWEEP_PANEL_ORIENTATION, + TruckGap::SweepProfileOffPath => strings::SWEEP_PANEL_OFF_PATH, + TruckGap::SweepThinWall => strings::SWEEP_PANEL_THIN, } } diff --git a/crates/bone-app/src/strings.rs b/crates/bone-app/src/strings.rs index 8f580e9..1389dfd 100644 --- a/crates/bone-app/src/strings.rs +++ b/crates/bone-app/src/strings.rs @@ -318,6 +318,30 @@ pub const REVOLVE_AXIS_3D_SKETCH_LINE: StringKey = StringKey::new("revolve.axis. pub const REVOLVE_CONTOUR_SELECT: StringKey = StringKey::new("revolve.contour.select"); pub const REVOLVE_CONTOUR_ALL: StringKey = StringKey::new("revolve.contour.all"); pub const REVOLVE_CONTOUR_ONE: StringKey = StringKey::new("revolve.contour.one"); +pub const RIBBON_GROUP_SWEEP: StringKey = StringKey::new("shell.ribbon.group.sweep"); +pub const TOOL_SWEPT_BOSS_BASE: StringKey = StringKey::new("tool.swept_boss_base"); +pub const TOOL_SWEPT_CUT: StringKey = StringKey::new("tool.swept_cut"); +pub const STATUS_SWEEP_ACTIVE: StringKey = StringKey::new("status.sweep_active"); +pub const PROPERTY_PANE_SWEEP_TITLE: StringKey = StringKey::new("property.pane.sweep.title"); +pub const PROPERTY_GROUP_SWEEP_PROFILE_PATH: StringKey = + StringKey::new("property.group.sweep.profile_path"); +pub const PROPERTY_GROUP_SWEEP_OPTIONS: StringKey = StringKey::new("property.group.sweep.options"); +pub const PROPERTY_ROW_SWEEP_PROFILE: StringKey = StringKey::new("property.row.sweep.profile"); +pub const PROPERTY_ROW_SWEEP_PATH: StringKey = StringKey::new("property.row.sweep.path"); +pub const PROPERTY_ROW_SWEEP_ORIENTATION: StringKey = + StringKey::new("property.row.sweep.orientation"); +pub const PROPERTY_ROW_SWEEP_PROFILE_KIND: StringKey = + StringKey::new("property.row.sweep.profile_kind"); +pub const PROPERTY_ROW_SWEEP_DIAMETER: StringKey = StringKey::new("property.row.sweep.diameter"); +pub const SWEEP_PROFILE_SKETCH: StringKey = StringKey::new("sweep.profile.sketch"); +pub const SWEEP_PATH_SELECT: StringKey = StringKey::new("sweep.path.select"); +pub const SWEEP_PATH_SKETCH: StringKey = StringKey::new("sweep.path.sketch"); +pub const SWEEP_PATH_EDGE: StringKey = StringKey::new("sweep.path.edge"); +pub const SWEEP_ORIENTATION_FOLLOW: StringKey = StringKey::new("sweep.orientation.follow"); +pub const SWEEP_ORIENTATION_KEEP_NORMAL: StringKey = + StringKey::new("sweep.orientation.keep_normal"); +pub const SWEEP_PROFILE_KIND_SKETCH: StringKey = StringKey::new("sweep.profile_kind.sketch"); +pub const SWEEP_PROFILE_KIND_CIRCULAR: StringKey = StringKey::new("sweep.profile_kind.circular"); pub const RIBBON_GROUP_DATUM: StringKey = StringKey::new("shell.ribbon.group.datum"); pub const RIBBON_GROUP_SKETCH3: StringKey = StringKey::new("shell.ribbon.group.sketch3"); pub const TOOL_SKETCH3: StringKey = StringKey::new("tool.sketch3"); @@ -437,6 +461,11 @@ pub const EXTRUDE_PANEL_REVOLVE_MIDPLANE: StringKey = StringKey::new("extrude.panel.revolve_midplane"); pub const REVOLVE_PANEL_UNSUPPORTED_POLE: StringKey = StringKey::new("revolve.panel.unsupported_pole"); +pub const SWEEP_PANEL_GENERAL_PATH: StringKey = StringKey::new("sweep.panel.general_path"); +pub const SWEEP_PANEL_PROFILE_KIND: StringKey = StringKey::new("sweep.panel.profile_kind"); +pub const SWEEP_PANEL_ORIENTATION: StringKey = StringKey::new("sweep.panel.orientation"); +pub const SWEEP_PANEL_OFF_PATH: StringKey = StringKey::new("sweep.panel.off_path"); +pub const SWEEP_PANEL_THIN: StringKey = StringKey::new("sweep.panel.thin"); pub const EXTRUDE_PANEL_DANGLING_REFERENCE: StringKey = StringKey::new("extrude.panel.dangling_reference"); pub const EXTRUDE_PANEL_NON_PLANAR_TARGET: StringKey = @@ -825,6 +854,26 @@ const EN_US: &[(StringKey, &str)] = &[ (REVOLVE_CONTOUR_SELECT, "Select a contour"), (REVOLVE_CONTOUR_ALL, "All contours"), (REVOLVE_CONTOUR_ONE, "Selected contour"), + (RIBBON_GROUP_SWEEP, "Sweep"), + (TOOL_SWEPT_BOSS_BASE, "Swept Boss/Base"), + (TOOL_SWEPT_CUT, "Swept Cut"), + (STATUS_SWEEP_ACTIVE, "Sweep"), + (PROPERTY_PANE_SWEEP_TITLE, "Sweep"), + (PROPERTY_GROUP_SWEEP_PROFILE_PATH, "Profile and Path"), + (PROPERTY_GROUP_SWEEP_OPTIONS, "Options"), + (PROPERTY_ROW_SWEEP_PROFILE, "Profile"), + (PROPERTY_ROW_SWEEP_PATH, "Path"), + (PROPERTY_ROW_SWEEP_ORIENTATION, "Profile Orientation"), + (PROPERTY_ROW_SWEEP_PROFILE_KIND, "Profile Type"), + (PROPERTY_ROW_SWEEP_DIAMETER, "Diameter"), + (SWEEP_PROFILE_SKETCH, "Sketch profile"), + (SWEEP_PATH_SELECT, "Select a path"), + (SWEEP_PATH_SKETCH, "Sketch"), + (SWEEP_PATH_EDGE, "Model edge"), + (SWEEP_ORIENTATION_FOLLOW, "Follow path"), + (SWEEP_ORIENTATION_KEEP_NORMAL, "Keep normal constant"), + (SWEEP_PROFILE_KIND_SKETCH, "Sketch profile"), + (SWEEP_PROFILE_KIND_CIRCULAR, "Circular profile"), (RIBBON_GROUP_DATUM, "Reference Geometry"), (RIBBON_GROUP_SKETCH3, "3D Sketch"), (TOOL_SKETCH3, "3D Sketch"), @@ -966,6 +1015,26 @@ const EN_US: &[(StringKey, &str)] = &[ REVOLVE_PANEL_UNSUPPORTED_POLE, "An axis-touching profile revolves only as a full solid turn", ), + ( + SWEEP_PANEL_GENERAL_PATH, + "Sweep path must be a single straight or circular segment", + ), + ( + SWEEP_PANEL_PROFILE_KIND, + "Circular sweep profile is only supported along a straight path", + ), + ( + SWEEP_PANEL_ORIENTATION, + "Only the follow path profile orientation is supported", + ), + ( + SWEEP_PANEL_OFF_PATH, + "Sweep profile must be perpendicular to the path", + ), + ( + SWEEP_PANEL_THIN, + "Thin-wall sweep is only supported along a perpendicular path", + ), ( EXTRUDE_PANEL_DANGLING_REFERENCE, "Sketch plane references a face that no longer exists", @@ -1376,6 +1445,26 @@ const AR_XB: &[(StringKey, &str)] = &[ (REVOLVE_CONTOUR_SELECT, "[!! Sêlect a contôur !!]"), (REVOLVE_CONTOUR_ALL, "[!! All contôurs !!]"), (REVOLVE_CONTOUR_ONE, "[!! Sêlected contôur !!]"), + (RIBBON_GROUP_SWEEP, "[!! Swêep !!]"), + (TOOL_SWEPT_BOSS_BASE, "[!! Swêpt Bôss/Base !!]"), + (TOOL_SWEPT_CUT, "[!! Swêpt Cût !!]"), + (STATUS_SWEEP_ACTIVE, "[!! Swêep !!]"), + (PROPERTY_PANE_SWEEP_TITLE, "[!! Swêep !!]"), + (PROPERTY_GROUP_SWEEP_PROFILE_PATH, "[!! Prôfile and Pâth !!]"), + (PROPERTY_GROUP_SWEEP_OPTIONS, "[!! Ôptions !!]"), + (PROPERTY_ROW_SWEEP_PROFILE, "[!! Prôfile !!]"), + (PROPERTY_ROW_SWEEP_PATH, "[!! Pâth !!]"), + (PROPERTY_ROW_SWEEP_ORIENTATION, "[!! Prôfile Ôrientation !!]"), + (PROPERTY_ROW_SWEEP_PROFILE_KIND, "[!! Prôfile Týpe !!]"), + (PROPERTY_ROW_SWEEP_DIAMETER, "[!! Dîameter !!]"), + (SWEEP_PROFILE_SKETCH, "[!! Skêtch prôfile !!]"), + (SWEEP_PATH_SELECT, "[!! Sêlect a pâth !!]"), + (SWEEP_PATH_SKETCH, "[!! Skêtch !!]"), + (SWEEP_PATH_EDGE, "[!! Môdel êdge !!]"), + (SWEEP_ORIENTATION_FOLLOW, "[!! Fôllow pâth !!]"), + (SWEEP_ORIENTATION_KEEP_NORMAL, "[!! Kêep nôrmal cônstant !!]"), + (SWEEP_PROFILE_KIND_SKETCH, "[!! Skêtch prôfile !!]"), + (SWEEP_PROFILE_KIND_CIRCULAR, "[!! Cîrcular prôfile !!]"), (RIBBON_GROUP_DATUM, "[!! ʁeference Gêometry !!]"), (RIBBON_GROUP_SKETCH3, "[!! 3D Skêtch !!]"), (TOOL_SKETCH3, "[!! 3D Skêtch !!]"), @@ -1538,6 +1627,26 @@ const AR_XB: &[(StringKey, &str)] = &[ REVOLVE_PANEL_UNSUPPORTED_POLE, "[!! An âxis-touching prôfile revôlves ônly as a fûll sôlid tûrn !!]", ), + ( + SWEEP_PANEL_GENERAL_PATH, + "[!! Swêep pâth mûst be a sîngle strâight or cîrcular sêgment !!]", + ), + ( + SWEEP_PANEL_PROFILE_KIND, + "[!! Cîrcular swêep prôfile is ônly suppôrted along a strâight pâth !!]", + ), + ( + SWEEP_PANEL_ORIENTATION, + "[!! Ônly the fôllow pâth prôfile oriêntation is suppôrted !!]", + ), + ( + SWEEP_PANEL_OFF_PATH, + "[!! Swêep prôfile mûst be perpendîcular to the pâth !!]", + ), + ( + SWEEP_PANEL_THIN, + "[!! Thîn-wâll swêep is ônly suppôrted along a perpendîcular pâth !!]", + ), ( EXTRUDE_PANEL_DANGLING_REFERENCE, "[!! Skêtch plâne refêrences a fâce that nô longer exîsts !!]", diff --git a/crates/bone-app/src/tools/geometry.rs b/crates/bone-app/src/tools/geometry.rs index deda830..f78a307 100644 --- a/crates/bone-app/src/tools/geometry.rs +++ b/crates/bone-app/src/tools/geometry.rs @@ -255,15 +255,20 @@ pub(super) fn ellipse_outline_segments( let (cx, cy) = center.coords_mm(); let (mx, my) = major.coords_mm(); let (nx, ny) = minor.coords_mm(); - let a = (mx - cx, my - cy); - let b = (nx - cx, ny - cy); - let n = 48u16; - let sample = |i: u16| -> Point2 { - let t = std::f64::consts::TAU * f64::from(i) / f64::from(n); - let (c, s) = (t.cos(), t.sin()); - Point2::from_mm(cx + a.0 * c + b.0 * s, cy + a.1 * c + b.1 * s) + let major_axis = (mx - cx, my - cy); + let minor_axis = (nx - cx, ny - cy); + let steps = 48u16; + let sample = |index: u16| -> Point2 { + let angle = std::f64::consts::TAU * f64::from(index) / f64::from(steps); + let (cos, sin) = (angle.cos(), angle.sin()); + Point2::from_mm( + cx + major_axis.0 * cos + minor_axis.0 * sin, + cy + major_axis.1 * cos + minor_axis.1 * sin, + ) }; - (0..n).map(|i| (sample(i), sample((i + 1) % n))).collect() + (0..steps) + .map(|index| (sample(index), sample((index + 1) % steps))) + .collect() } pub(super) fn degenerate_triangle(p1: Point2, p2: Point2, p3: Point2) -> bool { diff --git a/crates/bone-types/src/icon.rs b/crates/bone-types/src/icon.rs index f7b5ddb..be66d51 100644 --- a/crates/bone-types/src/icon.rs +++ b/crates/bone-types/src/icon.rs @@ -68,6 +68,8 @@ icon_ids! { Centerline, Spline, Ellipse, + SweptBossBase, + SweptCut, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] diff --git a/crates/bone-types/src/lib.rs b/crates/bone-types/src/lib.rs index 295f74c..dcc3b7d 100644 --- a/crates/bone-types/src/lib.rs +++ b/crates/bone-types/src/lib.rs @@ -123,6 +123,7 @@ slotmap::new_key_type! { pub struct SketchParameterId; pub struct ExtrudeId; pub struct RevolveId; + pub struct SweepId; pub struct DatumId; pub struct BodyId; pub struct BrepShellId; @@ -151,6 +152,7 @@ impl_as_u64!( SketchId, ExtrudeId, RevolveId, + SweepId, DatumId, BrepEdgeId, BrepVertexId diff --git a/crates/bone-ui/src/hotkey.rs b/crates/bone-ui/src/hotkey.rs index ff263ee..3869b58 100644 --- a/crates/bone-ui/src/hotkey.rs +++ b/crates/bone-ui/src/hotkey.rs @@ -43,6 +43,7 @@ pub enum HotkeyScope { Sketch, Extrude, Revolve, + Sweep, Datum, Modal, TextInput, -- 2.51.2