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() {
|
||||
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(),
|
||||
);
|
||||
return Task::none();
|
||||
@@ -76,6 +76,7 @@ impl OpenCADStudio {
|
||||
parameter,
|
||||
targets,
|
||||
location,
|
||||
frame,
|
||||
} => {
|
||||
if self.tabs[i].id != document
|
||||
|| self.tabs[i]
|
||||
@@ -90,8 +91,15 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
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(_) => {
|
||||
self.push_undo_snapshot(i, &format!("{} action", kind.name()));
|
||||
self.tabs[i].scene.document = staged;
|
||||
|
||||
@@ -59,6 +59,138 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
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]
|
||||
fn block_parameters_rotation_only_save_close_reopen_then_flip() {
|
||||
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,
|
||||
targets: members(&app),
|
||||
location: glam::DVec3::ZERO,
|
||||
frame: None,
|
||||
});
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
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]
|
||||
fn block_actions_rotate_flip_palette_requires_matching_parameters() {
|
||||
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::Rotate)),
|
||||
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 => {
|
||||
|
||||
+12
-1
@@ -1203,8 +1203,19 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(_, action, _, _)| {
|
||||
.map(|(handle, action, _, _)| {
|
||||
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.labels
|
||||
.push((action.display_location, action.element.name.clone()));
|
||||
|
||||
@@ -1528,6 +1528,7 @@ pub enum CmdResult {
|
||||
parameter: Handle,
|
||||
targets: Vec<Handle>,
|
||||
location: DVec3,
|
||||
frame: Option<[DVec3; 2]>,
|
||||
},
|
||||
EvaluateBlockParameter {
|
||||
insert: Option<Handle>,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) struct ActionCommand {
|
||||
parameter: Option<Handle>,
|
||||
targets: Vec<Handle>,
|
||||
selecting: bool,
|
||||
frame: Vec<DVec3>,
|
||||
context: Option<PointPickContext>,
|
||||
}
|
||||
impl ActionCommand {
|
||||
@@ -33,6 +34,7 @@ impl ActionCommand {
|
||||
parameter: None,
|
||||
targets: vec![],
|
||||
selecting: false,
|
||||
frame: vec![],
|
||||
context: None,
|
||||
}
|
||||
}
|
||||
@@ -43,7 +45,7 @@ impl ActionCommand {
|
||||
);
|
||||
}
|
||||
self.parameter = Some(p.handle);
|
||||
self.selecting = true;
|
||||
self.selecting = self.kind != Some(Kind::Stretch);
|
||||
CmdResult::DeselectAndContinue
|
||||
}
|
||||
}
|
||||
@@ -62,7 +64,8 @@ impl CadCommand for ActionCommand {
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
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() {
|
||||
return crate::tf!(
|
||||
@@ -71,11 +74,17 @@ impl CadCommand for ActionCommand {
|
||||
)
|
||||
.into_owned();
|
||||
}
|
||||
crate::t!(if self.selecting {
|
||||
"BACTION Select objects for the action, then press Enter:"
|
||||
} else {
|
||||
"BACTION Specify action marker location:"
|
||||
})
|
||||
crate::t!(
|
||||
if self.kind == Some(Kind::Stretch) && self.frame.is_empty() {
|
||||
"BACTION Specify first corner of stretch frame:"
|
||||
} 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()
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
@@ -83,6 +92,7 @@ impl CadCommand for ActionCommand {
|
||||
vec![
|
||||
CmdOption::new("Move", "Move"),
|
||||
CmdOption::new("Scale", "Scale"),
|
||||
CmdOption::new("Stretch", "Stretch"),
|
||||
CmdOption::new("Rotate", "Rotate"),
|
||||
CmdOption::new("Flip", "Flip"),
|
||||
]
|
||||
@@ -97,7 +107,7 @@ impl CadCommand for ActionCommand {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
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 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
@@ -164,8 +203,26 @@ impl CadCommand for ActionCommand {
|
||||
parameter: self.parameter.unwrap(),
|
||||
targets: self.targets.clone(),
|
||||
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 {
|
||||
self.selecting
|
||||
}
|
||||
|
||||
@@ -277,7 +277,13 @@ pub fn stretch_entity(
|
||||
|
||||
/// The bulge of the segment from vertex `k` once its ends moved from
|
||||
/// `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() {
|
||||
k + 1
|
||||
} 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 codec::objects::{
|
||||
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
|
||||
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, BlockFlipAction,
|
||||
DynamicBlockData, ObjectType,
|
||||
BlockStretchAction, DynamicBlockData, ObjectType,
|
||||
};
|
||||
use codec::types::{Transform, Vector3};
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
pub(crate) mod instance;
|
||||
mod stretch;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -18,6 +19,7 @@ pub enum Kind {
|
||||
Scale,
|
||||
Rotate,
|
||||
Flip,
|
||||
Stretch,
|
||||
}
|
||||
impl Kind {
|
||||
pub(crate) fn parse(s: &str) -> Option<Self> {
|
||||
@@ -26,6 +28,7 @@ impl Kind {
|
||||
"SCALE" | "S" => Some(Self::Scale),
|
||||
"ROTATE" | "R" => Some(Self::Rotate),
|
||||
"FLIP" | "F" => Some(Self::Flip),
|
||||
"STRETCH" | "ST" => Some(Self::Stretch),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -35,6 +38,7 @@ impl Kind {
|
||||
Self::Scale => "Scale",
|
||||
Self::Rotate => "Rotate",
|
||||
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 {
|
||||
match kind {
|
||||
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::Rotate => p.rotation,
|
||||
Kind::Flip => p.flip,
|
||||
@@ -342,6 +347,7 @@ pub(crate) fn supported(e: &EntityType) -> bool {
|
||||
| EntityType::MText(_)
|
||||
)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn create(
|
||||
doc: &mut CadDocument,
|
||||
definition: Handle,
|
||||
@@ -349,6 +355,18 @@ pub(crate) fn create(
|
||||
parameter: Handle,
|
||||
targets: &[Handle],
|
||||
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> {
|
||||
if !location.is_finite() {
|
||||
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.");
|
||||
}
|
||||
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 (graph, slots) = authoring::prepare(&mut staged, definition)?;
|
||||
let (dxf, cpp) = match kind {
|
||||
@@ -381,6 +412,7 @@ pub(crate) fn create(
|
||||
Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"),
|
||||
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
|
||||
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
|
||||
Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"),
|
||||
};
|
||||
register_classes(&mut staged, &[(dxf, cpp, 55)]);
|
||||
let handle = staged.allocate_handle();
|
||||
@@ -404,6 +436,21 @@ pub(crate) fn create(
|
||||
name: name.into(),
|
||||
};
|
||||
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 {
|
||||
action,
|
||||
connections: if p.point {
|
||||
@@ -489,6 +536,9 @@ pub(crate) fn actions(
|
||||
DynamicBlockData::MoveAction(a) => {
|
||||
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) => {
|
||||
Some((o.handle, &a.action, Kind::Flip, a.connections[0].code))
|
||||
}
|
||||
@@ -505,6 +555,16 @@ pub(crate) fn actions(
|
||||
})
|
||||
.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 {
|
||||
let handles = actions(doc, definition)
|
||||
.iter()
|
||||
@@ -518,6 +578,17 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform)
|
||||
DynamicBlockData::MoveAction(a) => {
|
||||
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) => {
|
||||
a.action.display_location = t.apply(a.action.display_location)
|
||||
}
|
||||
@@ -613,8 +684,10 @@ pub(crate) fn evaluate_value(
|
||||
) -> Result<bool, &'static str> {
|
||||
let graph = authoring::graph(baseline, definition)?
|
||||
.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,
|
||||
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::ArrayAction(_)))) {
|
||||
if baseline.objects.values().any(|o| {
|
||||
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.");
|
||||
}
|
||||
let p = all_parameters(baseline, definition)
|
||||
@@ -660,7 +733,16 @@ pub(crate) fn evaluate_value(
|
||||
set_value(&mut staged, &p, input)?;
|
||||
let all = actions(baseline, definition);
|
||||
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());
|
||||
}
|
||||
for h in affected {
|
||||
@@ -688,7 +770,9 @@ pub(crate) fn evaluate_value(
|
||||
}
|
||||
let end = current(&staged, &q);
|
||||
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 => {
|
||||
let a = 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();
|
||||
if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() {
|
||||
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.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));
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -277,6 +288,7 @@ pub fn move_part(
|
||||
}
|
||||
}
|
||||
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::ScaleAction(p) | DynamicBlockData::RotateAction(p) => {
|
||||
p.action.action.display_location = at
|
||||
|
||||
@@ -1725,7 +1725,10 @@ impl Scene {
|
||||
.chain(
|
||||
super::block_actions::actions(&self.document, block)
|
||||
.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 {
|
||||
let point = glam::DVec3::new(point.x, point.y, point.z);
|
||||
|
||||
@@ -198,6 +198,7 @@ fn catalog(
|
||||
scale_error: Option<&'static str>,
|
||||
rotate_error: Option<&'static str>,
|
||||
flip_action_error: Option<&'static str>,
|
||||
stretch_action_error: Option<&'static str>,
|
||||
) -> Element<'static, Message> {
|
||||
let (entries, unavailable) = match tab {
|
||||
Tab::Parameters => (
|
||||
@@ -219,8 +220,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"|"Rotate"|"Flip");
|
||||
let action_error=match *label {"Scale"=>scale_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,_=>move_error};
|
||||
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip");
|
||||
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 base = if enabled {
|
||||
icons::semantic(icon, 28.0)
|
||||
@@ -266,7 +267,7 @@ fn catalog(
|
||||
} 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()
|
||||
} 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 {
|
||||
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
|
||||
} else {
|
||||
@@ -284,7 +285,7 @@ fn catalog(
|
||||
} else if single {
|
||||
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
|
||||
} 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 {
|
||||
container(entry).id("authoring-tool-lookup-table").into()
|
||||
} else {
|
||||
@@ -405,6 +406,7 @@ pub fn view(
|
||||
scale_error: Option<&'static str>,
|
||||
rotate_error: Option<&'static str>,
|
||||
flip_action_error: Option<&'static str>,
|
||||
stretch_action_error: Option<&'static str>,
|
||||
) -> Element<'static, Message> {
|
||||
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
||||
let label = category.label();
|
||||
@@ -463,7 +465,8 @@ pub fn view(
|
||||
move_error,
|
||||
scale_error,
|
||||
rotate_error,
|
||||
flip_action_error
|
||||
flip_action_error,
|
||||
stretch_action_error
|
||||
))
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
|
||||
Reference in new issue
Block a user