forked from 1b-agent/opencadstudio-upstream
Merge upstream main; codec and kernel patches on branches merged with it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
0158ea533f
48 files changed
+5133
-52
No files matched your search
@@ -0,0 +1,265 @@
|
||||
use super::{Message, OpenCADStudio};
|
||||
use crate::command::CadCommand;
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
use codec::{EntityType, Handle};
|
||||
use iced::Task;
|
||||
|
||||
// These grips start an ordinary point-input command, keeping snapping, typed
|
||||
// coordinates and Escape on the command driver's existing path.
|
||||
const TEST_GRIP_START: usize = usize::MAX / 2;
|
||||
pub(super) fn is_test_grip(id: usize) -> bool {
|
||||
(TEST_GRIP_START..usize::MAX).contains(&id)
|
||||
}
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn action_error(&self, kind: Option<Kind>) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(s) = tab.active_block_edit_session() else {
|
||||
return Some("Open a block in the Block Editor before adding actions.");
|
||||
};
|
||||
if let Some(e) =
|
||||
crate::scene::block_point_parameter::placement_error(&tab.scene.document, s.br_handle)
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
if actions::parameters(&tab.scene.document, s.br_handle, kind.unwrap_or(Kind::Move))
|
||||
.is_empty()
|
||||
{
|
||||
return Some("Place a compatible parameter before adding this action.");
|
||||
}
|
||||
None
|
||||
}
|
||||
pub(in crate::app) fn start_block_action(&mut self, args: &str) -> Task<Message> {
|
||||
let kind = if args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Kind::parse(args)
|
||||
};
|
||||
if !args.is_empty() && kind.is_none() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Supported actions: Move, Scale. Other action types are not wired yet.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(e) = self.action_error(kind) {
|
||||
self.command_line.push_error(crate::t!(e).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let definition = tab.active_block_edit_session().unwrap().br_handle;
|
||||
let command = crate::modules::block_editor::action::ActionCommand::new(
|
||||
tab.id,
|
||||
definition,
|
||||
kind,
|
||||
actions::parameters(&tab.scene.document, definition, kind.unwrap_or(Kind::Move)),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.command_line.set_step_options(command.options());
|
||||
self.tabs[self.active_tab].active_cmd = Some(Box::new(command));
|
||||
Task::none()
|
||||
}
|
||||
pub(in crate::app) fn handle_block_action_result(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
match result {
|
||||
crate::command::CmdResult::PlaceBlockAction {
|
||||
document,
|
||||
definition,
|
||||
kind,
|
||||
parameter,
|
||||
targets,
|
||||
location,
|
||||
} => {
|
||||
if self.tabs[i].id != document
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|s| s.br_handle)
|
||||
!= Some(definition)
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("The action's Block Editor session is no longer active.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let mut staged = self.tabs[i].scene.document.clone();
|
||||
match actions::create(&mut staged, definition, kind, parameter, &targets, location)
|
||||
{
|
||||
Ok(_) => {
|
||||
self.push_undo_snapshot(i, &format!("{} action", kind.name()));
|
||||
self.tabs[i].scene.document = staged;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(crate::tf!("{} action attached. Open Test Block and click the parameter grip to test it.",crate::t!(kind.name())).as_ref());
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
}
|
||||
}
|
||||
crate::command::CmdResult::EvaluateBlockParameter {
|
||||
document,
|
||||
definition,
|
||||
parameter,
|
||||
position,
|
||||
} => {
|
||||
if self.tabs[i].id != document {
|
||||
return Task::none();
|
||||
}
|
||||
let Some(test) = self.tabs[i].block_test.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
if test.definition != definition {
|
||||
return Task::none();
|
||||
}
|
||||
let Some(EntityType::Insert(insert)) =
|
||||
self.tabs[i].scene.document.get_entity(test.insert)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
if doc.entities().any(|e|matches!(e,EntityType::Insert(other) if other.common.handle!=test.insert && (other.block_name==insert.block_name||doc.dynamic_definition_for_insert(other.common.handle)==Some(definition)))) {
|
||||
self.command_line.push_error(crate::t!("Test actions with one reference in Test Block. Remove the extra references or reopen Test Block.").as_ref());return Task::none();
|
||||
}
|
||||
let Some(inverse) =
|
||||
crate::scene::inverse_affine(&crate::scene::render_graph::insert_transform(
|
||||
&self.tabs[i].scene.document,
|
||||
insert,
|
||||
))
|
||||
else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("The test reference has a singular transform.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let local = actions::v(inverse.apply(actions::native(position)));
|
||||
let representation = insert.block_name.clone();
|
||||
let mut staged = self.tabs[i].scene.document.clone();
|
||||
match actions::evaluate(&mut staged, &test.baseline, definition, parameter, local) {
|
||||
Ok(changed) => {
|
||||
if changed {
|
||||
if let Err(e) = actions::sync_representation(
|
||||
&mut staged,
|
||||
definition,
|
||||
&representation,
|
||||
) {
|
||||
self.command_line.push_error(crate::t!(e).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.finish_pending_history(i);
|
||||
let before = &self.tabs[i].scene.document;
|
||||
let entities = before
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
let h = e.common().handle;
|
||||
(staged.get_entity(h) != Some(e))
|
||||
.then(|| (h, before.get_entity_arc(h).unwrap()))
|
||||
})
|
||||
.collect();
|
||||
let objects = before
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(h, o)| staged.objects.get(h) != Some(*o))
|
||||
.map(|(h, o)| (*h, o.clone()))
|
||||
.collect();
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
self.tabs[i].scene.document = staged;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
"Test block parameter",
|
||||
entities,
|
||||
objects,
|
||||
dirty_before,
|
||||
);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_properties();
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
pub(super) fn refresh_test_action_grips(&mut self, wo: [f64; 3]) {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let Some(test) = tab.block_test.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if tab.selected_handle != Some(test.insert) {
|
||||
return;
|
||||
}
|
||||
let Some(EntityType::Insert(insert)) = tab.scene.document.get_entity(test.insert) else {
|
||||
return;
|
||||
};
|
||||
let transform = crate::scene::render_graph::insert_transform(&tab.scene.document, insert);
|
||||
let mut grips = vec![];
|
||||
let all = actions::actions(&test.baseline, test.definition);
|
||||
for (index, p) in actions::parameters(&test.baseline, test.definition, Kind::Move)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
|
||||
continue;
|
||||
}
|
||||
let world = actions::v(
|
||||
transform.apply(actions::native(actions::current(&tab.scene.document, &p))),
|
||||
) - glam::DVec3::from_array(wo);
|
||||
grips.push(crate::scene::model::object::GripDef {
|
||||
id: TEST_GRIP_START + index,
|
||||
world,
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::Triangle,
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
}
|
||||
let handle = test.insert;
|
||||
let tab = &mut self.tabs[i];
|
||||
for grip in grips {
|
||||
tab.selected_grips.push(grip);
|
||||
tab.selected_grip_handles.push(handle);
|
||||
}
|
||||
}
|
||||
pub(super) fn start_test_action_grip(&mut self, handle: Handle, id: usize) -> bool {
|
||||
if !is_test_grip(id) {
|
||||
return false;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let Some(test) = tab.block_test.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
if handle != test.insert {
|
||||
return false;
|
||||
}
|
||||
if tab.scene.is_layer_locked(handle) {
|
||||
return true;
|
||||
}
|
||||
let Some(p) = actions::parameters(&test.baseline, test.definition, Kind::Move)
|
||||
.get(id - TEST_GRIP_START)
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let command = crate::modules::block_editor::action::TestParameterCommand {
|
||||
document: tab.id,
|
||||
definition: test.definition,
|
||||
parameter: p.handle,
|
||||
};
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
use super::{Message, OpenCADStudio};
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::block_authoring::edit::{self, Part};
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
use iced::{Point, Rectangle, Task};
|
||||
|
||||
const EDIT_GRIP_START: usize = usize::MAX / 4;
|
||||
pub(super) fn is_editor_grip(id: usize) -> bool {
|
||||
(EDIT_GRIP_START..EDIT_GRIP_START + 16).contains(&id)
|
||||
}
|
||||
struct EditCommand {
|
||||
document: u64,
|
||||
definition: Handle,
|
||||
handle: Handle,
|
||||
part: Part,
|
||||
}
|
||||
impl CadCommand for EditCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BEDITGRIP"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!("Block Editor Specify the new authoring grip location (Escape cancels):")
|
||||
.into_owned()
|
||||
}
|
||||
fn on_point(&mut self, position: DVec3) -> CmdResult {
|
||||
CmdResult::EditBlockAuthoring {
|
||||
document: self.document,
|
||||
definition: self.definition,
|
||||
handle: self.handle,
|
||||
part: self.part,
|
||||
position,
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn pick_authoring_object(&mut self, p: Point, bounds: Rectangle) -> bool {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
if tab.active_cmd.is_some() || tab.active_grip.is_some() {
|
||||
return false;
|
||||
}
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
return false;
|
||||
};
|
||||
let items = edit::items(&tab.scene.document, session.br_handle);
|
||||
let camera = tab.scene.camera.borrow();
|
||||
let project = |v: codec::types::Vector3| {
|
||||
camera
|
||||
.project(DVec3::new(v.x, v.y, v.z), bounds)
|
||||
.map(|v| Point::new(v.x, v.y))
|
||||
};
|
||||
let segment = |a: Point, b: Point| {
|
||||
let ab = glam::Vec2::new(b.x - a.x, b.y - a.y);
|
||||
let ap = glam::Vec2::new(p.x - a.x, p.y - a.y);
|
||||
let t = (ap.dot(ab) / ab.length_squared().max(1e-12)).clamp(0., 1.);
|
||||
(ap - ab * t).length()
|
||||
};
|
||||
let hit = items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let mut d = f32::INFINITY;
|
||||
for (_, v) in &item.grips {
|
||||
if let Some(v) = project(*v) {
|
||||
d = d.min(segment(v, v));
|
||||
}
|
||||
}
|
||||
for [a, b] in &item.graphic.lines {
|
||||
if let (Some(a), Some(b)) = (project(*a), project(*b)) {
|
||||
d = d.min(segment(a, b));
|
||||
}
|
||||
}
|
||||
for (v, label) in &item.graphic.labels {
|
||||
if let Some(v) = project(*v) {
|
||||
// Labels are drawn starting beside their anchor, in screen pixels.
|
||||
let width = (label.chars().count() as f32 * 7.).max(20.);
|
||||
if p.x >= v.x - 8. && p.x <= v.x + width + 12. && (p.y - v.y).abs() < 16. {
|
||||
d = d.min(0.);
|
||||
}
|
||||
}
|
||||
}
|
||||
(d <= 10.).then_some((d, item.handle, item.name.clone()))
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0));
|
||||
drop(camera);
|
||||
if let Some((_, handle, name)) = hit {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].selected_authoring = (!self.shift_down).then_some(handle);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.selection
|
||||
.borrow_mut()
|
||||
.clear_left_selection_gesture();
|
||||
self.refresh_properties();
|
||||
self.command_line.push_info(&format!(
|
||||
"{} — {}",
|
||||
name,
|
||||
crate::t!("Select a grip to edit its location.")
|
||||
));
|
||||
true
|
||||
} else {
|
||||
if self.tabs[i].selected_authoring.take().is_some() {
|
||||
self.refresh_properties();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
pub(super) fn refresh_authoring_grips(&mut self, wo: [f64; 3]) {
|
||||
let i = self.active_tab;
|
||||
let Some(handle) = self.tabs[i].selected_authoring else {
|
||||
return;
|
||||
};
|
||||
let item = self.tabs[i].active_block_edit_session().and_then(|s| {
|
||||
edit::items(&self.tabs[i].scene.document, s.br_handle)
|
||||
.into_iter()
|
||||
.find(|p| p.handle == handle)
|
||||
});
|
||||
let Some(item) = item.filter(|_| self.tabs[i].scene.selected.is_empty()) else {
|
||||
self.tabs[i].selected_authoring = None;
|
||||
return;
|
||||
};
|
||||
for (index, (_, point)) in item.grips.into_iter().enumerate() {
|
||||
self.tabs[i]
|
||||
.selected_grips
|
||||
.push(crate::scene::model::object::GripDef {
|
||||
id: EDIT_GRIP_START + index,
|
||||
world: DVec3::new(point.x - wo[0], point.y - wo[1], point.z - wo[2]),
|
||||
is_midpoint: false,
|
||||
shape: crate::scene::model::object::GripShape::Square,
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
self.tabs[i].selected_grip_handles.push(handle);
|
||||
}
|
||||
}
|
||||
pub(super) fn start_authoring_grip(&mut self, handle: Handle, id: usize) -> bool {
|
||||
if !is_editor_grip(id) {
|
||||
return false;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
return false;
|
||||
};
|
||||
let Some(part) = edit::items(&self.tabs[i].scene.document, session.br_handle)
|
||||
.into_iter()
|
||||
.find(|p| p.handle == handle)
|
||||
.and_then(|p| p.grips.get(id - EDIT_GRIP_START).map(|(part, _)| *part))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let command = EditCommand {
|
||||
document: self.tabs[i].id,
|
||||
definition: session.br_handle,
|
||||
handle,
|
||||
part,
|
||||
};
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
true
|
||||
}
|
||||
pub(super) fn handle_authoring_edit(&mut self, result: CmdResult) -> Task<Message> {
|
||||
let CmdResult::EditBlockAuthoring {
|
||||
document,
|
||||
definition,
|
||||
handle,
|
||||
part,
|
||||
position,
|
||||
} = result
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].id != document
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|s| s.br_handle)
|
||||
!= Some(definition)
|
||||
{
|
||||
self.command_line
|
||||
.push_error(crate::t!("The Block Editor session is no longer active.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let mut staged = self.tabs[i].scene.document.clone();
|
||||
match edit::move_part(
|
||||
&mut staged,
|
||||
definition,
|
||||
handle,
|
||||
part,
|
||||
codec::types::Vector3::new(position.x, position.y, position.z),
|
||||
) {
|
||||
Ok(changed) => {
|
||||
if changed {
|
||||
self.push_undo_snapshot(i, "Block authoring grip");
|
||||
// Keep the live entity-change recorder attached. Basepoint edits
|
||||
// can also relocate INSERT attributes outside the edited block.
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
let changed_entities = staged
|
||||
.entities()
|
||||
.filter(|e| doc.get_entity(e.common().handle) != Some(*e))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for entity in changed_entities {
|
||||
let handle = entity.common().handle;
|
||||
*doc.get_entity_mut(handle)
|
||||
.expect("authoring edits retain entity identities") = entity;
|
||||
}
|
||||
for (h, o) in staged.objects.iter() {
|
||||
if doc.objects.get(h) != Some(o) {
|
||||
doc.objects.insert(*h, o.clone());
|
||||
}
|
||||
}
|
||||
doc.block_visibility_params = staged.block_visibility_params.clone();
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.finish_pending_history(i);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_properties();
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,565 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoring_edit_basepoint_attribute_relocation_undo_and_cancel() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER BASEPOINT 2,3");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let handle = crate::scene::block_basepoint::parameter(&app.tabs[i].scene.document, def)
|
||||
.unwrap()
|
||||
.0;
|
||||
let mut attribute = codec::entities::AttributeEntity::new("TAG".into(), "value".into());
|
||||
attribute.insertion_point = Vector3::new(7., 8., 0.);
|
||||
let EntityType::Insert(insert) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(reference)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
insert.attributes.push(attribute);
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let result = crate::command::CmdResult::EditBlockAuthoring {
|
||||
document: app.tabs[i].id,
|
||||
definition: def,
|
||||
handle,
|
||||
part: crate::scene::block_authoring::edit::Part::Point,
|
||||
position: glam::DVec3::new(4., 6., 0.),
|
||||
};
|
||||
let _ = app.handle_authoring_edit(result);
|
||||
assert_ne!(
|
||||
app.tabs[i].scene.document.get_entity(reference),
|
||||
before.get_entity(reference)
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
app.redo_steps(1);
|
||||
assert_ne!(
|
||||
app.tabs[i].scene.document.get_entity(reference),
|
||||
before.get_entity(reference)
|
||||
);
|
||||
app.tabs[i].selected_authoring = Some(handle);
|
||||
app.refresh_properties();
|
||||
let id = app.tabs[i].selected_grips[0].id;
|
||||
assert!(app.start_authoring_grip(handle, id));
|
||||
let after = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].scene.document, after);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].selected_authoring, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoring_edit_mouse_selection_endpoint_label_marker_history_and_reopen() {
|
||||
use crate::scene::block_authoring::edit::{self, Part};
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 2,2 6,2 4,3");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].clone();
|
||||
let targets = members(&app);
|
||||
let a = actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p.handle,
|
||||
&targets,
|
||||
glam::DVec3::new(5., 5., 0.),
|
||||
)
|
||||
.unwrap();
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.);
|
||||
app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64(
|
||||
glam::DVec3::new(-1., -1., 0.),
|
||||
glam::DVec3::new(10., 7., 0.),
|
||||
1600. / 900.,
|
||||
);
|
||||
let click = |app: &mut OpenCADStudio, point: Vector3| {
|
||||
let scene = &app.tabs[app.active_tab].scene;
|
||||
let (w, h) = scene.selection.borrow().view.vp_size;
|
||||
let b = scene.active_model_tile_bounds(w, h);
|
||||
let s = scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
iced::Rectangle {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
width: b.width,
|
||||
height: b.height,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.update(Message::ViewportLeftPressAt(iced::Point::new(
|
||||
s.x + b.x,
|
||||
s.y + b.y,
|
||||
)));
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
};
|
||||
click(&mut app, Vector3::new(4., 3., 0.));
|
||||
assert_eq!(app.tabs[i].selected_authoring, Some(p.handle));
|
||||
assert_eq!(app.tabs[i].selected_grips.len(), 3);
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
click(&mut app, Vector3::new(6., 2., 0.));
|
||||
assert_eq!(
|
||||
app.tabs[i].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("BEDITGRIP")
|
||||
);
|
||||
feed(&mut app, "8,2");
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
assert_eq!(
|
||||
actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].end,
|
||||
glam::DVec3::new(8., 2., 0.)
|
||||
);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
app.redo_steps(1);
|
||||
let label = edit::items(&app.tabs[i].scene.document, def)
|
||||
.into_iter()
|
||||
.find(|i| i.handle == p.handle)
|
||||
.unwrap()
|
||||
.grips
|
||||
.into_iter()
|
||||
.find(|(p, _)| *p == Part::Label(0))
|
||||
.unwrap()
|
||||
.1;
|
||||
click(&mut app, label);
|
||||
feed(&mut app, "5,4");
|
||||
let graphic = edit::items(&app.tabs[i].scene.document, def)
|
||||
.into_iter()
|
||||
.find(|i| i.handle == p.handle)
|
||||
.unwrap();
|
||||
assert_eq!(graphic.graphic.labels[0].0, Vector3::new(5., 4., 0.));
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
click(&mut app, Vector3::new(5., 5., 0.));
|
||||
assert_eq!(app.tabs[i].selected_authoring, Some(a));
|
||||
click(&mut app, Vector3::new(5., 5., 0.));
|
||||
feed(&mut app, "7,5");
|
||||
let action = actions::actions(&app.tabs[i].scene.document, def);
|
||||
assert_eq!(action[0].1.display_location, Vector3::new(7., 5., 0.));
|
||||
assert_eq!(action[0].1.dependencies, targets);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert!(app.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.all(|g| !super::block_authoring_edit::is_editor_grip(g.id)));
|
||||
// Persisted native coordinates, rather than session-only display positions.
|
||||
use std::io::Cursor;
|
||||
let bytes = codec::DwgWriter::write_to_vec(&app.tabs[i].scene.document).unwrap();
|
||||
let loaded = codec::DwgReader::from_stream(Cursor::new(bytes))
|
||||
.read()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
actions::parameters(&loaded, def, Kind::Move)[0].end,
|
||||
glam::DVec3::new(8., 2., 0.)
|
||||
);
|
||||
assert_eq!(
|
||||
actions::actions(&loaded, def)[0].1.display_location,
|
||||
Vector3::new(7., 5., 0.)
|
||||
);
|
||||
app.tabs[i].scene.document = loaded;
|
||||
begin(&mut app, reference);
|
||||
click(&mut app, Vector3::new(8., 2., 0.));
|
||||
assert_eq!(app.tabs[i].selected_authoring, Some(p.handle));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_mouse_selection_waits_for_enter_for_move_and_scale() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
for (kind, command) in [(Kind::Move, "BACTION MOVE"), (Kind::Scale, "BACTION SCALE")] {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let source = app.active_tab;
|
||||
let _ = app.run_command_line("CIRCLE 0.5,0.5 0.2");
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 2,0 1,-0.2");
|
||||
let definition = app.tabs[source]
|
||||
.active_block_edit_session()
|
||||
.unwrap()
|
||||
.br_handle;
|
||||
app.tabs[source].scene.selection.borrow_mut().view.vp_size = (1600., 900.);
|
||||
app.tabs[source]
|
||||
.scene
|
||||
.camera
|
||||
.borrow_mut()
|
||||
.fit_to_bounds_f64(
|
||||
glam::DVec3::new(-0.5, -0.5, 0.),
|
||||
glam::DVec3::new(2.5, 1.5, 0.),
|
||||
1600. / 900.,
|
||||
);
|
||||
let _ = app.dispatch_command(command);
|
||||
let click = |app: &mut OpenCADStudio, point: [f64; 3], modifiers: &str| {
|
||||
let scene = &app.tabs[app.active_tab].scene;
|
||||
let (vw, vh) = scene.selection.borrow().view.vp_size;
|
||||
let tile = scene.active_model_tile_bounds(vw, vh);
|
||||
let screen = scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(
|
||||
glam::DVec3::from_array(point),
|
||||
iced::Rectangle {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let cursor = iced::Point::new(screen.x + tile.x, screen.y + tile.y);
|
||||
let previous = (app.ctrl_down, app.shift_down);
|
||||
app.ctrl_down = modifiers.contains("ctrl");
|
||||
app.shift_down = modifiers.contains("shift");
|
||||
let _ = app.update(Message::ViewportLeftPressAt(cursor));
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
(app.ctrl_down, app.shift_down) = previous;
|
||||
};
|
||||
click(&mut app, [2., 0., 0.], "");
|
||||
assert!(app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering());
|
||||
feed(&mut app, ""); // Empty selection does not cancel or advance.
|
||||
assert!(app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering());
|
||||
for (point, modifiers, count) in [
|
||||
([0.5, 0., 0.], "", 1),
|
||||
([0.5, 0.7, 0.], "", 2),
|
||||
([0.5, 0., 0.], "shift", 1),
|
||||
([0.5, 0., 0.], "", 2),
|
||||
] {
|
||||
click(&mut app, point, modifiers);
|
||||
assert!(
|
||||
app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering(),
|
||||
"every mouse pick/removal stays in object selection"
|
||||
);
|
||||
assert_eq!(app.tabs[source].scene.selected.len(), count);
|
||||
assert!(actions::actions(&app.tabs[source].scene.document, definition).is_empty());
|
||||
}
|
||||
feed(&mut app, "");
|
||||
assert!(!app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering());
|
||||
click(&mut app, [1.5, 1., 0.], "");
|
||||
assert!(app.tabs[source].active_cmd.is_none());
|
||||
let authored = actions::actions(&app.tabs[source].scene.document, definition);
|
||||
assert_eq!(authored.len(), 1);
|
||||
assert_eq!(authored[0].2, kind);
|
||||
assert_eq!(authored[0].1.dependencies.len(), 2);
|
||||
let original = app.tabs[source].scene.document.clone();
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.);
|
||||
app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64(
|
||||
glam::DVec3::new(-0.5, -0.5, 0.),
|
||||
glam::DVec3::new(4.5, 1.5, 0.),
|
||||
1600. / 900.,
|
||||
);
|
||||
click(&mut app, [0.5, 0., 0.], "");
|
||||
click(&mut app, [2., 0., 0.], "");
|
||||
assert_eq!(
|
||||
app.tabs[i].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("BTESTBLOCK")
|
||||
);
|
||||
click(&mut app, [4., 0., 0.], "");
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
let circle = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities_in_block("EDIT_ME")
|
||||
.find_map(|e| {
|
||||
if let EntityType::Circle(c) = e {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let (center, radius) = if kind == Kind::Move {
|
||||
(Vector3::new(2.5, 0.5, 0.), 0.2)
|
||||
} else {
|
||||
(Vector3::new(1., 1., 0.), 0.4)
|
||||
};
|
||||
// Mouse projection uses f32 screen coordinates; allow sub-pixel roundoff.
|
||||
assert!(
|
||||
(circle.center - center).length() < 1e-5,
|
||||
"{kind:?}: actual {:?}, expected {center:?}",
|
||||
circle.center
|
||||
);
|
||||
assert!((circle.radius - radius).abs() < 1e-5);
|
||||
assert_eq!(app.tabs[source].scene.document, original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_command_selection_cancel_undo_and_test_block_isolation() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let source = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let definition = app.tabs[source]
|
||||
.active_block_edit_session()
|
||||
.unwrap()
|
||||
.br_handle;
|
||||
let p =
|
||||
actions::parameters(&app.tabs[source].scene.document, definition, Kind::Move)[0].clone();
|
||||
let targets = members(&app);
|
||||
let before = app.tabs[source].scene.document.clone();
|
||||
let depth = app.tabs[source].history.undo_stack.len();
|
||||
let _ = app.dispatch_command("BACTION MOVE");
|
||||
feed(&mut app, &p.name);
|
||||
assert!(app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering());
|
||||
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
|
||||
targets.clone(),
|
||||
));
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[source].scene.document, before);
|
||||
assert_eq!(app.tabs[source].history.undo_stack.len(), depth);
|
||||
for kind in ["Move", "Scale"] {
|
||||
let _ = app.dispatch_command(&format!("BACTION {kind}"));
|
||||
feed(&mut app, &p.name);
|
||||
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
|
||||
targets.clone(),
|
||||
));
|
||||
assert!(app.tabs[source]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering());
|
||||
let _ = app.feed_command(crate::command::StepInput::Enter);
|
||||
feed(&mut app, "3,4");
|
||||
assert!(app.tabs[source].active_cmd.is_none());
|
||||
}
|
||||
assert_eq!(
|
||||
actions::actions(&app.tabs[source].scene.document, definition).len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(app.tabs[source].history.undo_stack.len(), depth + 2);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
actions::actions(&app.tabs[source].scene.document, definition).len(),
|
||||
1
|
||||
);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
actions::actions(&app.tabs[source].scene.document, definition).len(),
|
||||
2
|
||||
);
|
||||
let original = app.tabs[source].scene.document.clone();
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
let test = app.active_tab;
|
||||
let insert = app.tabs[test].block_test.as_ref().unwrap().insert;
|
||||
app.tabs[test].selected_handle = Some(insert);
|
||||
app.refresh_test_action_grips([0.; 3]);
|
||||
let grip = app.tabs[test].selected_grips.last().unwrap().id;
|
||||
assert!(app.start_test_action_grip(insert, grip));
|
||||
feed(&mut app, "20,5");
|
||||
let EntityType::Line(line) = app.tabs[test]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(targets[0])
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(line.start, Vector3::new(20., 0., 0.));
|
||||
assert_eq!(line.end, Vector3::new(22., 0., 0.));
|
||||
assert_eq!(app.tabs[source].scene.document, original);
|
||||
app.undo_steps(1);
|
||||
let EntityType::Line(line) = app.tabs[test]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(targets[0])
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(line.start, Vector3::ZERO);
|
||||
app.redo_steps(1);
|
||||
let EntityType::Line(line) = app.tabs[test]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(targets[0])
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(line.start, Vector3::new(20., 0., 0.));
|
||||
app.tabs[test]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(Insert::new(
|
||||
"EDIT_ME",
|
||||
Vector3::new(50., 0., 0.),
|
||||
)))
|
||||
.unwrap();
|
||||
let shared_before = app.tabs[test].scene.document.clone();
|
||||
let id = app.tabs[test].id;
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: glam::DVec3::new(30., 0., 0.),
|
||||
});
|
||||
assert_eq!(
|
||||
app.tabs[test].scene.document, shared_before,
|
||||
"shared references are refused rather than silently modified together"
|
||||
);
|
||||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||||
assert_eq!(app.active_tab, source);
|
||||
assert_eq!(app.tabs[source].scene.document, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_rejected_outside_editor_preserve_running_command_and_stale_results_are_inert() {
|
||||
use crate::scene::block_actions::Kind;
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("LINE");
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let _ = app.dispatch_command("BACTION MOVE");
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE");
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
begin(&mut app, reference);
|
||||
let _ = app.run_command_line("BPARAMETER POINT 2,3 4,5");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = crate::scene::block_actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)
|
||||
[0]
|
||||
.handle;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::PlaceBlockAction {
|
||||
document: app.tabs[i].id + 1,
|
||||
definition: def,
|
||||
kind: Kind::Move,
|
||||
parameter: p,
|
||||
targets: members(&app),
|
||||
location: glam::DVec3::ZERO,
|
||||
});
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.dispatch_command("LINE");
|
||||
let _ = app.dispatch_command("BACTION SCALE");
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_test_reference_transform_visibility_and_noop_history() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let source = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER POINT 2,3 4,5");
|
||||
let _ = app.run_command_line("BPARAMETER VISIBILITY 8,8");
|
||||
let _ = app.run_command_line("BPARAMETER BASEPOINT 1,2");
|
||||
let definition = app.tabs[source]
|
||||
.active_block_edit_session()
|
||||
.unwrap()
|
||||
.br_handle;
|
||||
let p =
|
||||
actions::parameters(&app.tabs[source].scene.document, definition, Kind::Move)[0].clone();
|
||||
let targets = members(&app);
|
||||
let _ = app.dispatch_command("BACTION MOVE");
|
||||
feed(&mut app, &p.name);
|
||||
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(targets));
|
||||
let _ = app.feed_command(crate::command::StepInput::Enter);
|
||||
feed(&mut app, "4,6");
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
let i = app.active_tab;
|
||||
let insert = app.tabs[i].block_test.as_ref().unwrap().insert;
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
let id = app.tabs[i].id;
|
||||
let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let current_world = actions::v(
|
||||
crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins)
|
||||
.apply(actions::native(p.end)),
|
||||
);
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: current_world,
|
||||
});
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
// Existing visibility UI creates a per-reference anonymous definition.
|
||||
app.apply_visibility_state(insert, 0);
|
||||
app.finish_pending_history(i);
|
||||
let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let after_world = actions::v(
|
||||
crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins)
|
||||
.apply(actions::native(p.end)),
|
||||
);
|
||||
assert_eq!(
|
||||
after_world, current_world,
|
||||
"visibility representation retains the native Basepoint anchor"
|
||||
);
|
||||
let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity_mut(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
ins.insert_point = Vector3::new(100., 200., 0.);
|
||||
ins.rotation = std::f64::consts::FRAC_PI_2;
|
||||
ins.set_x_scale(2.);
|
||||
ins.set_y_scale(3.);
|
||||
let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let name = ins.block_name.clone();
|
||||
assert_ne!(name, "EDIT_ME");
|
||||
let world = actions::v(
|
||||
crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins)
|
||||
.apply(Vector3::new(12., 13., 0.)),
|
||||
);
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: world,
|
||||
});
|
||||
let anon = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities_in_block(&name)
|
||||
.find_map(|e| {
|
||||
if let EntityType::Line(l) = e {
|
||||
Some(l)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
assert!((anon.start.x - 10.).abs() < 1e-9 && (anon.start.y - 10.).abs() < 1e-9);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
}
|
||||
|
||||
fn authored_points(app: &OpenCADStudio) -> Vec<codec::objects::BlockPointParameter> {
|
||||
let tab = &app.tabs[app.active_tab];
|
||||
let definition = tab
|
||||
@@ -98,6 +657,201 @@ fn orientation_parameter_count(app: &OpenCADStudio) -> usize {
|
||||
crate::scene::block_orientation_parameter::parameters(&tab.scene.document, definition).len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_basepoint_placement_history_test_block_and_saved_parent_undo() {
|
||||
use crate::scene::{block_basepoint, block_lookup, render_graph};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let members_before = members(&app);
|
||||
let _ = app.run_command_line("BPARAMETER BASEPOINT 3,4");
|
||||
let (handle, p) = block_basepoint::parameter(&app.tabs[i].scene.document, definition).unwrap();
|
||||
assert_eq!(p.point, Vector3::new(3.0, 4.0, 0.0));
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
assert_eq!(depth, 1);
|
||||
let _ = app.run_command_line("BPARAMETER BASEPOINT 3,4");
|
||||
assert_eq!(
|
||||
app.tabs[i].history.undo_stack.len(),
|
||||
depth,
|
||||
"same anchor is a no-op"
|
||||
);
|
||||
let _ = app.run_command_line("BPARAMETER BASEPOINT 5,6");
|
||||
assert_eq!(
|
||||
block_basepoint::parameter(&app.tabs[i].scene.document, definition)
|
||||
.unwrap()
|
||||
.0,
|
||||
handle
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
block_basepoint::parameter(&app.tabs[i].scene.document, definition)
|
||||
.unwrap()
|
||||
.1
|
||||
.point,
|
||||
Vector3::new(3.0, 4.0, 0.0)
|
||||
);
|
||||
app.redo_steps(1);
|
||||
let _ = app.dispatch_command("BPARAMETER");
|
||||
feed(&mut app, "K");
|
||||
feed(&mut app, "2,2");
|
||||
assert_eq!(
|
||||
block_lookup::parameters(&app.tabs[i].scene.document, definition).len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
members(&app),
|
||||
members_before,
|
||||
"authoring objects are not drawing entities"
|
||||
);
|
||||
let before_test = app.tabs[i].scene.document.clone();
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
let insert = doc
|
||||
.entities()
|
||||
.find_map(|e| match e {
|
||||
EntityType::Insert(i) => Some(i),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
(render_graph::insert_transform(doc, insert).apply(Vector3::new(5.0, 6.0, 0.0))
|
||||
- insert.insert_point)
|
||||
.length()
|
||||
< 1e-9
|
||||
);
|
||||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||||
assert_eq!(app.tabs[i].scene.document, before_test);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_some());
|
||||
app.undo_steps(1);
|
||||
assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_none());
|
||||
app.redo_steps(1);
|
||||
assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_some());
|
||||
}
|
||||
|
||||
fn lookup_editor_fixture() -> (OpenCADStudio, Handle, Handle) {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 2,0 0,1");
|
||||
let _ = app.run_command_line("BPARAMETER LOOKUP 1,1");
|
||||
let i = app.active_tab;
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let parameter =
|
||||
crate::scene::block_lookup::parameters(&app.tabs[i].scene.document, definition)[0].0;
|
||||
(app, definition, parameter)
|
||||
}
|
||||
fn edit_lookup(app: &mut OpenCADStudio) {
|
||||
use crate::ui::window::block_lookup::Action as A;
|
||||
let _ = app.update(Message::BlockLookup(A::Property(0)));
|
||||
let _ = app.update(Message::BlockLookup(A::AddProperty));
|
||||
let _ = app.update(Message::BlockLookup(A::AddRow));
|
||||
let _ = app.update(Message::BlockLookup(A::Cell(0, 1, "Small".into())));
|
||||
}
|
||||
#[test]
|
||||
fn lookup_editor_cancel_apply_one_undo_and_stale_guard() {
|
||||
use crate::ui::window::block_lookup::Action as A;
|
||||
use crate::scene::block_lookup;
|
||||
let (mut app, definition, parameter) = lookup_editor_fixture();
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
for cancel in [Message::CloseModal, Message::CommandEscape] {
|
||||
let _ = app.dispatch_command("BLOOKUPTABLE");
|
||||
edit_lookup(&mut app);
|
||||
assert_eq!(app.tabs[i].scene.document, before, "draft is isolated");
|
||||
let _ = app.update(cancel);
|
||||
assert!(app.block_lookup.is_none());
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
}
|
||||
let _ = app.dispatch_command("BLOOKUPTABLE");
|
||||
let _ = app.update(Message::BlockLookup(A::Apply));
|
||||
assert_eq!(
|
||||
app.tabs[i].history.undo_stack.len(),
|
||||
depth,
|
||||
"unchanged OK is a no-op"
|
||||
);
|
||||
let _ = app.dispatch_command("BLOOKUPTABLE");
|
||||
edit_lookup(&mut app);
|
||||
let _ = app.update(Message::BlockLookup(A::Audit));
|
||||
assert!(app.block_lookup.as_ref().unwrap().error.is_none());
|
||||
let _ = app.update(Message::BlockLookup(A::Apply));
|
||||
assert!(app.block_lookup.is_none());
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
assert_eq!(
|
||||
block_lookup::table(&app.tabs[i].scene.document, definition, parameter)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.1
|
||||
.expressions,
|
||||
vec!["2", "Small"]
|
||||
);
|
||||
let saved = app.tabs[i].scene.document.clone();
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, saved);
|
||||
let _ = app.dispatch_command("BLOOKUPTABLE Lookup1");
|
||||
let _ = app.update(Message::BlockLookup(A::Cell(0, 1, "Changed".into())));
|
||||
app.tabs[i].scene.bump_geometry();
|
||||
let _ = app.update(Message::BlockLookup(A::Apply));
|
||||
assert!(app
|
||||
.block_lookup
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("changed"));
|
||||
assert_eq!(app.tabs[i].scene.document, saved);
|
||||
let _ = app.update(Message::CloseModal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_editor_multiple_tables_apply_atomically_and_parameter_cancel_is_clean() {
|
||||
use crate::ui::window::block_lookup::Action as A;
|
||||
let (mut app, definition, _) = lookup_editor_fixture();
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LOOKUP 3,3");
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
for kind in ["BASEPOINT", "LOOKUP"] {
|
||||
let _ = app.dispatch_command(&format!("BPARAMETER {kind}"));
|
||||
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("BLOOKUPTABLE");
|
||||
edit_lookup(&mut app);
|
||||
let _ = app.update(Message::BlockLookup(A::Select(1)));
|
||||
edit_lookup(&mut app);
|
||||
let _ = app.update(Message::BlockLookup(A::Cell(0, 0, "NaN".into())));
|
||||
let _ = app.update(Message::BlockLookup(A::Apply));
|
||||
assert!(app.block_lookup.as_ref().unwrap().error.is_some());
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.update(Message::BlockLookup(A::Cell(0, 0, "5".into())));
|
||||
let _ = app.update(Message::BlockLookup(A::Apply));
|
||||
assert!(app.block_lookup.is_none());
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
for (h, _) in crate::scene::block_lookup::parameters(&app.tabs[i].scene.document, definition) {
|
||||
assert!(
|
||||
crate::scene::block_lookup::table(&app.tabs[i].scene.document, definition, h)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
let _ = app.dispatch_command("LINE");
|
||||
for cmd in ["BPARAMETER BASEPOINT", "BPARAMETER LOOKUP", "BLOOKUPTABLE"] {
|
||||
let _ = app.dispatch_command(cmd);
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orientation_parameter_batch_numeric_rotation_local_history_save_and_parent_undo() {
|
||||
let (mut app, reference) = fixture();
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
use super::{Message, ModalKind, OpenCADStudio};
|
||||
use crate::scene::{block_basepoint, block_lookup as lookup};
|
||||
use crate::ui::window::block_lookup::{Action, Draft, State};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn handle_place_block_single_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
use crate::modules::block_editor::single_parameter::Kind;
|
||||
let crate::command::CmdResult::PlaceBlockSingleParameter {
|
||||
document_id,
|
||||
definition,
|
||||
kind,
|
||||
location,
|
||||
} = result
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].id != document_id
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|s| s.br_handle)
|
||||
!= Some(definition)
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("The parameter's Block Editor session is no longer active.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(error) = self.parameter_placement_error(kind.name()) {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
if !location.is_finite() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Specify a finite parameter location.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let location = codec::types::Vector3::new(location.x, location.y, location.z);
|
||||
if kind == Kind::Basepoint
|
||||
&& block_basepoint::parameter(&self.tabs[i].scene.document, definition).is_some_and(
|
||||
|(_, p)| {
|
||||
p.point == location
|
||||
&& p.base_point == location
|
||||
&& p.parameter.definition_point == location
|
||||
},
|
||||
)
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, &format!("{} parameter", kind.name()));
|
||||
match kind {
|
||||
Kind::Basepoint => {
|
||||
block_basepoint::create(&mut self.tabs[i].scene.document, definition, location)
|
||||
}
|
||||
Kind::Lookup => lookup::create(&mut self.tabs[i].scene.document, definition, location),
|
||||
}
|
||||
.expect("validated parameter placement");
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(
|
||||
crate::t!(if kind == Kind::Lookup {
|
||||
"Lookup parameter placed. Use BLOOKUPTABLE to edit its property table."
|
||||
} else {
|
||||
"Basepoint parameter placed."
|
||||
})
|
||||
.as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
pub(in crate::app) fn lookup_table_error(&self) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
return Some("Open a block in the Block Editor before editing a Lookup table.");
|
||||
};
|
||||
if lookup::parameters(&tab.scene.document, session.br_handle).is_empty() {
|
||||
Some("Place a Lookup parameter before editing its property table.")
|
||||
} else {
|
||||
lookup::placement_error(&tab.scene.document, session.br_handle)
|
||||
}
|
||||
}
|
||||
pub(in crate::app) fn open_lookup_table(&mut self, name: &str) -> Task<Message> {
|
||||
if let Some(error) = self.lookup_table_error() {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let definition = tab.active_block_edit_session().unwrap().br_handle;
|
||||
let mut drafts = vec![];
|
||||
for (handle, parameter) in lookup::parameters(&tab.scene.document, definition) {
|
||||
let original = match lookup::table(&tab.scene.document, definition, handle) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let edited = original
|
||||
.as_ref()
|
||||
.map(|(_, t)| t.clone())
|
||||
.unwrap_or_else(|| lookup::empty_table(parameter));
|
||||
if edited.row_count < 0
|
||||
|| edited.row_count > 1000
|
||||
|| edited.columns.is_empty()
|
||||
|| edited.columns.len() > 64
|
||||
|| edited.column_count != edited.columns.len() as i32
|
||||
|| edited.expressions.len() != edited.columns.len() * edited.row_count as usize
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("This Lookup table has inconsistent or unsupported dimensions.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
drafts.push(Draft {
|
||||
parameter: handle,
|
||||
original_parameter: parameter.clone(),
|
||||
original,
|
||||
edited,
|
||||
});
|
||||
}
|
||||
let selected = if name.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let Some(index) = drafts
|
||||
.iter()
|
||||
.position(|d| d.original_parameter.lookup_name.eq_ignore_ascii_case(name))
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No Lookup parameter has that name.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
index
|
||||
};
|
||||
self.block_lookup = Some(State {
|
||||
document: tab.id,
|
||||
definition,
|
||||
epoch: tab.scene.geometry_epoch,
|
||||
drafts,
|
||||
selected,
|
||||
properties: lookup::properties(&tab.scene.document, definition),
|
||||
selected_property: None,
|
||||
error: None,
|
||||
notice: None,
|
||||
});
|
||||
self.active_modal = Some(ModalKind::BlockLookup);
|
||||
Task::none()
|
||||
}
|
||||
pub(super) fn handle_block_lookup(&mut self, action: Action) -> Task<Message> {
|
||||
if !matches!(action, Action::Apply) {
|
||||
if let Some(state) = self.block_lookup.as_mut() {
|
||||
state.edit(action);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let Some(state) = self.block_lookup.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
let stale = tab.id != state.document
|
||||
|| tab.active_block_edit_session().map(|s| s.br_handle) != Some(state.definition)
|
||||
|| tab.scene.geometry_epoch != state.epoch
|
||||
|| state.drafts.iter().any(|d| {
|
||||
lookup::parameters(&tab.scene.document, state.definition)
|
||||
.into_iter()
|
||||
.find(|(h, _)| *h == d.parameter)
|
||||
.map(|(_, p)| p)
|
||||
!= Some(&d.original_parameter)
|
||||
|| lookup::table(&tab.scene.document, state.definition, d.parameter)
|
||||
.ok()
|
||||
.as_ref()
|
||||
!= Some(&d.original)
|
||||
});
|
||||
if stale {
|
||||
self.block_lookup.as_mut().unwrap().error = Some(
|
||||
crate::t!("The block changed. Cancel and reopen the Lookup table.").into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let mut working = tab.scene.document.clone();
|
||||
let mut changed = false;
|
||||
for draft in &state.drafts {
|
||||
if draft.changed() {
|
||||
if let Err(error) = lookup::apply_table(
|
||||
&mut working,
|
||||
state.definition,
|
||||
draft.parameter,
|
||||
draft.edited.clone(),
|
||||
) {
|
||||
self.block_lookup.as_mut().unwrap().error =
|
||||
Some(crate::t!(error.as_str()).into_owned());
|
||||
return Task::none();
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.push_undo_snapshot(i, "Lookup tables");
|
||||
self.tabs[i].scene.document = working;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.block_lookup = None;
|
||||
self.active_modal = None;
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -5,6 +5,9 @@ use iced::Task;
|
||||
|
||||
pub(super) struct BlockTestSession {
|
||||
pub source_id: u64,
|
||||
pub definition: codec::Handle,
|
||||
pub insert: codec::Handle,
|
||||
pub baseline: std::sync::Arc<codec::CadDocument>,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
@@ -67,8 +70,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
document.header.show_model_space = true;
|
||||
crate::scene::block_visibility::finish_editing(&mut document, definition);
|
||||
let record = document.block_records.get(&name).unwrap();
|
||||
let base = record.base_point;
|
||||
let base = crate::scene::render_graph::block_base_point(&document, &name);
|
||||
let mut insert = Insert::new(&name, codec::types::Vector3::ZERO);
|
||||
insert.common.layer = self.tabs[i].active_layer.clone();
|
||||
insert.common.handle = document.allocate_handle();
|
||||
@@ -98,13 +100,19 @@ impl OpenCADStudio {
|
||||
if !insert.attributes.is_empty() {
|
||||
insert.seqend_handle = Some(document.allocate_handle());
|
||||
}
|
||||
let insert_handle = insert.common.handle;
|
||||
if let Err(error) = document.add_entity(EntityType::Insert(insert)) {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return Task::none();
|
||||
}
|
||||
let mut tab = DocumentTab::new_drawing(0);
|
||||
tab.tab_title = crate::tf!("Test Block Window - {}", name).into_owned();
|
||||
tab.block_test = Some(BlockTestSession { source_id });
|
||||
tab.block_test = Some(BlockTestSession {
|
||||
source_id,
|
||||
definition,
|
||||
insert: insert_handle,
|
||||
baseline: std::sync::Arc::new(document.clone()),
|
||||
});
|
||||
tab.active_layer = self.tabs[i].active_layer.clone();
|
||||
tab.scene.document = document;
|
||||
tab.scene.named_parameters = self.tabs[i].scene.named_parameters.clone();
|
||||
|
||||
@@ -575,6 +575,16 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
result @ CmdResult::EditBlockAuthoring { .. } => {
|
||||
return self.handle_authoring_edit(result);
|
||||
}
|
||||
result @ (CmdResult::PlaceBlockAction { .. }
|
||||
| CmdResult::EvaluateBlockParameter { .. }) => {
|
||||
return self.handle_block_action_result(result);
|
||||
}
|
||||
result @ CmdResult::PlaceBlockSingleParameter { .. } => {
|
||||
return self.handle_place_block_single_parameter(result);
|
||||
}
|
||||
result @ CmdResult::PlaceBlockOrientationParameter { .. } => {
|
||||
return self.handle_place_block_orientation_parameter(result);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ impl OpenCADStudio {
|
||||
return Some("Open a block in the Block Editor before placing a parameter.");
|
||||
};
|
||||
match kind.to_ascii_uppercase().as_str() {
|
||||
"BASEPOINT"|"BASE"|"B" => crate::scene::block_basepoint::placement_error(&tab.scene.document,session.br_handle),
|
||||
"LOOKUP"|"K" => crate::scene::block_lookup::placement_error(&tab.scene.document,session.br_handle),
|
||||
"POINT" | "P" | "" => crate::scene::block_point_parameter::placement_error(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
@@ -151,7 +153,7 @@ impl OpenCADStudio {
|
||||
"VISIBILITY" | "V" => {
|
||||
visibility::placement_error(&tab.scene.document, session.br_handle)
|
||||
}
|
||||
_ => Some("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility."),
|
||||
_ => Some("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility, Lookup, Basepoint."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,11 +391,38 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
}
|
||||
match cmd {
|
||||
cmd if cmd == "BACTION"
|
||||
|| cmd.starts_with("BACTION ")
|
||||
|| cmd == "BACTIONTOOL"
|
||||
|| cmd.starts_with("BACTIONTOOL ") =>
|
||||
{
|
||||
Some(
|
||||
self.start_block_action(
|
||||
cmd.split_once(' ').map_or("", |(_, args)| args.trim()),
|
||||
),
|
||||
)
|
||||
}
|
||||
cmd if cmd == "BLOOKUPTABLE" || cmd.starts_with("BLOOKUPTABLE ") => {
|
||||
Some(self.open_lookup_table(cmd.strip_prefix("BLOOKUPTABLE").unwrap().trim()))
|
||||
}
|
||||
cmd if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") => {
|
||||
let Some(block) = self.visibility_block(i, "BPARAMETER") else {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
||||
if let Some(kind) =
|
||||
crate::modules::block_editor::single_parameter::Kind::parse(parameter_type)
|
||||
{
|
||||
let command =
|
||||
crate::modules::block_editor::single_parameter::SingleParameterCommand::new(
|
||||
self.tabs[i].id,
|
||||
block,
|
||||
kind,
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
if let Some(kind) =
|
||||
crate::scene::block_orientation_parameter::Kind::parse(parameter_type)
|
||||
{
|
||||
@@ -428,7 +457,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility."
|
||||
"Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility, Lookup, Basepoint."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
@@ -175,6 +175,22 @@ impl OpenCADStudio {
|
||||
};
|
||||
let resolved = self.resolve_alias(cmd);
|
||||
let cmd = resolved.as_deref().unwrap_or(cmd);
|
||||
if matches!(cmd.split(' ').next(), Some("BACTION" | "BACTIONTOOL")) {
|
||||
let args = cmd.split_once(' ').map_or("", |(_, a)| a.trim());
|
||||
if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() {
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Supported actions: Move, Scale. Other action types are not wired yet."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(error) = self.action_error(crate::scene::block_actions::Kind::parse(args)) {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") {
|
||||
if let Some(error) =
|
||||
self.parameter_placement_error(cmd.strip_prefix("BPARAMETER").unwrap().trim())
|
||||
@@ -183,6 +199,12 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if cmd == "BLOOKUPTABLE" || cmd.starts_with("BLOOKUPTABLE ") {
|
||||
if let Some(error) = self.lookup_table_error() {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if self.tabs[i].block_test.is_some() {
|
||||
let name = cmd.split([' ', ':']).next().unwrap_or(cmd);
|
||||
if matches!(
|
||||
|
||||
+216
-3
@@ -5,6 +5,17 @@ use crate::app::{Message, OpenCADStudio};
|
||||
fn fresh() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
// These tests drive dock controls, not the hardware warning dialog.
|
||||
// Software snapshots intentionally have no scene GPU adapter.
|
||||
app.gpu_warning_silenced = crate::scene::pipeline::gpu_status()
|
||||
.identity()
|
||||
.or_else(|| crate::scene::pipeline::GpuStatus::NoRenderer.identity())
|
||||
.unwrap();
|
||||
app.pending_startup_modals
|
||||
.retain(|modal| *modal != crate::app::ModalKind::GpuWarning);
|
||||
if app.active_modal == Some(crate::app::ModalKind::GpuWarning) {
|
||||
app.active_modal = None;
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
@@ -1095,6 +1106,203 @@ fn authoring_app() -> OpenCADStudio {
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_palette_controls_require_compatible_parameters_and_start_commands() {
|
||||
with_stack(|| {
|
||||
use crate::ui::window::authoring_palettes::Tab;
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let point = authoring_control(&app, Tab::Actions.widget_id());
|
||||
authoring_click(&mut app, point);
|
||||
for id in ["authoring-tool-move-action", "authoring-tool-scale-action"] {
|
||||
let point = authoring_control(&app, id);
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||
}
|
||||
let _ = app.run_command_line("BPARAMETER POINT 0.5,0.5 0.7,0.7");
|
||||
let point = authoring_control(&app, "authoring-tool-scale-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||
let point = authoring_control(&app, "authoring-tool-move-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
"BACTION"
|
||||
);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 1,0 0.5,0.3");
|
||||
let point = authoring_control(&app, "authoring-tool-scale-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
"BACTION"
|
||||
);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
authoring_capture(&app, "move-scale-actions-enabled");
|
||||
let i = app.active_tab;
|
||||
let targets = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("AUTHOR")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.clone();
|
||||
let point = authoring_control(&app, "authoring-tool-scale-action");
|
||||
authoring_click(&mut app, point);
|
||||
let _ = app.feed_command(crate::command::StepInput::Point(glam::DVec3::X));
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_selection_gathering(),
|
||||
"picking the parameter endpoint starts object selection"
|
||||
);
|
||||
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(targets));
|
||||
let _ = app.feed_command(crate::command::StepInput::Enter);
|
||||
let _ = app.update(Message::CommandInput("0.8,0.5".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert!(app.tabs[i].active_cmd.is_none());
|
||||
let _ = app.run_command_line("ZOOM E");
|
||||
authoring_capture(&app, "scale-action-placed");
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.);
|
||||
app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64(
|
||||
glam::DVec3::new(-0.2, -0.4, 0.),
|
||||
glam::DVec3::new(1.2, 0.6, 0.),
|
||||
1600. / 900.,
|
||||
);
|
||||
let insert = app.tabs[i].block_test.as_ref().unwrap().insert;
|
||||
app.tabs[i].scene.select_entity(insert, true);
|
||||
app.refresh_properties();
|
||||
assert!(app.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.any(|g| crate::app::block_actions::is_test_grip(g.id)));
|
||||
authoring_capture(&app, "scale-action-test-grip");
|
||||
// Drawing a software snapshot updates the canvas size. Use the same
|
||||
// current tile as the viewport input path, including when captures run.
|
||||
let (vw, vh) = app.tabs[i].scene.selection.borrow().view.vp_size;
|
||||
let tile = app.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
};
|
||||
let grip = app.tabs[i]
|
||||
.selected_grips
|
||||
.iter()
|
||||
.find(|g| crate::app::block_actions::is_test_grip(g.id))
|
||||
.unwrap();
|
||||
let screen = app.tabs[i]
|
||||
.scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(grip.world, bounds)
|
||||
.unwrap();
|
||||
app.cursor_pos = iced::Point::new(screen.x + tile.x, screen.y + tile.y);
|
||||
app.tabs[i].scene.selection.borrow_mut().input.last_move_pos = Some(app.cursor_pos);
|
||||
assert!(
|
||||
app.tabs[i].active_cmd.is_none(),
|
||||
"a running command consumes the test grip"
|
||||
);
|
||||
let hit = crate::scene::pick::grip::find_hit_grip(
|
||||
iced::Point::new(screen.x, screen.y),
|
||||
&app.tabs[i].selected_grips,
|
||||
&app.tabs[i].scene.camera.borrow(),
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
hit.is_some_and(|(_, id, _, _)| crate::app::block_actions::is_test_grip(id)),
|
||||
"test grip hit failed: {hit:?}; bounds={bounds:?}, cursor={:?}",
|
||||
app.cursor_pos
|
||||
);
|
||||
let _ = app.update(Message::ViewportLeftPressAt(app.cursor_pos));
|
||||
assert_eq!(
|
||||
app.tabs[i].active_cmd.as_ref().map(|c|c.name()),
|
||||
Some("BTESTBLOCK"),
|
||||
"viewport click routes the parameter grip to the test command: modal={:?}, active grip={:?}, selected={:?}, navigation={:?}, ucs={}",app.active_modal,app.tabs[i].active_grip,app.tabs[i].selected_handle,(app.tabs[i].orbit_mode,app.tabs[i].pan_mode,app.tabs[i].zoom_dynamic_mode),app.ucs_icon_selected
|
||||
);
|
||||
let _ = app.update(Message::CommandInput("2,0".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
let line = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities_in_block("AUTHOR")
|
||||
.find_map(|e| {
|
||||
if let codec::EntityType::Line(l) = e {
|
||||
Some(l)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(line.end, codec::types::Vector3::new(2., 0., 0.));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_basepoint_palette_and_table_dialog_real_controls() {
|
||||
with_stack(|| {
|
||||
use crate::ui::window::{authoring_palettes::Tab, block_lookup::Action as A};
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
for (id, point_text) in [
|
||||
("authoring-tool-basepoint", "0.2,0.1"),
|
||||
("authoring-tool-lookup", "0.5,0.2"),
|
||||
] {
|
||||
let point = authoring_control(&app, id);
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].active_cmd.as_ref().unwrap().name(),
|
||||
"BPARAMETER"
|
||||
);
|
||||
let _ = app.update(Message::CommandInput(point_text.into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 1,0 0,0.3");
|
||||
authoring_capture(&app, "lookup-basepoint-placed");
|
||||
let point = authoring_control(&app, Tab::Actions.widget_id());
|
||||
authoring_click(&mut app, point);
|
||||
let point = authoring_control(&app, "authoring-tool-lookup-table");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(app.active_modal, Some(crate::app::ModalKind::BlockLookup));
|
||||
let _ = app.update(Message::BlockLookup(A::Property(0)));
|
||||
let point = find_in_view(&app, "Add Property");
|
||||
authoring_click(&mut app, point);
|
||||
let point = find_in_view(&app, "Add Row");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.block_lookup.as_ref().unwrap().drafts[0]
|
||||
.edited
|
||||
.row_count,
|
||||
1
|
||||
);
|
||||
let _ = app.update(Message::BlockLookup(A::Cell(0, 1, "Small".into())));
|
||||
let point = find_in_view(&app, "Audit");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.block_lookup.as_ref().unwrap().notice.is_some());
|
||||
authoring_capture(&app, "lookup-property-table");
|
||||
let point = find_in_view(&app, "OK");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.block_lookup.is_none());
|
||||
let i = app.active_tab;
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let parameter =
|
||||
crate::scene::block_lookup::parameters(&app.tabs[i].scene.document, definition)[0].0;
|
||||
assert!(crate::scene::block_lookup::table(
|
||||
&app.tabs[i].scene.document,
|
||||
definition,
|
||||
parameter
|
||||
)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoring_palettes_ribbon_toggle_and_dock_close_leave_document_and_history_unchanged() {
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
@@ -1166,11 +1374,12 @@ fn authoring_palettes_toggle_keeps_the_running_drawing_command() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoring_palettes_catalog_in_real_view_has_no_parameter_placement_action() {
|
||||
fn authoring_palettes_unwired_move_action_does_not_start_a_command() {
|
||||
with_stack(|| {
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let point = find_in_view(&app, "Basepoint");
|
||||
app.authoring_palette_tab = crate::ui::window::authoring_palettes::Tab::Actions;
|
||||
let point = find_in_view(&app, "Move");
|
||||
let before = app.tabs[app.active_tab].scene.document.clone();
|
||||
drive_view(
|
||||
&mut app,
|
||||
@@ -1262,7 +1471,11 @@ fn authoring_palettes_real_tabs_switch_catalogs_and_unavailable_rows_stay_inert(
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|command| command.name()),
|
||||
Some("LINE")
|
||||
Some(if tab == Tab::Parameters {
|
||||
"BPARAMETER"
|
||||
} else {
|
||||
"LINE"
|
||||
})
|
||||
);
|
||||
}
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
|
||||
@@ -410,6 +410,9 @@ impl OpenCADStudio {
|
||||
self.visibility_placement_error(),
|
||||
self.point_parameter_placement_error(),
|
||||
self.parameter_placement_error("FLIP"),
|
||||
self.lookup_table_error(),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Move)),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Scale)),
|
||||
)
|
||||
}
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
@@ -182,6 +182,7 @@ pub(super) struct DocumentTab {
|
||||
/// the active grip edit instead of committing it.
|
||||
pub(super) grip_base_pending: bool,
|
||||
pub(super) selected_handle: Option<Handle>,
|
||||
pub(super) selected_authoring: Option<Handle>,
|
||||
/// Dynamic-block visibility grip for the current single selection.
|
||||
pub(super) visibility_grip: Option<super::visibility::VisibilityGrip>,
|
||||
pub(super) wireframe: bool,
|
||||
@@ -657,6 +658,7 @@ impl DocumentTab {
|
||||
grip_copy: false,
|
||||
grip_base_pending: false,
|
||||
selected_handle: None,
|
||||
selected_authoring: None,
|
||||
visibility_grip: None,
|
||||
wireframe: false,
|
||||
render_mode: codec::entities::ViewportRenderMode::Wireframe2D,
|
||||
|
||||
@@ -13,6 +13,9 @@ mod block_editor;
|
||||
mod block_editor_tests;
|
||||
mod block_save_as;
|
||||
mod block_visibility;
|
||||
mod block_lookup;
|
||||
mod block_actions;
|
||||
mod block_authoring_edit;
|
||||
mod block_properties;
|
||||
#[cfg(test)]
|
||||
mod block_properties_tests;
|
||||
@@ -593,6 +596,7 @@ pub(super) struct OpenCADStudio {
|
||||
block_edit_picker: Option<crate::ui::window::block_edit_picker::State>,
|
||||
block_save_as: Option<crate::ui::window::block_save_as::State>,
|
||||
block_visibility: Option<crate::ui::window::block_visibility::State>,
|
||||
block_lookup: Option<crate::ui::window::block_lookup::State>,
|
||||
block_visibility_mode: bool,
|
||||
/// PDF dialogs' working copies; `None` while closed.
|
||||
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
|
||||
@@ -2295,6 +2299,7 @@ pub enum ModalKind {
|
||||
BlockEditPicker,
|
||||
BlockSaveAs,
|
||||
BlockVisibility,
|
||||
BlockLookup,
|
||||
SaveDialog,
|
||||
Recovery,
|
||||
/// Fonts referenced by the drawing are missing on this machine.
|
||||
@@ -2949,6 +2954,7 @@ pub enum Message {
|
||||
BlockSaveAsName(String),
|
||||
BlockSaveAsApply,
|
||||
BlockVisibility(crate::ui::window::block_visibility::Action),
|
||||
BlockLookup(crate::ui::window::block_lookup::Action),
|
||||
BlockEditPickerName(String),
|
||||
BlockEditPickerSelect(String),
|
||||
BlockEditPickerApply,
|
||||
@@ -4732,6 +4738,7 @@ impl OpenCADStudio {
|
||||
block_edit_picker: None,
|
||||
block_save_as: None,
|
||||
block_visibility: None,
|
||||
block_lookup: None,
|
||||
block_visibility_mode: false,
|
||||
pdf_attach: None,
|
||||
point_cloud_attach: None,
|
||||
|
||||
@@ -2475,6 +2475,45 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}",
|
||||
}
|
||||
}
|
||||
};
|
||||
if selected.is_empty() {
|
||||
if let Some(item) = self.tabs[i].selected_authoring.and_then(|handle| {
|
||||
let session = self.tabs[i].active_block_edit_session()?;
|
||||
crate::scene::block_authoring::edit::items(
|
||||
&self.tabs[i].scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.find(|item| item.handle == handle)
|
||||
}) {
|
||||
use crate::scene::model::object::{PropSection, PropValue, Property};
|
||||
panel.title = item.name;
|
||||
panel.sections = vec![PropSection {
|
||||
title: crate::t!("Geometry").into_owned(),
|
||||
props: item
|
||||
.grips
|
||||
.into_iter()
|
||||
.map(|(part, point)| Property {
|
||||
label: crate::t!(match part {
|
||||
crate::scene::block_authoring::edit::Part::Point => "Position",
|
||||
crate::scene::block_authoring::edit::Part::Base => "Base point",
|
||||
crate::scene::block_authoring::edit::Part::End => "End point",
|
||||
crate::scene::block_authoring::edit::Part::Reference =>
|
||||
"Reference point",
|
||||
crate::scene::block_authoring::edit::Part::Label(0) =>
|
||||
"Label position",
|
||||
_ => "Second label position",
|
||||
})
|
||||
.into_owned(),
|
||||
field: "authoring_grip_position",
|
||||
value: PropValue::ReadOnly(format!(
|
||||
"{:.4}, {:.4}, {:.4}",
|
||||
point.x, point.y, point.z
|
||||
)),
|
||||
})
|
||||
.collect(),
|
||||
}];
|
||||
}
|
||||
}
|
||||
// Precompute the focused-id → field-key map for O(1) lookups on
|
||||
// `PropSyncActive`; derived from `sections`, so rebuild it here.
|
||||
panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections);
|
||||
@@ -2886,6 +2925,8 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
// Append the dynamic-block visibility (lookup) grip, if the lone
|
||||
// selection is a visibility-parametric block reference.
|
||||
self.refresh_visibility_grip(wo);
|
||||
self.refresh_test_action_grips(wo);
|
||||
self.refresh_authoring_grips(wo);
|
||||
}
|
||||
|
||||
pub(super) fn property_target_handles(&self, i: usize) -> Vec<Handle> {
|
||||
|
||||
@@ -1410,6 +1410,7 @@ impl OpenCADStudio {
|
||||
// ESC while in MSPACE → exit back to paper space.
|
||||
return Task::done(Message::ExitViewport);
|
||||
} else {
|
||||
self.tabs[i].selected_authoring = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refresh_properties();
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
|
||||
@@ -237,6 +237,7 @@ impl OpenCADStudio {
|
||||
Some(BlockEditPicker) => self.block_edit_picker = None,
|
||||
Some(BlockSaveAs) => self.block_save_as = None,
|
||||
Some(BlockVisibility) => self.block_visibility = None,
|
||||
Some(BlockLookup) => self.block_lookup = None,
|
||||
Some(PdfAttach) => self.pdf_attach = None,
|
||||
Some(UnderlayLayers) => self.underlay_layers = None,
|
||||
Some(PdfImportSettings) => self.pdf_import_settings = None,
|
||||
@@ -9729,6 +9730,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::CloseModal => {
|
||||
if self.active_modal == Some(super::ModalKind::BlockLookup) {
|
||||
self.block_lookup = None;
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockVisibility) {
|
||||
self.block_visibility = None;
|
||||
}
|
||||
@@ -10753,6 +10757,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
|
||||
Message::BlockVisibility(action) => self.handle_block_visibility(action),
|
||||
Message::BlockLookup(action) => self.handle_block_lookup(action),
|
||||
Message::BlockEditPickerName(name) => {
|
||||
if let Some(state) = self.block_edit_picker.as_mut() {
|
||||
state.name = name.clone();
|
||||
|
||||
@@ -829,7 +829,11 @@ impl OpenCADStudio {
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.filter(|(owner, grip)| self.tabs[i].hot_grips.contains(&(*owner, grip.id)))
|
||||
.filter(|(_, grip)| grip.id != crate::app::visibility::VIS_GRIP_ID)
|
||||
.filter(|(_, grip)| {
|
||||
grip.id != crate::app::visibility::VIS_GRIP_ID
|
||||
&& !crate::app::block_actions::is_test_grip(grip.id)
|
||||
&& !crate::app::block_authoring_edit::is_editor_grip(grip.id)
|
||||
})
|
||||
.map(|(owner, grip)| GripTarget {
|
||||
handle: owner,
|
||||
grip_id: grip.id,
|
||||
@@ -851,7 +855,11 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.filter(|(_, grip)| grip.id != crate::app::visibility::VIS_GRIP_ID)
|
||||
.filter(|(_, grip)| {
|
||||
grip.id != crate::app::visibility::VIS_GRIP_ID
|
||||
&& !crate::app::block_actions::is_test_grip(grip.id)
|
||||
&& !crate::app::block_authoring_edit::is_editor_grip(grip.id)
|
||||
})
|
||||
.filter(|(_, grip)| (grip.world - world).length_squared() <= epsilon_sq)
|
||||
.map(|(owner, grip)| GripTarget {
|
||||
handle: owner,
|
||||
@@ -993,6 +1001,8 @@ impl OpenCADStudio {
|
||||
// hover grip-menu.
|
||||
if self.grip_popup.is_none()
|
||||
&& grip_id != crate::app::visibility::VIS_GRIP_ID
|
||||
&& !crate::app::block_actions::is_test_grip(grip_id)
|
||||
&& !crate::app::block_authoring_edit::is_editor_grip(grip_id)
|
||||
&& self
|
||||
.grip_hover
|
||||
.as_ref()
|
||||
@@ -3993,6 +4003,12 @@ impl OpenCADStudio {
|
||||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
}
|
||||
if self.start_test_action_grip(handle, grip_id) {
|
||||
return Task::none();
|
||||
}
|
||||
if self.start_authoring_grip(handle, grip_id) {
|
||||
return Task::none();
|
||||
}
|
||||
if self.shift_down {
|
||||
let key = (handle, grip_id);
|
||||
if !self.tabs[i].hot_grips.remove(&key) {
|
||||
@@ -4111,6 +4127,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
if edit_cam.is_none() && self.pick_authoring_object(p, bounds) {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Ctrl+click on a composite solid picks the solid of it under the
|
||||
// cursor — the one the Properties palette and grips then edit.
|
||||
if self.ctrl_down
|
||||
|
||||
@@ -1119,6 +1119,45 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
)
|
||||
}),
|
||||
)
|
||||
.chain(
|
||||
crate::scene::block_basepoint::parameter(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.map(|(_, p)| {
|
||||
let mut g = crate::scene::block_distance_parameter::Graphic::default();
|
||||
g.base_grips.push(p.parameter.definition_point);
|
||||
(g, p.parameter.definition_point)
|
||||
}),
|
||||
)
|
||||
.chain(
|
||||
crate::scene::block_lookup::parameters(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(_, p)| {
|
||||
let mut g = crate::scene::block_distance_parameter::Graphic::default();
|
||||
g.lookup_grips.push(p.parameter.definition_point);
|
||||
g.labels
|
||||
.push((p.parameter.definition_point, p.lookup_name.clone()));
|
||||
(g, p.parameter.definition_point)
|
||||
}),
|
||||
)
|
||||
.chain(
|
||||
crate::scene::block_actions::actions(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(_, action, _, _)| {
|
||||
let mut g = crate::scene::block_distance_parameter::Graphic::default();
|
||||
g.grips.push((action.display_location, false));
|
||||
g.labels
|
||||
.push((action.display_location, action.element.name.clone()));
|
||||
(g, action.display_location)
|
||||
}),
|
||||
)
|
||||
.filter_map(|(graphic, marker_tail)| {
|
||||
let project = |p: codec::types::Vector3| {
|
||||
let p = tab
|
||||
@@ -1155,6 +1194,16 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(crate::ui::overlay::AuthoringDimension {
|
||||
lines,
|
||||
base_grips: graphic
|
||||
.base_grips
|
||||
.into_iter()
|
||||
.map(project)
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
lookup_grips: graphic
|
||||
.lookup_grips
|
||||
.into_iter()
|
||||
.map(project)
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
round_grips: graphic
|
||||
.round_grips
|
||||
.into_iter()
|
||||
|
||||
@@ -89,6 +89,7 @@ impl OpenCADStudio {
|
||||
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
|
||||
Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(),
|
||||
Some(K::BlockVisibility) => crate::t!("Visibility States").into_owned(),
|
||||
Some(K::BlockLookup) => crate::t!("Property Lookup Table").into_owned(),
|
||||
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
|
||||
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
|
||||
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
|
||||
@@ -523,6 +524,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::block_edit_picker::view(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockLookup => {
|
||||
let state = self.block_lookup.as_ref()?;
|
||||
sized_flow(ex, 940, 600, |flow| {
|
||||
crate::ui::window::block_lookup::view(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockVisibility => {
|
||||
let state = self.block_visibility.as_ref()?;
|
||||
sized_flow(ex, 580, 470, |flow| {
|
||||
|
||||
Reference in new issue
Block a user