feat:(BEDIT) : add Polar Stretch block action authoring and evaluation

Enable Polar Stretch actions through the authoring palette and BACTION command. Persist native action data, edit target selections and frames, and evaluate parameter-driven displacement on block references.
This commit is contained in:
Karim Jerbi committed 2026-10-17 23:24:20 +01:00
1 parent 4aa4b72d17
commit bbf5afe53d
8 files changed
+263 -32

No files matched your search

+1 -1
View File
@@ -95,7 +95,7 @@ impl OpenCADStudio {
}; };
if !args.is_empty() && kind.is_none() { if !args.is_empty() && kind.is_none() {
self.command_line.push_error( 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(), .as_ref(),
); );
return Task::none(); return Task::none();
+5 -2
View File
@@ -181,7 +181,10 @@ impl OpenCADStudio {
return Task::none(); 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 ") { let args = if cmd == "BARRAY" || cmd.starts_with("BARRAY ") {
"ARRAY" "ARRAY"
} else { } else {
@@ -190,7 +193,7 @@ impl OpenCADStudio {
if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() { if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() {
self.command_line.push_error( self.command_line.push_error(
crate::t!( 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(), .as_ref(),
); );
+1
View File
@@ -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::Rotate)),
self.action_error(Some(crate::scene::block_actions::Kind::Flip)), 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::Stretch)),
self.action_error(Some(crate::scene::block_actions::Kind::PolarStretch)),
self.action_error(Some(crate::scene::block_actions::Kind::Array)), self.action_error(Some(crate::scene::block_actions::Kind::Array)),
) )
} }
+13 -9
View File
@@ -49,7 +49,7 @@ impl ActionCommand {
); );
} }
self.parameter = Some(p.handle); self.parameter = Some(p.handle);
self.selecting = self.kind != Some(Kind::Stretch); self.selecting = !self.kind.is_some_and(actions::needs_frame);
CmdResult::DeselectAndContinue CmdResult::DeselectAndContinue
} }
} }
@@ -78,8 +78,10 @@ impl CadCommand for ActionCommand {
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.kind.is_none() { if self.kind.is_none() {
return crate::t!("BACTION Enter action type [Move/Scale/Stretch/Rotate/Flip/Array]:") return crate::t!(
.into_owned(); "BACTION Enter action type [Move/Scale/Stretch/Polar Stretch/Rotate/Flip/Array]:"
)
.into_owned();
} }
if self.parameter.is_none() { if self.parameter.is_none() {
return crate::tf!( return crate::tf!(
@@ -88,11 +90,11 @@ impl CadCommand for ActionCommand {
) )
.into_owned(); .into_owned();
} }
if self.kind == Some(Kind::Stretch) && self.frame.is_empty() { if self.kind.is_some_and(actions::needs_frame) && self.frame.is_empty() {
return crate::t!("BACTION Specify first corner of stretch frame:").into_owned(); return crate::t!("BACTION Specify first corner of action frame:").into_owned();
} }
if self.kind == Some(Kind::Stretch) && self.frame.len() == 1 { if self.kind.is_some_and(actions::needs_frame) && self.frame.len() == 1 {
return crate::t!("BACTION Specify opposite corner of stretch frame:").into_owned(); return crate::t!("BACTION Specify opposite corner of action frame:").into_owned();
} }
if self.selecting { if self.selecting {
return crate::t!("BACTION Select objects for the action, then press Enter:") 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("Move", "Move"),
CmdOption::new("Scale", "Scale"), CmdOption::new("Scale", "Scale"),
CmdOption::new("Stretch", "Stretch"), CmdOption::new("Stretch", "Stretch"),
CmdOption::new("Polar Stretch", "Polar Stretch"),
CmdOption::new("Rotate", "Rotate"), CmdOption::new("Rotate", "Rotate"),
CmdOption::new("Flip", "Flip"), CmdOption::new("Flip", "Flip"),
CmdOption::new("Array", "Array"), CmdOption::new("Array", "Array"),
@@ -137,7 +140,8 @@ impl CadCommand for ActionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} else { } else {
CmdResult::ReportError( 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 let plane = self
.candidates .candidates
.iter() .iter()
+4 -4
View File
@@ -1,6 +1,6 @@
//! Edit native action selections without replacing their graph nodes. //! Edit native action selections without replacing their graph nodes.
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, PointPickContext}; 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::objects::ObjectType;
use codec::{CadDocument, Handle}; use codec::{CadDocument, Handle};
use glam::DVec3; use glam::DVec3;
@@ -48,7 +48,7 @@ impl ActionSetCommand {
name: a.element.name.clone(), name: a.element.name.clone(),
at: actions::v(a.display_location), at: actions::v(a.display_location),
targets: a.dependencies.clone(), targets: a.dependencies.clone(),
stretch: kind == Kind::Stretch, stretch: actions::needs_frame(kind),
expected: doc.objects[&handle].clone(), expected: doc.objects[&handle].clone(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -108,9 +108,9 @@ impl CadCommand for ActionSetCommand {
} else if self.mode.is_none() { } else if self.mode.is_none() {
"BACTIONSET [New/Add/Remove] <New>:" "BACTIONSET [New/Add/Remove] <New>:"
} else if matches!(self.mode, Some(Mode::Frame)) && self.frame.is_empty() { } 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)) { } 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 { } else {
"BACTIONSET Select objects, then press Enter:" "BACTIONSET Select objects, then press Enter:"
}) })
+143 -12
View File
@@ -3,14 +3,14 @@ use super::block_authoring::{self as authoring, *};
use codec::objects::{ use codec::objects::{
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockArrayAction, BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockArrayAction,
BlockBasePointAction, BlockConnection, BlockElement, BlockEvalValue, BlockFlipAction, BlockBasePointAction, BlockConnection, BlockElement, BlockEvalValue, BlockFlipAction,
BlockMoveAction, BlockStretchAction, DynamicBlockData, ObjectType, BlockMoveAction, BlockPolarStretchAction, BlockStretchAction, DynamicBlockData, ObjectType,
}; };
use codec::types::{Transform, Vector3}; use codec::types::{Transform, Vector3};
use codec::{CadDocument, EntityType, Handle}; use codec::{CadDocument, EntityType, Handle};
use glam::DVec3; use glam::DVec3;
pub(crate) mod instance; pub(crate) mod instance;
mod stretch;
mod representation; mod representation;
mod stretch;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -21,16 +21,18 @@ pub enum Kind {
Rotate, Rotate,
Flip, Flip,
Stretch, Stretch,
PolarStretch,
Array, Array,
} }
impl Kind { impl Kind {
pub(crate) fn parse(s: &str) -> Option<Self> { pub(crate) fn parse(s: &str) -> Option<Self> {
match s.to_ascii_uppercase().as_str() { match s.trim().to_ascii_uppercase().as_str() {
"MOVE" | "M" => Some(Self::Move), "MOVE" | "M" => Some(Self::Move),
"SCALE" | "S" => Some(Self::Scale), "SCALE" | "S" => Some(Self::Scale),
"ROTATE" | "R" => Some(Self::Rotate), "ROTATE" | "R" => Some(Self::Rotate),
"FLIP" | "F" => Some(Self::Flip), "FLIP" | "F" => Some(Self::Flip),
"STRETCH" | "ST" => Some(Self::Stretch), "STRETCH" | "ST" => Some(Self::Stretch),
"POLARSTRETCH" | "POLAR STRETCH" | "PS" => Some(Self::PolarStretch),
"ARRAY" | "A" => Some(Self::Array), "ARRAY" | "A" => Some(Self::Array),
_ => None, _ => None,
} }
@@ -42,11 +44,17 @@ impl Kind {
Self::Rotate => "Rotate", Self::Rotate => "Rotate",
Self::Flip => "Flip", Self::Flip => "Flip",
Self::Stretch => "Stretch", Self::Stretch => "Stretch",
Self::PolarStretch => "Polar Stretch",
Self::Array => "Array", 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)] #[derive(Clone, Debug)]
pub(crate) struct Parameter { pub(crate) struct Parameter {
pub handle: Handle, pub handle: Handle,
@@ -58,6 +66,7 @@ pub(crate) struct Parameter {
pub linear: bool, pub linear: bool,
pub point: bool, pub point: bool,
pub xy: bool, pub xy: bool,
pub polar: bool,
pub rotation: bool, pub rotation: bool,
pub flip: bool, pub flip: bool,
pub reference: DVec3, pub reference: DVec3,
@@ -87,6 +96,7 @@ pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool {
match kind { match kind {
Kind::Move => !p.rotation && !p.flip, Kind::Move => !p.rotation && !p.flip,
Kind::Stretch => !p.rotation && !p.flip && !p.point, Kind::Stretch => !p.rotation && !p.flip && !p.point,
Kind::PolarStretch => p.polar,
Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy, Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy,
Kind::Rotate => p.rotation, Kind::Rotate => p.rotation,
Kind::Flip => p.flip, Kind::Flip => p.flip,
@@ -208,6 +218,7 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
linear: false, linear: false,
point: true, point: true,
xy: false, xy: false,
polar: false,
rotation: false, rotation: false,
flip: false, flip: false,
reference: DVec3::ZERO, 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) { if matches!(kind, Kind::Rotate | Kind::Flip) {
continue; continue;
} }
let polar = p.kind == super::block_distance_parameter::Kind::Polar;
let xy = p.kind == super::block_distance_parameter::Kind::XY; let xy = p.kind == super::block_distance_parameter::Kind::XY;
if kind == Kind::Scale && xy { if kind == Kind::Scale && xy {
continue; continue;
} }
if kind == Kind::PolarStretch && !polar {
continue;
}
result.push(Parameter { result.push(Parameter {
handle: p.handle, handle: p.handle,
id: p.native.parameter.element.eval.node_id, 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, linear: p.kind == super::block_distance_parameter::Kind::Linear,
point: false, point: false,
xy, xy,
polar,
rotation: false, rotation: false,
flip: false, flip: false,
reference: DVec3::ZERO, reference: DVec3::ZERO,
@@ -255,6 +271,7 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
linear: false, linear: false,
point: false, point: false,
xy: false, xy: false,
polar: false,
rotation: kind == Kind::Rotate, rotation: kind == Kind::Rotate,
flip: kind == Kind::Flip, flip: kind == Kind::Flip,
reference: v(p.reference), reference: v(p.reference),
@@ -361,7 +378,9 @@ pub(crate) fn create(
targets: &[Handle], targets: &[Handle],
location: DVec3, location: DVec3,
) -> Result<Handle, &'static str> { ) -> Result<Handle, &'static str> {
create_with_options(doc, definition, kind, parameter, targets, location, None, None) create_with_options(
doc, definition, kind, parameter, targets, location, None, None,
)
} }
#[cfg(test)] #[cfg(test)]
@@ -396,7 +415,9 @@ pub(crate) fn create_with_frame(
location: DVec3, location: DVec3,
frame: Option<[DVec3; 2]>, frame: Option<[DVec3; 2]>,
) -> Result<Handle, &'static str> { ) -> Result<Handle, &'static str> {
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( pub(crate) fn create_with_options(
@@ -458,6 +479,19 @@ pub(crate) fn create_with_options(
} else { } else {
None 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 mut staged = doc.clone();
let (graph, slots) = authoring::prepare(&mut staged, definition)?; let (graph, slots) = authoring::prepare(&mut staged, definition)?;
let (dxf, cpp) = match kind { let (dxf, cpp) = match kind {
@@ -466,6 +500,7 @@ pub(crate) fn create_with_options(
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"), Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"), Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"), Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"),
Kind::PolarStretch => ("BLOCKPOLARSTRETCHACTION", "AcDbBlockPolarStretchAction"),
Kind::Array => ("BLOCKARRAYACTION", "AcDbBlockArrayAction"), Kind::Array => ("BLOCKARRAYACTION", "AcDbBlockArrayAction"),
}; };
register_classes(&mut staged, &[(dxf, cpp, 55)]); register_classes(&mut staged, &[(dxf, cpp, 55)]);
@@ -482,7 +517,7 @@ pub(crate) fn create_with_options(
eed_1071: 0, eed_1071: 0,
}, },
display_location: native(location), display_location: native(location),
dependencies, dependencies: dependencies.clone(),
action_ids: vec![], action_ids: vec![],
}; };
let connection = |name: &str| BlockConnection { 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 { Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction {
action, action,
connections: if p.point { connections: if p.point {
@@ -575,7 +635,6 @@ pub(crate) fn create_with_options(
Ok(handle) Ok(handle)
} }
/// Retarget an existing native action, retaining identity, graph and options. /// Retarget an existing native action, retaining identity, graph and options.
pub(crate) fn retarget( pub(crate) fn retarget(
doc: &mut CadDocument, doc: &mut CadDocument,
@@ -642,6 +701,33 @@ pub(crate) fn retarget(
} }
a.action.dependencies = targets; 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::<Vec<_>>();
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::MoveAction(a) => a.action.dependencies = targets,
DynamicBlockData::FlipAction(a) => a.action.dependencies = targets, DynamicBlockData::FlipAction(a) => a.action.dependencies = targets,
DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => {
@@ -650,8 +736,8 @@ pub(crate) fn retarget(
DynamicBlockData::ArrayAction(a) => a.action.dependencies = targets, DynamicBlockData::ArrayAction(a) => a.action.dependencies = targets,
_ => return Err("This action's selection cannot be edited yet."), _ => return Err("This action's selection cannot be edited yet."),
} }
if frame.is_some() && kind != Kind::Stretch { if frame.is_some() && !needs_frame(kind) {
return Err("Only Stretch actions have a stretch frame."); return Err("Only Stretch actions have an action frame.");
} }
if staged == *doc { if staged == *doc {
return Ok(false); return Ok(false);
@@ -689,6 +775,12 @@ pub(crate) fn actions(
DynamicBlockData::StretchAction(a) => { DynamicBlockData::StretchAction(a) => {
Some((o.handle, &a.action, Kind::Stretch, a.connections[0].code)) 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) => { DynamicBlockData::FlipAction(a) => {
Some((o.handle, &a.action, Kind::Flip, a.connections[0].code)) 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)); a.handles.retain(|b| live.contains(&b.handle));
&mut a.action &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) => { DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => {
&mut a.action.action &mut a.action.action
} }
@@ -744,6 +841,9 @@ pub(crate) fn frame_outline(doc: &CadDocument, handle: Handle) -> Vec<Vector3> {
return vec![]; return vec![];
}; };
let DynamicBlockData::StretchAction(a) = &o.data else { 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![]; return vec![];
}; };
stretch::outline(&a.points, a.action.display_location.z) 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(); .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) => { DynamicBlockData::FlipAction(a) => {
a.action.display_location = t.apply(a.action.display_location) a.action.display_location = t.apply(a.action.display_location)
} }
@@ -966,7 +1077,11 @@ fn evaluate_impl(
for h in extra_handles { for h in extra_handles {
staged.remove_entity(h); 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| { record.entity_handles.retain(|h| {
baseline_members.contains(h) baseline_members.contains(h)
|| *h == record.block_entity_handle || *h == record.block_entity_handle
@@ -984,6 +1099,14 @@ fn evaluate_impl(
unreachable!() unreachable!()
}; };
stretch::validate(baseline, s)?; 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()); affected.extend(a.dependencies.iter().copied());
} }
@@ -1012,7 +1135,7 @@ fn evaluate_impl(
} }
let end = current(&staged, &q); let end = current(&staged, &q);
let transform = match kind { let transform = match kind {
Kind::Move | Kind::Stretch => { Kind::Move | Kind::Stretch | Kind::PolarStretch => {
crate::command::EntityTransform::Translate(end - q.end) crate::command::EntityTransform::Translate(end - q.end)
} }
Kind::Rotate => { Kind::Rotate => {
@@ -1134,6 +1257,15 @@ fn evaluate_impl(
} }
if let DynamicBlockData::StretchAction(s) = &o.data { if let DynamicBlockData::StretchAction(s) = &o.data {
stretch::apply(&mut entity, s, end - q.end)?; 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 { } else {
super::view::dispatch::apply_transform(&mut entity, &transform); super::view::dispatch::apply_transform(&mut entity, &transform);
} }
@@ -1262,7 +1394,6 @@ fn evaluate_impl(
Ok(true) Ok(true)
} }
fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool { fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else {
return false; return false;
+90 -1
View File
@@ -1,6 +1,6 @@
//! A native Stretch action freezes its vertex membership at authoring time. //! A native Stretch action freezes its vertex membership at authoring time.
use super::*; use super::*;
use codec::objects::{BlockStretchAction, BlockStretchHandle}; use codec::objects::{BlockPolarStretchAction, BlockStretchAction, BlockStretchHandle};
use codec::types::Vector2; use codec::types::Vector2;
pub(crate) fn control_points(e: &EntityType) -> Result<Vec<DVec3>, &'static str> { pub(crate) fn control_points(e: &EntityType) -> Result<Vec<DVec3>, &'static str> {
@@ -160,6 +160,75 @@ pub(crate) fn validate(doc: &CadDocument, a: &BlockStretchAction) -> Result<(),
Ok(()) 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::<std::collections::BTreeSet<_>>();
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( pub(crate) fn apply(
e: &mut EntityType, e: &mut EntityType,
a: &BlockStretchAction, a: &BlockStretchAction,
@@ -224,6 +293,26 @@ pub(crate) fn apply(
Ok(()) 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<Vector3> { pub(crate) fn outline(points: &[Vector2], z: f64) -> Vec<Vector3> {
let points = if points.len() == 2 { let points = if points.len() == 2 {
let (a, b) = (points[0], points[1]); let (a, b) = (points[0], points[1]);
+6 -3
View File
@@ -6,7 +6,7 @@ use crate::ui::dock::{self, Chrome, PanelId, VerticalLabel};
use crate::ui::icons; use crate::ui::icons;
use crate::ui::ribbon::{tooltip_content, tooltip_style}; use crate::ui::ribbon::{tooltip_content, tooltip_style};
use iced::widget::{ 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}; use iced::{Background, Border, Element, Fill, Theme};
@@ -199,6 +199,7 @@ fn catalog(
rotate_error: Option<&'static str>, rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>, flip_action_error: Option<&'static str>,
stretch_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>,
polar_stretch_action_error: Option<&'static str>,
array_action_error: Option<&'static str>, array_action_error: Option<&'static str>,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let (entries, unavailable) = match tab { let (entries, unavailable) = match tab {
@@ -221,8 +222,8 @@ fn catalog(
let orientation_error = if *label=="Flip" {flip_error} else {point_error}; let orientation_error = if *label=="Flip" {flip_error} else {point_error};
let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint"); let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint");
let lookup_action=tab==Tab::Actions&&*label=="Lookup"; let lookup_action=tab==Tab::Actions&&*label=="Lookup";
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip"|"Array"); 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,"Rotate"=>rotate_error,"Flip"=>flip_action_error,"Array"=>array_action_error,_=>move_error}; 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 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 { let base = if enabled {
icons::semantic(icon, 28.0) icons::semantic(icon, 28.0)
@@ -408,6 +409,7 @@ pub fn view(
rotate_error: Option<&'static str>, rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>, flip_action_error: Option<&'static str>,
stretch_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>,
polar_stretch_action_error: Option<&'static str>,
array_action_error: Option<&'static str>, array_action_error: Option<&'static str>,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let tabs = column(Tab::ALL.into_iter().map(|category| { let tabs = column(Tab::ALL.into_iter().map(|category| {
@@ -469,6 +471,7 @@ pub fn view(
rotate_error, rotate_error,
flip_action_error, flip_action_error,
stretch_action_error, stretch_action_error,
polar_stretch_action_error,
array_action_error, array_action_error,
)) ))
.width(Fill) .width(Fill)