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:
ramox81andClaude Opus 5.5 committed 2026-10-16 22:12:01 +03:00
commit 0158ea533f
48 files changed
+5133 -52

No files matched your search

+265
View File
@@ -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
}
}
+230
View File
@@ -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()
}
}
+754
View File
@@ -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();
+219
View File
@@ -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
View File
@@ -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();
+10
View File
@@ -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);
}
+31 -2
View File
@@ -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(),
);
+22
View File
@@ -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
View File
@@ -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);
+3
View File
@@ -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 => {
+2
View File
@@ -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,
+7
View File
@@ -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,
+41
View File
@@ -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> {
+1
View File
@@ -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();
+5
View File
@@ -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();
+22 -2
View File
@@ -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
+49
View File
@@ -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()
+7
View File
@@ -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| {