forked from 1b-agent/opencadstudio-upstream
feat(blocks): wire Stretch actions and independent reference evaluation
Author a native Stretch action from a Linear, Polar or XY parameter, a two-corner stretch frame, an object selection and a marker. Preview and retain its frame in BEDIT, with selectable and movable action markers. Freeze line and planar lightweight-polyline vertex bindings, preserve curved-segment height, move wholly selected objects, and evaluate independent saved INSERTs from their definition. Retain native DWG/DXF data, copied-reference isolation and undo/redo. Validate with synthetic frame, topology, parameter, history, Test Block, drawing-reference, COPY and roundtrip regressions: 340 block tests pass (6 existing opt-in ignored), plus 41 dock and 27 viewport tests. Other entity types, Polar Stretch, custom multipliers and chaining remain unsupported.
This commit is contained in:
1 parent
94940ad77c
commit
042f26a4cc
14 files changed
+827
-27
No files matched your search
@@ -38,7 +38,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, Rotate, Flip. Other action types are not wired yet.")
|
crate::t!("Supported actions: Move, Scale, Stretch, Rotate, Flip. Other action types are not wired yet.")
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
);
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -76,6 +76,7 @@ impl OpenCADStudio {
|
|||||||
parameter,
|
parameter,
|
||||||
targets,
|
targets,
|
||||||
location,
|
location,
|
||||||
|
frame,
|
||||||
} => {
|
} => {
|
||||||
if self.tabs[i].id != document
|
if self.tabs[i].id != document
|
||||||
|| self.tabs[i]
|
|| self.tabs[i]
|
||||||
@@ -90,8 +91,15 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let mut staged = self.tabs[i].scene.document.clone();
|
let mut staged = self.tabs[i].scene.document.clone();
|
||||||
match actions::create(&mut staged, definition, kind, parameter, &targets, location)
|
match actions::create_with_frame(
|
||||||
{
|
&mut staged,
|
||||||
|
definition,
|
||||||
|
kind,
|
||||||
|
parameter,
|
||||||
|
&targets,
|
||||||
|
location,
|
||||||
|
frame,
|
||||||
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.push_undo_snapshot(i, &format!("{} action", kind.name()));
|
self.push_undo_snapshot(i, &format!("{} action", kind.name()));
|
||||||
self.tabs[i].scene.document = staged;
|
self.tabs[i].scene.document = staged;
|
||||||
|
|||||||
@@ -59,6 +59,138 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
|||||||
let _ = app.update(Message::CommandSubmit);
|
let _ = app.update(Message::CommandSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_frame_selection_history_test_and_saved_grip() {
|
||||||
|
use crate::scene::block_actions::{self as actions, Kind};
|
||||||
|
let (mut app, reference) = fixture();
|
||||||
|
begin(&mut app, reference);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||||
|
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||||
|
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Stretch)[0].clone();
|
||||||
|
let before = app.tabs[i].scene.document.clone();
|
||||||
|
// A half-authored frame and Escape create neither objects nor history.
|
||||||
|
let depth = app.tabs[i].history.undo_stack.len();
|
||||||
|
let _ = app.dispatch_command("BACTION Stretch");
|
||||||
|
feed(&mut app, &p.name);
|
||||||
|
feed(&mut app, "0.5,-1");
|
||||||
|
let _ = app.update(Message::CommandEscape);
|
||||||
|
assert_eq!(app.tabs[i].scene.document, before);
|
||||||
|
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||||
|
let _ = app.dispatch_command("BACTION Stretch");
|
||||||
|
feed(&mut app, &p.name);
|
||||||
|
assert!(!app.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_selection_gathering());
|
||||||
|
feed(&mut app, "0.5,-1");
|
||||||
|
feed(&mut app, "0.5,1"); // zero-width error retains first corner
|
||||||
|
assert!(!app.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_selection_gathering());
|
||||||
|
feed(&mut app, "2,1");
|
||||||
|
assert!(app.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_selection_gathering());
|
||||||
|
let targets = members(&app);
|
||||||
|
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
|
||||||
|
targets.clone(),
|
||||||
|
));
|
||||||
|
let _ = app.feed_command(crate::command::StepInput::Enter);
|
||||||
|
feed(&mut app, "3,4");
|
||||||
|
assert!(app.tabs[i].active_cmd.is_none());
|
||||||
|
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||||
|
let action = actions::actions(&app.tabs[i].scene.document, def)[0].0;
|
||||||
|
assert_eq!(
|
||||||
|
actions::frame_outline(&app.tabs[i].scene.document, action).len(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
app.undo_steps(1);
|
||||||
|
assert!(actions::actions(&app.tabs[i].scene.document, def).is_empty());
|
||||||
|
app.redo_steps(1);
|
||||||
|
let editor = app.tabs[i].scene.document.clone();
|
||||||
|
let _ = app.dispatch_command("BTESTBLOCK");
|
||||||
|
let ti = app.active_tab;
|
||||||
|
let test_insert = app.tabs[ti].block_test.as_ref().unwrap().insert;
|
||||||
|
app.tabs[ti].scene.selected.insert(test_insert);
|
||||||
|
app.refresh_properties();
|
||||||
|
let grip = app.tabs[ti]
|
||||||
|
.selected_grips
|
||||||
|
.iter()
|
||||||
|
.find(|g| super::block_actions::is_test_grip(g.id))
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
assert!(app.start_test_action_grip(test_insert, grip));
|
||||||
|
feed(&mut app, "12,0");
|
||||||
|
let EntityType::Line(line) = app.tabs[ti].scene.document.get_entity(targets[0]).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.start, Vector3::ZERO);
|
||||||
|
assert_eq!(line.end, Vector3::new(3., 0., 0.));
|
||||||
|
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||||||
|
assert_eq!(app.tabs[i].scene.document, editor);
|
||||||
|
let _ = app.dispatch_command("BSAVE");
|
||||||
|
let _ = app.dispatch_command("BCLOSE");
|
||||||
|
app.tabs[i].scene.selected.insert(reference);
|
||||||
|
app.refresh_properties();
|
||||||
|
let grip = app.tabs[i]
|
||||||
|
.selected_grips
|
||||||
|
.iter()
|
||||||
|
.find(|g| super::block_actions::is_test_grip(g.id))
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let depth = app.tabs[i].history.undo_stack.len();
|
||||||
|
assert!(app.start_test_action_grip(reference, grip));
|
||||||
|
feed(&mut app, "15,0");
|
||||||
|
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||||
|
let changed = app.tabs[i].scene.document.clone();
|
||||||
|
let EntityType::Insert(insert) = changed.get_entity(reference).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let member = changed
|
||||||
|
.block_records
|
||||||
|
.get(&insert.block_name)
|
||||||
|
.unwrap()
|
||||||
|
.entity_handles[0];
|
||||||
|
let EntityType::Line(line) = changed.get_entity(member).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.start, Vector3::ZERO);
|
||||||
|
assert_eq!(line.end, Vector3::new(6., 0., 0.));
|
||||||
|
assert_eq!(
|
||||||
|
changed.get_entity(targets[0]),
|
||||||
|
editor.get_entity(targets[0])
|
||||||
|
);
|
||||||
|
app.undo_steps(1);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.get_entity(reference),
|
||||||
|
editor.get_entity(reference)
|
||||||
|
);
|
||||||
|
app.redo_steps(1);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.get_entity(member),
|
||||||
|
changed.get_entity(member)
|
||||||
|
);
|
||||||
|
let bytes = codec::DwgWriter::write_to_vec(&changed).unwrap();
|
||||||
|
app.tabs[i].scene.document = codec::DwgReader::from_stream(std::io::Cursor::new(bytes))
|
||||||
|
.read()
|
||||||
|
.unwrap();
|
||||||
|
begin(&mut app, reference);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].active_block_edit_session().unwrap().br_handle,
|
||||||
|
def
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
actions::frame_outline(&app.tabs[i].scene.document, action).len(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_parameters_rotation_only_save_close_reopen_then_flip() {
|
fn block_parameters_rotation_only_save_close_reopen_then_flip() {
|
||||||
use crate::scene::block_actions::{self as actions, Kind};
|
use crate::scene::block_actions::{self as actions, Kind};
|
||||||
@@ -903,6 +1035,7 @@ fn block_actions_rejected_outside_editor_preserve_running_command_and_stale_resu
|
|||||||
parameter: p,
|
parameter: p,
|
||||||
targets: members(&app),
|
targets: members(&app),
|
||||||
location: glam::DVec3::ZERO,
|
location: glam::DVec3::ZERO,
|
||||||
|
frame: None,
|
||||||
});
|
});
|
||||||
assert_eq!(app.tabs[i].scene.document, before);
|
assert_eq!(app.tabs[i].scene.document, before);
|
||||||
let _ = app.dispatch_command("LINE");
|
let _ = app.dispatch_command("LINE");
|
||||||
|
|||||||
@@ -1244,6 +1244,27 @@ fn block_actions_palette_controls_require_compatible_parameters_and_start_comman
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_palette_requires_distance_parameter() {
|
||||||
|
with_stack(|| {
|
||||||
|
let mut app = authoring_app();
|
||||||
|
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||||
|
app.authoring_palette_tab = crate::ui::window::authoring_palettes::Tab::Actions;
|
||||||
|
let point = authoring_control(&app, "authoring-tool-stretch-action");
|
||||||
|
authoring_click(&mut app, point);
|
||||||
|
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||||
|
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||||
|
let point = authoring_control(&app, "authoring-tool-stretch-action");
|
||||||
|
authoring_click(&mut app, point);
|
||||||
|
assert!(app.tabs[app.active_tab]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.prompt()
|
||||||
|
.contains("Stretch"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_actions_rotate_flip_palette_requires_matching_parameters() {
|
fn block_actions_rotate_flip_palette_requires_matching_parameters() {
|
||||||
with_stack(|| {
|
with_stack(|| {
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ impl OpenCADStudio {
|
|||||||
self.action_error(Some(crate::scene::block_actions::Kind::Scale)),
|
self.action_error(Some(crate::scene::block_actions::Kind::Scale)),
|
||||||
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)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
crate::ui::dock::PanelId::BlockPalette => {
|
crate::ui::dock::PanelId::BlockPalette => {
|
||||||
|
|||||||
+12
-1
@@ -1203,8 +1203,19 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
session.br_handle,
|
session.br_handle,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, action, _, _)| {
|
.map(|(handle, action, _, _)| {
|
||||||
let mut g = crate::scene::block_distance_parameter::Graphic::default();
|
let mut g = crate::scene::block_distance_parameter::Graphic::default();
|
||||||
|
let frame = crate::scene::block_actions::frame_outline(
|
||||||
|
&tab.scene.document,
|
||||||
|
handle,
|
||||||
|
);
|
||||||
|
g.lines.extend(
|
||||||
|
frame
|
||||||
|
.iter()
|
||||||
|
.zip(frame.iter().cycle().skip(1))
|
||||||
|
.take(frame.len())
|
||||||
|
.map(|(a, b)| [*a, *b]),
|
||||||
|
);
|
||||||
g.grips.push((action.display_location, false));
|
g.grips.push((action.display_location, false));
|
||||||
g.labels
|
g.labels
|
||||||
.push((action.display_location, action.element.name.clone()));
|
.push((action.display_location, action.element.name.clone()));
|
||||||
|
|||||||
@@ -1528,6 +1528,7 @@ pub enum CmdResult {
|
|||||||
parameter: Handle,
|
parameter: Handle,
|
||||||
targets: Vec<Handle>,
|
targets: Vec<Handle>,
|
||||||
location: DVec3,
|
location: DVec3,
|
||||||
|
frame: Option<[DVec3; 2]>,
|
||||||
},
|
},
|
||||||
EvaluateBlockParameter {
|
EvaluateBlockParameter {
|
||||||
insert: Option<Handle>,
|
insert: Option<Handle>,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub(crate) struct ActionCommand {
|
|||||||
parameter: Option<Handle>,
|
parameter: Option<Handle>,
|
||||||
targets: Vec<Handle>,
|
targets: Vec<Handle>,
|
||||||
selecting: bool,
|
selecting: bool,
|
||||||
|
frame: Vec<DVec3>,
|
||||||
context: Option<PointPickContext>,
|
context: Option<PointPickContext>,
|
||||||
}
|
}
|
||||||
impl ActionCommand {
|
impl ActionCommand {
|
||||||
@@ -33,6 +34,7 @@ impl ActionCommand {
|
|||||||
parameter: None,
|
parameter: None,
|
||||||
targets: vec![],
|
targets: vec![],
|
||||||
selecting: false,
|
selecting: false,
|
||||||
|
frame: vec![],
|
||||||
context: None,
|
context: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ impl ActionCommand {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.parameter = Some(p.handle);
|
self.parameter = Some(p.handle);
|
||||||
self.selecting = true;
|
self.selecting = self.kind != Some(Kind::Stretch);
|
||||||
CmdResult::DeselectAndContinue
|
CmdResult::DeselectAndContinue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +64,8 @@ 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/Rotate/Flip]:").into_owned();
|
return crate::t!("BACTION Enter action type [Move/Scale/Stretch/Rotate/Flip]:")
|
||||||
|
.into_owned();
|
||||||
}
|
}
|
||||||
if self.parameter.is_none() {
|
if self.parameter.is_none() {
|
||||||
return crate::tf!(
|
return crate::tf!(
|
||||||
@@ -71,11 +74,17 @@ impl CadCommand for ActionCommand {
|
|||||||
)
|
)
|
||||||
.into_owned();
|
.into_owned();
|
||||||
}
|
}
|
||||||
crate::t!(if self.selecting {
|
crate::t!(
|
||||||
"BACTION Select objects for the action, then press Enter:"
|
if self.kind == Some(Kind::Stretch) && self.frame.is_empty() {
|
||||||
} else {
|
"BACTION Specify first corner of stretch frame:"
|
||||||
"BACTION Specify action marker location:"
|
} else if self.kind == Some(Kind::Stretch) && self.frame.len() == 1 {
|
||||||
})
|
"BACTION Specify opposite corner of stretch frame:"
|
||||||
|
} else if self.selecting {
|
||||||
|
"BACTION Select objects for the action, then press Enter:"
|
||||||
|
} else {
|
||||||
|
"BACTION Specify action marker location:"
|
||||||
|
}
|
||||||
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
}
|
}
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
@@ -83,6 +92,7 @@ impl CadCommand for ActionCommand {
|
|||||||
vec![
|
vec![
|
||||||
CmdOption::new("Move", "Move"),
|
CmdOption::new("Move", "Move"),
|
||||||
CmdOption::new("Scale", "Scale"),
|
CmdOption::new("Scale", "Scale"),
|
||||||
|
CmdOption::new("Stretch", "Stretch"),
|
||||||
CmdOption::new("Rotate", "Rotate"),
|
CmdOption::new("Rotate", "Rotate"),
|
||||||
CmdOption::new("Flip", "Flip"),
|
CmdOption::new("Flip", "Flip"),
|
||||||
]
|
]
|
||||||
@@ -97,7 +107,7 @@ impl CadCommand for ActionCommand {
|
|||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
} else {
|
} else {
|
||||||
CmdResult::ReportError(
|
CmdResult::ReportError(
|
||||||
crate::t!("Choose Move, Scale, Rotate or Flip.").into_owned(),
|
crate::t!("Choose Move, Scale, Stretch, Rotate or Flip.").into_owned(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -149,6 +159,35 @@ impl CadCommand for ActionCommand {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if self.kind == Some(Kind::Stretch) && self.frame.len() < 2 {
|
||||||
|
let plane = self
|
||||||
|
.candidates
|
||||||
|
.iter()
|
||||||
|
.find(|p| Some(p.handle) == self.parameter)
|
||||||
|
.unwrap()
|
||||||
|
.end
|
||||||
|
.z;
|
||||||
|
if !point.is_finite() || (point.z - plane).abs() > 1e-8 {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
crate::t!("Specify a finite corner in the parameter's XY plane.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.frame
|
||||||
|
.first()
|
||||||
|
.is_some_and(|a| (point.x - a.x).abs() < 1e-9 || (point.y - a.y).abs() < 1e-9)
|
||||||
|
{
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
crate::t!("The stretch frame needs a nonzero width and height.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.frame.push(point);
|
||||||
|
if self.frame.len() == 2 {
|
||||||
|
self.selecting = true;
|
||||||
|
return CmdResult::DeselectAndContinue;
|
||||||
|
}
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
if self.selecting {
|
if self.selecting {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
@@ -164,8 +203,26 @@ impl CadCommand for ActionCommand {
|
|||||||
parameter: self.parameter.unwrap(),
|
parameter: self.parameter.unwrap(),
|
||||||
targets: self.targets.clone(),
|
targets: self.targets.clone(),
|
||||||
location: point,
|
location: point,
|
||||||
|
frame: (self.frame.len() == 2).then(|| [self.frame[0], self.frame[1]]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn on_preview_wires(&mut self, point: DVec3) -> Vec<crate::scene::WireModel> {
|
||||||
|
let Some(a) = self.frame.first().filter(|_| point.is_finite()) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let b = self.frame.get(1).copied().unwrap_or(point);
|
||||||
|
vec![crate::scene::WireModel::solid_f64(
|
||||||
|
"preview_stretch_frame".into(),
|
||||||
|
vec![
|
||||||
|
a.to_array(),
|
||||||
|
[b.x, a.y, a.z],
|
||||||
|
[b.x, b.y, a.z],
|
||||||
|
[a.x, b.y, a.z],
|
||||||
|
],
|
||||||
|
crate::scene::WireModel::CYAN,
|
||||||
|
true,
|
||||||
|
)]
|
||||||
|
}
|
||||||
fn is_selection_gathering(&self) -> bool {
|
fn is_selection_gathering(&self) -> bool {
|
||||||
self.selecting
|
self.selecting
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,13 @@ pub fn stretch_entity(
|
|||||||
|
|
||||||
/// The bulge of the segment from vertex `k` once its ends moved from
|
/// The bulge of the segment from vertex `k` once its ends moved from
|
||||||
/// `before` to `after`: the arc keeps its height, bulge · chord / 2.
|
/// `before` to `after`: the arc keeps its height, bulge · chord / 2.
|
||||||
fn held_bulge(bulge: f64, before: &[[f64; 2]], after: &[[f64; 2]], k: usize, closed: bool) -> f64 {
|
pub(crate) fn held_bulge(
|
||||||
|
bulge: f64,
|
||||||
|
before: &[[f64; 2]],
|
||||||
|
after: &[[f64; 2]],
|
||||||
|
k: usize,
|
||||||
|
closed: bool,
|
||||||
|
) -> f64 {
|
||||||
let next = if k + 1 < before.len() {
|
let next = if k + 1 < before.len() {
|
||||||
k + 1
|
k + 1
|
||||||
} else if closed {
|
} else if closed {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
//! Native Move/Scale/Rotate/Flip authoring and deterministic block evaluation.
|
//! Native Move/Scale/Stretch/Rotate/Flip authoring and deterministic block evaluation.
|
||||||
use super::block_authoring::{self as authoring, *};
|
use super::block_authoring::{self as authoring, *};
|
||||||
use codec::objects::{
|
use codec::objects::{
|
||||||
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
|
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
|
||||||
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, BlockFlipAction,
|
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, BlockFlipAction,
|
||||||
DynamicBlockData, ObjectType,
|
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;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ pub enum Kind {
|
|||||||
Scale,
|
Scale,
|
||||||
Rotate,
|
Rotate,
|
||||||
Flip,
|
Flip,
|
||||||
|
Stretch,
|
||||||
}
|
}
|
||||||
impl Kind {
|
impl Kind {
|
||||||
pub(crate) fn parse(s: &str) -> Option<Self> {
|
pub(crate) fn parse(s: &str) -> Option<Self> {
|
||||||
@@ -26,6 +28,7 @@ impl Kind {
|
|||||||
"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),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +38,7 @@ impl Kind {
|
|||||||
Self::Scale => "Scale",
|
Self::Scale => "Scale",
|
||||||
Self::Rotate => "Rotate",
|
Self::Rotate => "Rotate",
|
||||||
Self::Flip => "Flip",
|
Self::Flip => "Flip",
|
||||||
|
Self::Stretch => "Stretch",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +82,7 @@ pub(crate) fn all_parameters(doc: &CadDocument, definition: Handle) -> Vec<Param
|
|||||||
pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool {
|
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::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,
|
||||||
@@ -342,6 +347,7 @@ pub(crate) fn supported(e: &EntityType) -> bool {
|
|||||||
| EntityType::MText(_)
|
| EntityType::MText(_)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn create(
|
pub(crate) fn create(
|
||||||
doc: &mut CadDocument,
|
doc: &mut CadDocument,
|
||||||
definition: Handle,
|
definition: Handle,
|
||||||
@@ -349,6 +355,18 @@ pub(crate) fn create(
|
|||||||
parameter: Handle,
|
parameter: Handle,
|
||||||
targets: &[Handle],
|
targets: &[Handle],
|
||||||
location: DVec3,
|
location: DVec3,
|
||||||
|
) -> Result<Handle, &'static str> {
|
||||||
|
create_with_frame(doc, definition, kind, parameter, targets, location, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_with_frame(
|
||||||
|
doc: &mut CadDocument,
|
||||||
|
definition: Handle,
|
||||||
|
kind: Kind,
|
||||||
|
parameter: Handle,
|
||||||
|
targets: &[Handle],
|
||||||
|
location: DVec3,
|
||||||
|
frame: Option<[DVec3; 2]>,
|
||||||
) -> Result<Handle, &'static str> {
|
) -> Result<Handle, &'static str> {
|
||||||
if !location.is_finite() {
|
if !location.is_finite() {
|
||||||
return Err("Specify a finite action location.");
|
return Err("Specify a finite action location.");
|
||||||
@@ -374,6 +392,19 @@ pub(crate) fn create(
|
|||||||
}) {
|
}) {
|
||||||
return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block.");
|
return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block.");
|
||||||
}
|
}
|
||||||
|
let stretch_binding = if kind == Kind::Stretch {
|
||||||
|
if (location.z - p.end.z).abs() > 1e-8 {
|
||||||
|
return Err("Place the Stretch marker in the parameter's XY plane.");
|
||||||
|
}
|
||||||
|
Some(stretch::bind(
|
||||||
|
doc,
|
||||||
|
targets,
|
||||||
|
frame.ok_or("Specify a 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 {
|
||||||
@@ -381,6 +412,7 @@ pub(crate) fn create(
|
|||||||
Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"),
|
Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"),
|
||||||
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
|
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
|
||||||
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
|
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
|
||||||
|
Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"),
|
||||||
};
|
};
|
||||||
register_classes(&mut staged, &[(dxf, cpp, 55)]);
|
register_classes(&mut staged, &[(dxf, cpp, 55)]);
|
||||||
let handle = staged.allocate_handle();
|
let handle = staged.allocate_handle();
|
||||||
@@ -404,6 +436,21 @@ pub(crate) fn create(
|
|||||||
name: name.into(),
|
name: name.into(),
|
||||||
};
|
};
|
||||||
let data = match kind {
|
let data = match kind {
|
||||||
|
Kind::Stretch => {
|
||||||
|
let (points, handles) = stretch_binding.unwrap();
|
||||||
|
DynamicBlockData::StretchAction(BlockStretchAction {
|
||||||
|
action,
|
||||||
|
connections: [connection("EndXDelta"), connection("EndYDelta")],
|
||||||
|
points,
|
||||||
|
handles,
|
||||||
|
codes: vec![],
|
||||||
|
offsets: BlockActionOffsets {
|
||||||
|
distance_multiplier: 1.,
|
||||||
|
angle_offset: 0.,
|
||||||
|
flags: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction {
|
Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction {
|
||||||
action,
|
action,
|
||||||
connections: if p.point {
|
connections: if p.point {
|
||||||
@@ -489,6 +536,9 @@ pub(crate) fn actions(
|
|||||||
DynamicBlockData::MoveAction(a) => {
|
DynamicBlockData::MoveAction(a) => {
|
||||||
Some((o.handle, &a.action, Kind::Move, a.connections[0].code))
|
Some((o.handle, &a.action, Kind::Move, a.connections[0].code))
|
||||||
}
|
}
|
||||||
|
DynamicBlockData::StretchAction(a) => {
|
||||||
|
Some((o.handle, &a.action, Kind::Stretch, 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))
|
||||||
}
|
}
|
||||||
@@ -505,6 +555,16 @@ pub(crate) fn actions(
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
pub(crate) fn frame_outline(doc: &CadDocument, handle: Handle) -> Vec<Vector3> {
|
||||||
|
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let DynamicBlockData::StretchAction(a) = &o.data else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
stretch::outline(&a.points, a.action.display_location.z)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform) -> usize {
|
pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform) -> usize {
|
||||||
let handles = actions(doc, definition)
|
let handles = actions(doc, definition)
|
||||||
.iter()
|
.iter()
|
||||||
@@ -518,6 +578,17 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform)
|
|||||||
DynamicBlockData::MoveAction(a) => {
|
DynamicBlockData::MoveAction(a) => {
|
||||||
a.action.display_location = t.apply(a.action.display_location)
|
a.action.display_location = t.apply(a.action.display_location)
|
||||||
}
|
}
|
||||||
|
DynamicBlockData::StretchAction(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)
|
||||||
}
|
}
|
||||||
@@ -613,8 +684,10 @@ pub(crate) fn evaluate_value(
|
|||||||
) -> Result<bool, &'static str> {
|
) -> Result<bool, &'static str> {
|
||||||
let graph = authoring::graph(baseline, definition)?
|
let graph = authoring::graph(baseline, definition)?
|
||||||
.ok_or("The block authoring graph is unavailable.")?;
|
.ok_or("The block authoring graph is unavailable.")?;
|
||||||
if baseline.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data,
|
if baseline.objects.values().any(|o| {
|
||||||
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::ArrayAction(_)))) {
|
matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data,
|
||||||
|
DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::ArrayAction(_)))
|
||||||
|
}) {
|
||||||
return Err("This block contains action types not supported yet.");
|
return Err("This block contains action types not supported yet.");
|
||||||
}
|
}
|
||||||
let p = all_parameters(baseline, definition)
|
let p = all_parameters(baseline, definition)
|
||||||
@@ -660,7 +733,16 @@ pub(crate) fn evaluate_value(
|
|||||||
set_value(&mut staged, &p, input)?;
|
set_value(&mut staged, &p, input)?;
|
||||||
let all = actions(baseline, definition);
|
let all = actions(baseline, definition);
|
||||||
let mut affected = std::collections::BTreeSet::new();
|
let mut affected = std::collections::BTreeSet::new();
|
||||||
for (_, a, _, _) in &all {
|
for (h, a, kind, _) in &all {
|
||||||
|
if *kind == Kind::Stretch {
|
||||||
|
let ObjectType::DynamicBlock(o) = &baseline.objects[h] else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let DynamicBlockData::StretchAction(s) = &o.data else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
stretch::validate(baseline, s)?;
|
||||||
|
}
|
||||||
affected.extend(a.dependencies.iter().copied());
|
affected.extend(a.dependencies.iter().copied());
|
||||||
}
|
}
|
||||||
for h in affected {
|
for h in affected {
|
||||||
@@ -688,7 +770,9 @@ pub(crate) fn evaluate_value(
|
|||||||
}
|
}
|
||||||
let end = current(&staged, &q);
|
let end = current(&staged, &q);
|
||||||
let transform = match kind {
|
let transform = match kind {
|
||||||
Kind::Move => crate::command::EntityTransform::Translate(end - q.end),
|
Kind::Move | Kind::Stretch => {
|
||||||
|
crate::command::EntityTransform::Translate(end - q.end)
|
||||||
|
}
|
||||||
Kind::Rotate => {
|
Kind::Rotate => {
|
||||||
let a = end - q.base;
|
let a = end - q.base;
|
||||||
let b = q.end - q.base;
|
let b = q.end - q.base;
|
||||||
@@ -805,7 +889,11 @@ pub(crate) fn evaluate_value(
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
super::view::dispatch::apply_transform(&mut entity, &transform);
|
if let DynamicBlockData::StretchAction(s) = &o.data {
|
||||||
|
stretch::apply(&mut entity, s, end - q.end)?;
|
||||||
|
} else {
|
||||||
|
super::view::dispatch::apply_transform(&mut entity, &transform);
|
||||||
|
}
|
||||||
let bounds = entity.as_entity().bounding_box();
|
let bounds = entity.as_entity().bounding_box();
|
||||||
if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() {
|
if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() {
|
||||||
return Err("The parameter value produces non-finite geometry.");
|
return Err("The parameter value produces non-finite geometry.");
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
//! A native Stretch action freezes its vertex membership at authoring time.
|
||||||
|
use super::*;
|
||||||
|
use codec::objects::{BlockStretchAction, BlockStretchHandle};
|
||||||
|
use codec::types::Vector2;
|
||||||
|
|
||||||
|
pub(crate) fn control_points(e: &EntityType) -> Result<Vec<DVec3>, &'static str> {
|
||||||
|
let points = match e {
|
||||||
|
EntityType::Line(l) => vec![v(l.start), v(l.end)],
|
||||||
|
EntityType::LwPolyline(p) => {
|
||||||
|
let p = crate::entities::curve::lwpolyline_world_xy(p)
|
||||||
|
.ok_or("Stretch requires a planar XY lightweight polyline.")?;
|
||||||
|
p.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| DVec3::new(v.location.x, v.location.y, p.elevation))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
_ => return Err("Stretch currently supports lines and planar lightweight polylines."),
|
||||||
|
};
|
||||||
|
if points.is_empty() || points.iter().any(|p| !p.is_finite()) {
|
||||||
|
return Err("Stretch targets need finite vertices.");
|
||||||
|
}
|
||||||
|
Ok(points)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bind(
|
||||||
|
doc: &CadDocument,
|
||||||
|
targets: &[Handle],
|
||||||
|
corners: [DVec3; 2],
|
||||||
|
plane: f64,
|
||||||
|
) -> Result<(Vec<Vector2>, Vec<BlockStretchHandle>), &'static str> {
|
||||||
|
if corners
|
||||||
|
.iter()
|
||||||
|
.any(|p| !p.is_finite() || (p.z - plane).abs() > 1e-8)
|
||||||
|
{
|
||||||
|
return Err("Specify finite stretch-frame corners in the parameter's XY plane.");
|
||||||
|
}
|
||||||
|
let low = corners[0].min(corners[1]);
|
||||||
|
let high = corners[0].max(corners[1]);
|
||||||
|
if high.x - low.x < 1e-9 || high.y - low.y < 1e-9 {
|
||||||
|
return Err("The stretch frame needs a nonzero width and height.");
|
||||||
|
}
|
||||||
|
let mut bindings = vec![];
|
||||||
|
let mut unique = targets.to_vec();
|
||||||
|
unique.sort();
|
||||||
|
unique.dedup();
|
||||||
|
for handle in unique {
|
||||||
|
let entity = doc
|
||||||
|
.get_entity(handle)
|
||||||
|
.ok_or("A Stretch target is unavailable.")?;
|
||||||
|
let points = control_points(entity)?;
|
||||||
|
if points.iter().any(|p| (p.z - plane).abs() > 1e-8) {
|
||||||
|
return Err("Stretch targets must lie in the parameter's XY plane.");
|
||||||
|
}
|
||||||
|
let indexes = points
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, p)| p.x >= low.x && p.x <= high.x && p.y >= low.y && p.y <= high.y)
|
||||||
|
.map(|(i, _)| i as i32)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !indexes.is_empty() {
|
||||||
|
bindings.push(BlockStretchHandle { handle, indexes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
vec![Vector2::new(low.x, low.y), Vector2::new(high.x, high.y)],
|
||||||
|
bindings,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate(doc: &CadDocument, a: &BlockStretchAction) -> Result<(), &'static str> {
|
||||||
|
if a.points.len() < 2
|
||||||
|
|| a.points
|
||||||
|
.iter()
|
||||||
|
.any(|p| !p.x.is_finite() || !p.y.is_finite())
|
||||||
|
|| a.offsets.distance_multiplier != 1.
|
||||||
|
|| a.offsets.angle_offset != 0.
|
||||||
|
|| a.offsets.flags != 0
|
||||||
|
|| !a.codes.is_empty()
|
||||||
|
|| !a.action.action_ids.is_empty()
|
||||||
|
|| a.connections[0].code != a.connections[1].code
|
||||||
|
|| a.connections[0].name != "EndXDelta"
|
||||||
|
|| a.connections[1].name != "EndYDelta"
|
||||||
|
{
|
||||||
|
return Err("This Stretch action uses options or parameter bindings not supported yet.");
|
||||||
|
}
|
||||||
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
|
for binding in &a.handles {
|
||||||
|
if !a.action.dependencies.contains(&binding.handle) || !seen.insert(binding.handle) {
|
||||||
|
return Err("The Stretch action has invalid target bindings.");
|
||||||
|
}
|
||||||
|
let count = control_points(
|
||||||
|
doc.get_entity(binding.handle)
|
||||||
|
.ok_or("A 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 Stretch vertex binding no longer matches its object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Selected objects without a partial vertex binding move as a whole.
|
||||||
|
for h in &a.action.dependencies {
|
||||||
|
control_points(
|
||||||
|
doc.get_entity(*h)
|
||||||
|
.ok_or("A Stretch target is unavailable.")?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply(
|
||||||
|
e: &mut EntityType,
|
||||||
|
a: &BlockStretchAction,
|
||||||
|
delta: DVec3,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
let Some(binding) = a
|
||||||
|
.handles
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.handle == e.common().handle && !b.indexes.is_empty())
|
||||||
|
else {
|
||||||
|
crate::scene::view::dispatch::apply_transform(
|
||||||
|
e,
|
||||||
|
&crate::command::EntityTransform::Translate(delta),
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
match e {
|
||||||
|
EntityType::Line(l) => {
|
||||||
|
for index in &binding.indexes {
|
||||||
|
let at = match index {
|
||||||
|
0 => &mut l.start,
|
||||||
|
1 => &mut l.end,
|
||||||
|
_ => return Err("Invalid Stretch line endpoint."),
|
||||||
|
};
|
||||||
|
*at = *at + native(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityType::LwPolyline(p) => {
|
||||||
|
let mut world = crate::entities::curve::lwpolyline_world_xy(p)
|
||||||
|
.ok_or("Stretch requires a planar XY lightweight polyline.")?;
|
||||||
|
let before = world
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| [v.location.x, v.location.y])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for index in &binding.indexes {
|
||||||
|
let v = world
|
||||||
|
.vertices
|
||||||
|
.get_mut(*index as usize)
|
||||||
|
.ok_or("Invalid Stretch polyline vertex.")?;
|
||||||
|
v.location.x += delta.x;
|
||||||
|
v.location.y += delta.y;
|
||||||
|
}
|
||||||
|
let after = world
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.map(|v| [v.location.x, v.location.y])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (i, v) in world.vertices.iter_mut().enumerate() {
|
||||||
|
v.bulge = crate::modules::draw::modify::stretch::held_bulge(
|
||||||
|
v.bulge,
|
||||||
|
&before,
|
||||||
|
&after,
|
||||||
|
i,
|
||||||
|
world.is_closed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*p = world;
|
||||||
|
}
|
||||||
|
_ => return Err("Unsupported Stretch target."),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn outline(points: &[Vector2], z: f64) -> Vec<Vector3> {
|
||||||
|
let points = if points.len() == 2 {
|
||||||
|
let (a, b) = (points[0], points[1]);
|
||||||
|
vec![a, Vector2::new(b.x, a.y), b, Vector2::new(a.x, b.y)]
|
||||||
|
} else {
|
||||||
|
points.to_vec()
|
||||||
|
};
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| Vector3::new(p.x, p.y, z))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -605,3 +605,264 @@ fn block_actions_reframe_moves_markers_and_retains_targets_and_connections() {
|
|||||||
assert_eq!(s.action.action.dependencies, b.action.action.dependencies);
|
assert_eq!(s.action.action.dependencies, b.action.action.dependencies);
|
||||||
assert_eq!(s.connections, b.connections);
|
assert_eq!(s.connections, b.connections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stretch_fixture() -> (CadDocument, Handle, Vec<Handle>, Handle, Handle) {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let mut poly = codec::entities::LwPolyline::from_points(vec![
|
||||||
|
codec::types::Vector2::new(0., 1.),
|
||||||
|
codec::types::Vector2::new(10., 1.),
|
||||||
|
codec::types::Vector2::new(10., 3.),
|
||||||
|
codec::types::Vector2::new(0., 3.),
|
||||||
|
]);
|
||||||
|
poly.is_closed = true;
|
||||||
|
poly.vertices[0].bulge = 0.2;
|
||||||
|
scene.define_block_raw(
|
||||||
|
"STRETCH",
|
||||||
|
Vector3::ZERO,
|
||||||
|
vec![
|
||||||
|
EntityType::Line(codec::entities::Line::from_points(
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(10., 0., 0.),
|
||||||
|
)),
|
||||||
|
EntityType::LwPolyline(poly),
|
||||||
|
EntityType::Line(codec::entities::Line::from_points(
|
||||||
|
Vector3::new(20., 0., 0.),
|
||||||
|
Vector3::new(21., 0., 0.),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let br = scene.document.block_records.get("STRETCH").unwrap();
|
||||||
|
let (def, handles) = (br.handle, br.entity_handles.clone());
|
||||||
|
let mut doc = scene.document;
|
||||||
|
let p = linear(&mut doc, def);
|
||||||
|
let action = create_with_frame(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
Kind::Stretch,
|
||||||
|
p,
|
||||||
|
&handles,
|
||||||
|
DVec3::new(5., 4., 0.),
|
||||||
|
Some([DVec3::new(9., -1., 0.), DVec3::new(11., 4., 0.)]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
(doc, def, handles, p, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_frozen_vertices_polyline_and_whole_objects_roundtrip() {
|
||||||
|
let (doc, def, handles, p, action) = stretch_fixture();
|
||||||
|
let ObjectType::DynamicBlock(o) = &doc.objects[&action] else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let DynamicBlockData::StretchAction(a) = &o.data else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(a.handles[0].indexes, vec![1]);
|
||||||
|
assert_eq!(a.handles[1].indexes, vec![1, 2]);
|
||||||
|
assert_eq!(a.action.dependencies, handles);
|
||||||
|
let mut evaluated = doc.clone();
|
||||||
|
for x in [20., 30., 12.] {
|
||||||
|
evaluate(&mut evaluated, &doc, def, p, DVec3::new(x, 0., 0.)).unwrap();
|
||||||
|
let EntityType::Line(line) = evaluated.get_entity(handles[0]).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.start, Vector3::ZERO);
|
||||||
|
assert_eq!(line.end, Vector3::new(x, 0., 0.));
|
||||||
|
// Entirely outside the frame but explicitly in the action selection.
|
||||||
|
assert_eq!(start(&evaluated, handles[2]), Vector3::new(x + 10., 0., 0.));
|
||||||
|
let EntityType::LwPolyline(poly) = evaluated.get_entity(handles[1]).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(poly.vertices[0].location.x, 0.);
|
||||||
|
assert_eq!(poly.vertices[1].location.x, x);
|
||||||
|
assert_eq!(poly.vertices[2].location.x, x);
|
||||||
|
assert_eq!(poly.vertices[3].location.x, 0.);
|
||||||
|
// Preserve the curved segment's sagitta when its chord changes.
|
||||||
|
assert!((poly.vertices[0].bulge * x / 2. - 1.).abs() < 1e-8);
|
||||||
|
}
|
||||||
|
for reopened in [
|
||||||
|
DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
|
||||||
|
.read()
|
||||||
|
.unwrap(),
|
||||||
|
DxfReader::from_reader(Cursor::new(DxfWriter::new(&doc).write_to_vec().unwrap()))
|
||||||
|
.unwrap()
|
||||||
|
.read()
|
||||||
|
.unwrap(),
|
||||||
|
] {
|
||||||
|
assert_eq!(reopened.objects[&action], doc.objects[&action]);
|
||||||
|
let mut changed = reopened.clone();
|
||||||
|
evaluate(&mut changed, &reopened, def, p, DVec3::new(14., 0., 0.)).unwrap();
|
||||||
|
let EntityType::Line(line) = changed.get_entity(handles[0]).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.end, Vector3::new(14., 0., 0.));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_saved_reference_independence_copy_and_reopen() {
|
||||||
|
let (mut doc, def, handles, p, _) = stretch_fixture();
|
||||||
|
let a = doc
|
||||||
|
.add_entity(EntityType::Insert(codec::entities::Insert::new(
|
||||||
|
"STRETCH",
|
||||||
|
Vector3::ZERO,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
let b = doc
|
||||||
|
.add_entity(EntityType::Insert(codec::entities::Insert::new(
|
||||||
|
"STRETCH",
|
||||||
|
Vector3::new(40., 0., 0.),
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
let original = doc.clone();
|
||||||
|
doc = instance::evaluate(&doc, a, p, DVec3::new(18., 0., 0.))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(doc.get_entity(handles[0]), original.get_entity(handles[0]));
|
||||||
|
assert_eq!(doc.get_entity(b), original.get_entity(b));
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
scene.document = doc;
|
||||||
|
let c = scene.copy_entities(
|
||||||
|
&[a],
|
||||||
|
&crate::command::EntityTransform::Translate(DVec3::new(60., 0., 0.)),
|
||||||
|
)[0];
|
||||||
|
let mut doc = scene.document;
|
||||||
|
let EntityType::Insert(ins) = doc.get_entity(a).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let a_member = doc
|
||||||
|
.block_records
|
||||||
|
.get(&ins.block_name)
|
||||||
|
.unwrap()
|
||||||
|
.entity_handles[0];
|
||||||
|
doc = instance::evaluate(&doc, c, p, DVec3::new(22., 0., 0.))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let EntityType::Line(line) = doc.get_entity(a_member).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.end, Vector3::new(18., 0., 0.));
|
||||||
|
let reopened = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
|
||||||
|
.read()
|
||||||
|
.unwrap();
|
||||||
|
let parameter = parameters(&reopened, def, Kind::Stretch)[0].clone();
|
||||||
|
assert_eq!(
|
||||||
|
instance::current(&reopened, c, ¶meter).unwrap(),
|
||||||
|
DVec3::new(22., 0., 0.)
|
||||||
|
);
|
||||||
|
let changed = instance::evaluate(&reopened, c, p, DVec3::new(13., 0., 0.))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let EntityType::Insert(ins) = changed.get_entity(c).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let EntityType::Line(line) = changed
|
||||||
|
.get_entity(
|
||||||
|
changed
|
||||||
|
.block_records
|
||||||
|
.get(&ins.block_name)
|
||||||
|
.unwrap()
|
||||||
|
.entity_handles[0],
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.end, Vector3::new(13., 0., 0.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_invalid_frame_binding_and_options_are_atomic() {
|
||||||
|
let (mut doc, def, handles, p, action) = stretch_fixture();
|
||||||
|
let before = doc.clone();
|
||||||
|
for frame in [
|
||||||
|
None,
|
||||||
|
Some([DVec3::ZERO, DVec3::X]),
|
||||||
|
Some([DVec3::ZERO, DVec3::splat(f64::NAN)]),
|
||||||
|
] {
|
||||||
|
assert!(create_with_frame(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
Kind::Stretch,
|
||||||
|
p,
|
||||||
|
&handles,
|
||||||
|
DVec3::ZERO,
|
||||||
|
frame
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(doc, before);
|
||||||
|
}
|
||||||
|
for corrupt in [0, 1, 2] {
|
||||||
|
let mut invalid = doc.clone();
|
||||||
|
let ObjectType::DynamicBlock(o) = invalid.objects.get_mut(&action).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let DynamicBlockData::StretchAction(a) = &mut o.data else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
match corrupt {
|
||||||
|
0 => a.handles[0].indexes = vec![99],
|
||||||
|
1 => a.offsets.distance_multiplier = 2.,
|
||||||
|
_ => a.connections[0].name = "BaseXDelta".into(),
|
||||||
|
}
|
||||||
|
let baseline = invalid.clone();
|
||||||
|
assert!(evaluate(&mut invalid, &baseline, def, p, DVec3::new(12., 0., 0.)).is_err());
|
||||||
|
assert_eq!(invalid, baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_actions_stretch_polar_xy_and_reframed_marker_keep_native_bindings() {
|
||||||
|
for kind in [distance::Kind::Polar, distance::Kind::XY] {
|
||||||
|
let (mut doc, def, targets) = fixture();
|
||||||
|
let p = distance::create(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
kind,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(10., 1., 0.),
|
||||||
|
Vector3::new(5., 2., 0.),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let action = create_with_frame(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
Kind::Stretch,
|
||||||
|
p,
|
||||||
|
&targets[..1],
|
||||||
|
DVec3::new(3., 4., 0.),
|
||||||
|
Some([DVec3::new(1.5, -1., 0.), DVec3::new(2.5, 1., 0.)]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let baseline = doc.clone();
|
||||||
|
evaluate(&mut doc, &baseline, def, p, DVec3::new(12., 4., 0.)).unwrap();
|
||||||
|
let EntityType::Line(line) = doc.get_entity(targets[0]).unwrap() else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(line.start, Vector3::UNIT_X);
|
||||||
|
assert!(line.end.distance(&Vector3::new(4., 3., 0.)) < 1e-8);
|
||||||
|
super::super::block_authoring::edit::move_part(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
action,
|
||||||
|
super::super::block_authoring::edit::Part::Point,
|
||||||
|
Vector3::new(6., 7., 0.),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let frame = frame_outline(&doc, action);
|
||||||
|
assert_eq!(frame[0], Vector3::new(1.5, -1., 0.));
|
||||||
|
reframe(
|
||||||
|
&mut doc,
|
||||||
|
def,
|
||||||
|
&Transform::from_translation(Vector3::new(-2., -3., 0.)),
|
||||||
|
);
|
||||||
|
assert_eq!(frame_outline(&doc, action)[0], Vector3::new(-0.5, -4., 0.));
|
||||||
|
let ObjectType::DynamicBlock(o) = &doc.objects[&action] else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let DynamicBlockData::StretchAction(a) = &o.data else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(a.handles[0].indexes, vec![1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,18 @@ pub fn items(doc: &CadDocument, definition: Handle) -> Vec<Item> {
|
|||||||
result.push(single(p.handle, p.name.clone(), p.def_point));
|
result.push(single(p.handle, p.name.clone(), p.def_point));
|
||||||
}
|
}
|
||||||
for (handle, p, _, _) in crate::scene::block_actions::actions(doc, definition) {
|
for (handle, p, _, _) in crate::scene::block_actions::actions(doc, definition) {
|
||||||
result.push(single(handle, p.element.name.clone(), p.display_location));
|
let mut item = single(handle, p.element.name.clone(), p.display_location);
|
||||||
|
let frame = crate::scene::block_actions::frame_outline(doc, handle);
|
||||||
|
if !frame.is_empty() {
|
||||||
|
item.graphic.lines.extend(
|
||||||
|
frame
|
||||||
|
.iter()
|
||||||
|
.zip(frame.iter().cycle().skip(1))
|
||||||
|
.take(frame.len())
|
||||||
|
.map(|(a, b)| [*a, *b]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.push(item);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -277,6 +288,7 @@ pub fn move_part(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DynamicBlockData::MoveAction(p) => p.action.display_location = at,
|
DynamicBlockData::MoveAction(p) => p.action.display_location = at,
|
||||||
|
DynamicBlockData::StretchAction(p) => p.action.display_location = at,
|
||||||
DynamicBlockData::FlipAction(p) => p.action.display_location = at,
|
DynamicBlockData::FlipAction(p) => p.action.display_location = at,
|
||||||
DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => {
|
DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => {
|
||||||
p.action.action.display_location = at
|
p.action.action.display_location = at
|
||||||
|
|||||||
@@ -1725,7 +1725,10 @@ impl Scene {
|
|||||||
.chain(
|
.chain(
|
||||||
super::block_actions::actions(&self.document, block)
|
super::block_actions::actions(&self.document, block)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, a, _, _)| a.display_location),
|
.flat_map(|(handle, a, _, _)| {
|
||||||
|
std::iter::once(a.display_location)
|
||||||
|
.chain(super::block_actions::frame_outline(&self.document, handle))
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
for point in locations {
|
for point in locations {
|
||||||
let point = glam::DVec3::new(point.x, point.y, point.z);
|
let point = glam::DVec3::new(point.x, point.y, point.z);
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ fn catalog(
|
|||||||
scale_error: Option<&'static str>,
|
scale_error: Option<&'static str>,
|
||||||
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>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let (entries, unavailable) = match tab {
|
let (entries, unavailable) = match tab {
|
||||||
Tab::Parameters => (
|
Tab::Parameters => (
|
||||||
@@ -219,8 +220,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"|"Rotate"|"Flip");
|
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip");
|
||||||
let action_error=match *label {"Scale"=>scale_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,_=>move_error};
|
let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_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)
|
||||||
@@ -266,7 +267,7 @@ fn catalog(
|
|||||||
} else if single {
|
} else if single {
|
||||||
crate::t!(point_error.unwrap_or(if *label=="Basepoint" {"Place or relocate the dynamic block insertion anchor."}else{"Place a Lookup parameter, then use the Lookup action to edit its property table."})).into_owned()
|
crate::t!(point_error.unwrap_or(if *label=="Basepoint" {"Place or relocate the dynamic block insertion anchor."}else{"Place a Lookup parameter, then use the Lookup action to edit its property table."})).into_owned()
|
||||||
} else if geometry_action {
|
} else if geometry_action {
|
||||||
crate::t!(action_error.unwrap_or("Select a parameter endpoint, select the objects it controls, then place the action marker. Use the reference parameter grip in the drawing or Test Block.")).into_owned()
|
crate::t!(action_error.unwrap_or(if *label=="Stretch" {"Select a parameter endpoint, define the stretch frame, select its objects, then place the action marker."}else{"Select a parameter endpoint, select the objects it controls, then place the action marker. Use the reference parameter grip in the drawing or Test Block."})).into_owned()
|
||||||
} else if lookup_action {
|
} else if lookup_action {
|
||||||
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
|
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
|
||||||
} else {
|
} else {
|
||||||
@@ -284,7 +285,7 @@ fn catalog(
|
|||||||
} else if single {
|
} else if single {
|
||||||
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
|
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
|
||||||
} else if geometry_action {
|
} else if geometry_action {
|
||||||
container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action",_=>"authoring-tool-flip-action"}).into()
|
container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action","Stretch"=>"authoring-tool-stretch-action",_=>"authoring-tool-flip-action"}).into()
|
||||||
} else if lookup_action {
|
} else if lookup_action {
|
||||||
container(entry).id("authoring-tool-lookup-table").into()
|
container(entry).id("authoring-tool-lookup-table").into()
|
||||||
} else {
|
} else {
|
||||||
@@ -405,6 +406,7 @@ pub fn view(
|
|||||||
scale_error: Option<&'static str>,
|
scale_error: Option<&'static str>,
|
||||||
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>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
||||||
let label = category.label();
|
let label = category.label();
|
||||||
@@ -463,7 +465,8 @@ pub fn view(
|
|||||||
move_error,
|
move_error,
|
||||||
scale_error,
|
scale_error,
|
||||||
rotate_error,
|
rotate_error,
|
||||||
flip_action_error
|
flip_action_error,
|
||||||
|
stretch_action_error
|
||||||
))
|
))
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill),
|
.height(Fill),
|
||||||
|
|||||||
Reference in new issue
Block a user