diff --git a/src/app/block_actions.rs b/src/app/block_actions.rs index 5a8c77cf2..f73e10c36 100644 --- a/src/app/block_actions.rs +++ b/src/app/block_actions.rs @@ -95,7 +95,7 @@ impl OpenCADStudio { }; if !args.is_empty() && kind.is_none() { self.command_line.push_error( - crate::t!("Supported actions: Move, Scale, Stretch, Rotate, Flip, Array. Other action types are not wired yet.") + crate::t!("Supported actions: Move, Scale, Stretch, Polar Stretch, Rotate, Flip, Array. Other action types are not wired yet.") .as_ref(), ); return Task::none(); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index d8c349f5e..1419e02f9 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -181,7 +181,10 @@ impl OpenCADStudio { return Task::none(); } } - if matches!(cmd.split(' ').next(), Some("BACTION" | "BACTIONTOOL" | "BARRAY")) { + if matches!( + cmd.split(' ').next(), + Some("BACTION" | "BACTIONTOOL" | "BARRAY") + ) { let args = if cmd == "BARRAY" || cmd.starts_with("BARRAY ") { "ARRAY" } else { @@ -190,7 +193,7 @@ impl OpenCADStudio { if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() { self.command_line.push_error( crate::t!( - "Supported actions: Move, Scale, Stretch, Rotate, Flip, Array. Other action types are not wired yet." + "Supported actions: Move, Scale, Stretch, Polar Stretch, Rotate, Flip, Array. Other action types are not wired yet." ) .as_ref(), ); diff --git a/src/app/dock/view.rs b/src/app/dock/view.rs index fe8c0c544..c0f722de0 100644 --- a/src/app/dock/view.rs +++ b/src/app/dock/view.rs @@ -416,6 +416,7 @@ impl OpenCADStudio { self.action_error(Some(crate::scene::block_actions::Kind::Rotate)), self.action_error(Some(crate::scene::block_actions::Kind::Flip)), self.action_error(Some(crate::scene::block_actions::Kind::Stretch)), + self.action_error(Some(crate::scene::block_actions::Kind::PolarStretch)), self.action_error(Some(crate::scene::block_actions::Kind::Array)), ) } diff --git a/src/modules/block_editor/action.rs b/src/modules/block_editor/action.rs index a576f561d..510ed1671 100644 --- a/src/modules/block_editor/action.rs +++ b/src/modules/block_editor/action.rs @@ -49,7 +49,7 @@ impl ActionCommand { ); } self.parameter = Some(p.handle); - self.selecting = self.kind != Some(Kind::Stretch); + self.selecting = !self.kind.is_some_and(actions::needs_frame); CmdResult::DeselectAndContinue } } @@ -78,8 +78,10 @@ impl CadCommand for ActionCommand { } fn prompt(&self) -> String { if self.kind.is_none() { - return crate::t!("BACTION Enter action type [Move/Scale/Stretch/Rotate/Flip/Array]:") - .into_owned(); + return crate::t!( + "BACTION Enter action type [Move/Scale/Stretch/Polar Stretch/Rotate/Flip/Array]:" + ) + .into_owned(); } if self.parameter.is_none() { return crate::tf!( @@ -88,11 +90,11 @@ impl CadCommand for ActionCommand { ) .into_owned(); } - if self.kind == Some(Kind::Stretch) && self.frame.is_empty() { - return crate::t!("BACTION Specify first corner of stretch frame:").into_owned(); + if self.kind.is_some_and(actions::needs_frame) && self.frame.is_empty() { + return crate::t!("BACTION Specify first corner of action frame:").into_owned(); } - if self.kind == Some(Kind::Stretch) && self.frame.len() == 1 { - return crate::t!("BACTION Specify opposite corner of stretch frame:").into_owned(); + if self.kind.is_some_and(actions::needs_frame) && self.frame.len() == 1 { + return crate::t!("BACTION Specify opposite corner of action frame:").into_owned(); } if self.selecting { return crate::t!("BACTION Select objects for the action, then press Enter:") @@ -122,6 +124,7 @@ impl CadCommand for ActionCommand { CmdOption::new("Move", "Move"), CmdOption::new("Scale", "Scale"), CmdOption::new("Stretch", "Stretch"), + CmdOption::new("Polar Stretch", "Polar Stretch"), CmdOption::new("Rotate", "Rotate"), CmdOption::new("Flip", "Flip"), CmdOption::new("Array", "Array"), @@ -137,7 +140,8 @@ impl CadCommand for ActionCommand { CmdResult::NeedPoint } else { CmdResult::ReportError( - crate::t!("Choose Move, Scale, Stretch, Rotate, Flip or Array.").into_owned(), + crate::t!("Choose Move, Scale, Stretch, Polar Stretch, Rotate, Flip or Array.") + .into_owned(), ) }); } @@ -224,7 +228,7 @@ impl CadCommand for ActionCommand { ) }); } - if self.kind == Some(Kind::Stretch) && self.frame.len() < 2 { + if self.kind.is_some_and(actions::needs_frame) && self.frame.len() < 2 { let plane = self .candidates .iter() diff --git a/src/modules/block_editor/action_set.rs b/src/modules/block_editor/action_set.rs index 6e9affcc5..b517fff1d 100644 --- a/src/modules/block_editor/action_set.rs +++ b/src/modules/block_editor/action_set.rs @@ -1,6 +1,6 @@ //! Edit native action selections without replacing their graph nodes. use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, PointPickContext}; -use crate::scene::block_actions::{self as actions, Kind}; +use crate::scene::block_actions as actions; use codec::objects::ObjectType; use codec::{CadDocument, Handle}; use glam::DVec3; @@ -48,7 +48,7 @@ impl ActionSetCommand { name: a.element.name.clone(), at: actions::v(a.display_location), targets: a.dependencies.clone(), - stretch: kind == Kind::Stretch, + stretch: actions::needs_frame(kind), expected: doc.objects[&handle].clone(), }) .collect::>(); @@ -108,9 +108,9 @@ impl CadCommand for ActionSetCommand { } else if self.mode.is_none() { "BACTIONSET [New/Add/Remove] :" } else if matches!(self.mode, Some(Mode::Frame)) && self.frame.is_empty() { - "BACTIONSET Specify first corner of replacement stretch frame:" + "BACTIONSET Specify first corner of replacement action frame:" } else if matches!(self.mode, Some(Mode::Frame)) { - "BACTIONSET Specify opposite corner of replacement stretch frame:" + "BACTIONSET Specify opposite corner of replacement action frame:" } else { "BACTIONSET Select objects, then press Enter:" }) diff --git a/src/scene/block_actions.rs b/src/scene/block_actions.rs index 7b41e2b63..7dcb6d3ba 100644 --- a/src/scene/block_actions.rs +++ b/src/scene/block_actions.rs @@ -3,14 +3,14 @@ use super::block_authoring::{self as authoring, *}; use codec::objects::{ BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockArrayAction, BlockBasePointAction, BlockConnection, BlockElement, BlockEvalValue, BlockFlipAction, - BlockMoveAction, BlockStretchAction, DynamicBlockData, ObjectType, + BlockMoveAction, BlockPolarStretchAction, BlockStretchAction, DynamicBlockData, ObjectType, }; use codec::types::{Transform, Vector3}; use codec::{CadDocument, EntityType, Handle}; use glam::DVec3; pub(crate) mod instance; -mod stretch; mod representation; +mod stretch; #[cfg(test)] mod tests; @@ -21,16 +21,18 @@ pub enum Kind { Rotate, Flip, Stretch, + PolarStretch, Array, } impl Kind { pub(crate) fn parse(s: &str) -> Option { - match s.to_ascii_uppercase().as_str() { + match s.trim().to_ascii_uppercase().as_str() { "MOVE" | "M" => Some(Self::Move), "SCALE" | "S" => Some(Self::Scale), "ROTATE" | "R" => Some(Self::Rotate), "FLIP" | "F" => Some(Self::Flip), "STRETCH" | "ST" => Some(Self::Stretch), + "POLARSTRETCH" | "POLAR STRETCH" | "PS" => Some(Self::PolarStretch), "ARRAY" | "A" => Some(Self::Array), _ => None, } @@ -42,11 +44,17 @@ impl Kind { Self::Rotate => "Rotate", Self::Flip => "Flip", Self::Stretch => "Stretch", + Self::PolarStretch => "Polar Stretch", Self::Array => "Array", } } } +/// Actions that author a rectangular frame before selecting objects. +pub(crate) fn needs_frame(kind: Kind) -> bool { + matches!(kind, Kind::Stretch | Kind::PolarStretch) +} + #[derive(Clone, Debug)] pub(crate) struct Parameter { pub handle: Handle, @@ -58,6 +66,7 @@ pub(crate) struct Parameter { pub linear: bool, pub point: bool, pub xy: bool, + pub polar: bool, pub rotation: bool, pub flip: bool, pub reference: DVec3, @@ -87,6 +96,7 @@ pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool { match kind { Kind::Move => !p.rotation && !p.flip, Kind::Stretch => !p.rotation && !p.flip && !p.point, + Kind::PolarStretch => p.polar, Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy, Kind::Rotate => p.rotation, Kind::Flip => p.flip, @@ -208,6 +218,7 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V linear: false, point: true, xy: false, + polar: false, rotation: false, flip: false, reference: DVec3::ZERO, @@ -218,10 +229,14 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V if matches!(kind, Kind::Rotate | Kind::Flip) { continue; } + let polar = p.kind == super::block_distance_parameter::Kind::Polar; let xy = p.kind == super::block_distance_parameter::Kind::XY; if kind == Kind::Scale && xy { continue; } + if kind == Kind::PolarStretch && !polar { + continue; + } result.push(Parameter { handle: p.handle, id: p.native.parameter.element.eval.node_id, @@ -232,6 +247,7 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V linear: p.kind == super::block_distance_parameter::Kind::Linear, point: false, xy, + polar, rotation: false, flip: false, reference: DVec3::ZERO, @@ -255,6 +271,7 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V linear: false, point: false, xy: false, + polar: false, rotation: kind == Kind::Rotate, flip: kind == Kind::Flip, reference: v(p.reference), @@ -361,7 +378,9 @@ pub(crate) fn create( targets: &[Handle], location: DVec3, ) -> Result { - create_with_options(doc, definition, kind, parameter, targets, location, None, None) + create_with_options( + doc, definition, kind, parameter, targets, location, None, None, + ) } #[cfg(test)] @@ -396,7 +415,9 @@ pub(crate) fn create_with_frame( location: DVec3, frame: Option<[DVec3; 2]>, ) -> Result { - create_with_options(doc, definition, kind, parameter, targets, location, frame, None) + create_with_options( + doc, definition, kind, parameter, targets, location, frame, None, + ) } pub(crate) fn create_with_options( @@ -458,6 +479,19 @@ pub(crate) fn create_with_options( } else { None }; + let polar_binding = if kind == Kind::PolarStretch { + if (location.z - p.end.z).abs() > 1e-8 { + return Err("Place the Polar Stretch marker in the parameter's XY plane."); + } + Some(stretch::bind( + doc, + targets, + frame.ok_or("Specify a Polar Stretch frame before selecting objects.")?, + p.end.z, + )?) + } else { + None + }; let mut staged = doc.clone(); let (graph, slots) = authoring::prepare(&mut staged, definition)?; let (dxf, cpp) = match kind { @@ -466,6 +500,7 @@ pub(crate) fn create_with_options( Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"), Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"), Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"), + Kind::PolarStretch => ("BLOCKPOLARSTRETCHACTION", "AcDbBlockPolarStretchAction"), Kind::Array => ("BLOCKARRAYACTION", "AcDbBlockArrayAction"), }; register_classes(&mut staged, &[(dxf, cpp, 55)]); @@ -482,7 +517,7 @@ pub(crate) fn create_with_options( eed_1071: 0, }, display_location: native(location), - dependencies, + dependencies: dependencies.clone(), action_ids: vec![], }; let connection = |name: &str| BlockConnection { @@ -516,6 +551,31 @@ pub(crate) fn create_with_options( }, }) } + Kind::PolarStretch => { + let (points, bindings) = polar_binding.unwrap(); + DynamicBlockData::PolarStretchAction(BlockPolarStretchAction { + action, + connections: std::array::from_fn(|i| BlockConnection { + code: p.id, + name: [ + "BaseXDelta", + "BaseYDelta", + "Base", + "End", + "UpdatedBase", + "UpdatedEnd", + ][i] + .into(), + }), + points, + handles: vec![], + bindings, + codes: vec![], + distance_multiplier: 1., + angle_offset: 0., + extra: vec![], + }) + } Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction { action, connections: if p.point { @@ -575,7 +635,6 @@ pub(crate) fn create_with_options( Ok(handle) } - /// Retarget an existing native action, retaining identity, graph and options. pub(crate) fn retarget( doc: &mut CadDocument, @@ -642,6 +701,33 @@ pub(crate) fn retarget( } a.action.dependencies = targets; } + DynamicBlockData::PolarStretchAction(a) => { + stretch::validate_polar(doc, a)?; + let plane = parameters(doc, definition, Kind::PolarStretch) + .into_iter() + .find(|p| p.id == id) + .ok_or("The Polar Stretch parameter is unavailable.")? + .end + .z; + if let Some(corners) = frame { + let (points, bindings) = stretch::bind(doc, &targets, corners, plane)?; + a.points = points; + a.bindings = bindings; + } else { + a.bindings.retain(|b| targets.contains(&b.handle)); + let added = targets + .iter() + .copied() + .filter(|h| !old_targets.contains(h)) + .collect::>(); + let frame = a.points.clone(); + a.bindings + .extend(stretch::bind_existing(doc, &added, &frame, plane)?); + a.bindings.sort_by_key(|b| b.handle); + } + a.handles.retain(|h| targets.contains(h)); + a.action.dependencies = targets; + } DynamicBlockData::MoveAction(a) => a.action.dependencies = targets, DynamicBlockData::FlipAction(a) => a.action.dependencies = targets, DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { @@ -650,8 +736,8 @@ pub(crate) fn retarget( DynamicBlockData::ArrayAction(a) => a.action.dependencies = targets, _ => return Err("This action's selection cannot be edited yet."), } - if frame.is_some() && kind != Kind::Stretch { - return Err("Only Stretch actions have a stretch frame."); + if frame.is_some() && !needs_frame(kind) { + return Err("Only Stretch actions have an action frame."); } if staged == *doc { return Ok(false); @@ -689,6 +775,12 @@ pub(crate) fn actions( DynamicBlockData::StretchAction(a) => { Some((o.handle, &a.action, Kind::Stretch, a.connections[0].code)) } + DynamicBlockData::PolarStretchAction(a) => Some(( + o.handle, + &a.action, + Kind::PolarStretch, + a.connections[0].code, + )), DynamicBlockData::FlipAction(a) => { Some((o.handle, &a.action, Kind::Flip, a.connections[0].code)) } @@ -730,6 +822,11 @@ pub(crate) fn prune_erased_targets(doc: &mut CadDocument, definition: Handle) { a.handles.retain(|b| live.contains(&b.handle)); &mut a.action } + DynamicBlockData::PolarStretchAction(a) => { + a.handles.retain(|h| live.contains(h)); + a.bindings.retain(|b| live.contains(&b.handle)); + &mut a.action + } DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { &mut a.action.action } @@ -744,6 +841,9 @@ pub(crate) fn frame_outline(doc: &CadDocument, handle: Handle) -> Vec { return vec![]; }; let DynamicBlockData::StretchAction(a) = &o.data else { + if let DynamicBlockData::PolarStretchAction(a) = &o.data { + return stretch::outline(&a.points, a.action.display_location.z); + } return vec![]; }; stretch::outline(&a.points, a.action.display_location.z) @@ -776,6 +876,17 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform) }) .collect(); } + DynamicBlockData::PolarStretchAction(a) => { + let plane = a.action.display_location.z; + a.action.display_location = t.apply(a.action.display_location); + a.points = stretch::outline(&a.points, plane) + .into_iter() + .map(|p| { + let p = t.apply(p); + codec::types::Vector2::new(p.x, p.y) + }) + .collect(); + } DynamicBlockData::FlipAction(a) => { a.action.display_location = t.apply(a.action.display_location) } @@ -966,7 +1077,11 @@ fn evaluate_impl( for h in extra_handles { staged.remove_entity(h); } - if let Some(record) = staged.block_records.iter_mut().find(|b| b.handle == definition) { + if let Some(record) = staged + .block_records + .iter_mut() + .find(|b| b.handle == definition) + { record.entity_handles.retain(|h| { baseline_members.contains(h) || *h == record.block_entity_handle @@ -984,6 +1099,14 @@ fn evaluate_impl( unreachable!() }; stretch::validate(baseline, s)?; + } else if *kind == Kind::PolarStretch { + let ObjectType::DynamicBlock(o) = &baseline.objects[h] else { + unreachable!() + }; + let DynamicBlockData::PolarStretchAction(s) = &o.data else { + unreachable!() + }; + stretch::validate_polar(baseline, s)?; } affected.extend(a.dependencies.iter().copied()); } @@ -1012,7 +1135,7 @@ fn evaluate_impl( } let end = current(&staged, &q); let transform = match kind { - Kind::Move | Kind::Stretch => { + Kind::Move | Kind::Stretch | Kind::PolarStretch => { crate::command::EntityTransform::Translate(end - q.end) } Kind::Rotate => { @@ -1134,6 +1257,15 @@ fn evaluate_impl( } if let DynamicBlockData::StretchAction(s) = &o.data { stretch::apply(&mut entity, s, end - q.end)?; + } else if let DynamicBlockData::PolarStretchAction(s) = &o.data { + if s.connections.iter().any(|c| c.code != q.id) { + return Err("This Polar Stretch action uses a different parameter binding."); + } + let direction = q.end - q.base; + if direction.length() <= 1e-9 { + return Err("The Polar parameter needs a nonzero distance."); + } + stretch::apply_polar(&mut entity, &s.bindings, q.base, q.end, end)?; } else { super::view::dispatch::apply_transform(&mut entity, &transform); } @@ -1262,7 +1394,6 @@ fn evaluate_impl( Ok(true) } - fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else { return false; diff --git a/src/scene/block_actions/stretch.rs b/src/scene/block_actions/stretch.rs index 4c47cca45..303f17ca3 100644 --- a/src/scene/block_actions/stretch.rs +++ b/src/scene/block_actions/stretch.rs @@ -1,6 +1,6 @@ //! A native Stretch action freezes its vertex membership at authoring time. use super::*; -use codec::objects::{BlockStretchAction, BlockStretchHandle}; +use codec::objects::{BlockPolarStretchAction, BlockStretchAction, BlockStretchHandle}; use codec::types::Vector2; pub(crate) fn control_points(e: &EntityType) -> Result, &'static str> { @@ -160,6 +160,75 @@ pub(crate) fn validate(doc: &CadDocument, a: &BlockStretchAction) -> Result<(), Ok(()) } +pub(crate) fn validate_polar( + doc: &CadDocument, + a: &BlockPolarStretchAction, +) -> Result<(), &'static str> { + let connection_code = a.connections[0].code; + let connections = [ + "BaseXDelta", + "BaseYDelta", + "Base", + "End", + "UpdatedBase", + "UpdatedEnd", + ] + .map(|name| codec::objects::BlockConnection { + code: connection_code, + name: name.into(), + }); + if a.distance_multiplier != 1. + || a.angle_offset != 0. + || a.connections != connections + || !a.codes.is_empty() + || !a.extra.is_empty() + || !a.action.action_ids.is_empty() + || a.points.len() < 2 + || a.points + .iter() + .any(|p| !p.x.is_finite() || !p.y.is_finite()) + { + return Err("This Polar Stretch action uses options not supported yet."); + } + let mut handles = a.handles.clone(); + handles.sort(); + handles.dedup(); + if handles != a.handles || handles.iter().any(|h| !a.action.dependencies.contains(h)) { + return Err("The Polar Stretch action has invalid target bindings."); + } + let mut seen = std::collections::BTreeSet::new(); + for binding in &a.bindings { + if !a.action.dependencies.contains(&binding.handle) || !seen.insert(binding.handle) { + return Err("The Polar Stretch action has invalid vertex bindings."); + } + let count = control_points( + doc.get_entity(binding.handle) + .ok_or("A Polar Stretch target is unavailable.")?, + )? + .len(); + let unique = binding + .indexes + .iter() + .copied() + .collect::>(); + if unique.len() != binding.indexes.len() + || binding + .indexes + .iter() + .any(|i| *i < 0 || *i as usize >= count) + { + return Err("A Polar Stretch vertex binding no longer matches its object."); + } + } + for h in &a.action.dependencies { + control_points( + doc.get_entity(*h) + .ok_or("A Polar Stretch target is unavailable.")?, + )?; + } + Ok(()) +} + pub(crate) fn apply( e: &mut EntityType, a: &BlockStretchAction, @@ -224,6 +293,26 @@ pub(crate) fn apply( Ok(()) } +/// Apply the authored polar displacement to only the vertices frozen in the +/// Polar Stretch bindings; targets without a partial binding move as a whole. +pub(crate) fn apply_polar( + e: &mut EntityType, + bindings: &[BlockStretchHandle], + base: DVec3, + old_end: DVec3, + new_end: DVec3, +) -> Result<(), &'static str> { + let (old_vec, new_vec) = (old_end - base, new_end - base); + if old_vec.length() <= 1e-9 || new_vec.length() <= 1e-9 { + return Err("The Polar parameter needs a nonzero distance."); + } + let action = BlockStretchAction { + handles: bindings.to_vec(), + ..Default::default() + }; + apply(e, &action, new_end - old_end) +} + pub(crate) fn outline(points: &[Vector2], z: f64) -> Vec { let points = if points.len() == 2 { let (a, b) = (points[0], points[1]); diff --git a/src/ui/window/authoring_palettes.rs b/src/ui/window/authoring_palettes.rs index 4cefc576f..2748d90e8 100644 --- a/src/ui/window/authoring_palettes.rs +++ b/src/ui/window/authoring_palettes.rs @@ -6,7 +6,7 @@ use crate::ui::dock::{self, Chrome, PanelId, VerticalLabel}; use crate::ui::icons; use crate::ui::ribbon::{tooltip_content, tooltip_style}; use iced::widget::{ - button, canvas, column, container, rule, row, scrollable, stack, text, tooltip, Space, + button, canvas, column, container, row, rule, scrollable, stack, text, tooltip, Space, }; use iced::{Background, Border, Element, Fill, Theme}; @@ -199,6 +199,7 @@ fn catalog( rotate_error: Option<&'static str>, flip_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>, + polar_stretch_action_error: Option<&'static str>, array_action_error: Option<&'static str>, ) -> Element<'static, Message> { let (entries, unavailable) = match tab { @@ -221,8 +222,8 @@ fn catalog( let orientation_error = if *label=="Flip" {flip_error} else {point_error}; let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint"); let lookup_action=tab==Tab::Actions&&*label=="Lookup"; - let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip"|"Array"); - let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,"Array"=>array_action_error,_=>move_error}; + let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Polar Stretch"|"Rotate"|"Flip"|"Array"); + let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Polar Stretch"=>polar_stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,"Array"=>array_action_error,_=>move_error}; let enabled = (geometry_action&&action_error.is_none())||(single&&point_error.is_none())||(lookup_action&&lookup_error.is_none())||(orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none()); let base = if enabled { icons::semantic(icon, 28.0) @@ -408,6 +409,7 @@ pub fn view( rotate_error: Option<&'static str>, flip_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>, + polar_stretch_action_error: Option<&'static str>, array_action_error: Option<&'static str>, ) -> Element<'static, Message> { let tabs = column(Tab::ALL.into_iter().map(|category| { @@ -469,6 +471,7 @@ pub fn view( rotate_error, flip_action_error, stretch_action_error, + polar_stretch_action_error, array_action_error, )) .width(Fill)