Merge upstream main; codec patch on the palette branch merged with it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 22:03:16 +03:00
commit 0b179a862e
48 files changed
+5131 -50

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
@@ -174,6 +174,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())
@@ -182,6 +198,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.
@@ -2947,6 +2952,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,
@@ -4730,6 +4736,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,
@@ -9728,6 +9729,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;
}
@@ -10752,6 +10756,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
@@ -88,6 +88,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"),
@@ -522,6 +523,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| {
+28
View File
@@ -1514,6 +1514,34 @@ impl DimensionPreview {
/// Returned by every `CadCommand` method to tell main.rs what to do.
#[allow(dead_code)]
pub enum CmdResult {
EditBlockAuthoring {
document: u64,
definition: Handle,
handle: Handle,
part: crate::scene::block_authoring::edit::Part,
position: DVec3,
},
PlaceBlockAction {
document: u64,
definition: Handle,
kind: crate::scene::block_actions::Kind,
parameter: Handle,
targets: Vec<Handle>,
location: DVec3,
},
EvaluateBlockParameter {
document: u64,
definition: Handle,
parameter: Handle,
position: DVec3,
},
/// Place or relocate a Basepoint, or place a Lookup parameter.
PlaceBlockSingleParameter {
document_id: u64,
definition: Handle,
kind: crate::modules::block_editor::single_parameter::Kind,
location: DVec3,
},
/// Place a native Rotation, Alignment or Flip parameter.
PlaceBlockOrientationParameter {
document_id: u64,
+203
View File
@@ -0,0 +1,203 @@
//! Reuse the command host's point picking and object-set selection.
use crate::command::{CadCommand, CmdOption, CmdResult, PointPickContext};
use crate::scene::block_actions::{Kind, Parameter};
use codec::Handle;
use glam::DVec3;
inventory::submit!(crate::command::CommandRegistration {
names: &["BACTION", "BACTIONTOOL"]
});
pub(crate) struct ActionCommand {
document: u64,
definition: Handle,
kind: Option<Kind>,
candidates: Vec<Parameter>,
parameter: Option<Handle>,
targets: Vec<Handle>,
selecting: bool,
context: Option<PointPickContext>,
}
impl ActionCommand {
pub(crate) fn new(
document: u64,
definition: Handle,
kind: Option<Kind>,
candidates: Vec<Parameter>,
) -> Self {
Self {
document,
definition,
kind,
candidates,
parameter: None,
targets: vec![],
selecting: false,
context: None,
}
}
fn choose(&mut self, p: Parameter) -> CmdResult {
if self.kind == Some(Kind::Scale) && (p.point || p.xy) {
return CmdResult::ReportError(
crate::t!("Scale requires a Linear or Polar parameter.").into_owned(),
);
}
self.parameter = Some(p.handle);
self.selecting = true;
CmdResult::DeselectAndContinue
}
}
impl CadCommand for ActionCommand {
fn name(&self) -> &'static str {
"BACTION"
}
fn wants_text_input(&self) -> bool {
self.parameter.is_none()
}
fn prompt(&self) -> String {
if self.kind.is_none() {
return crate::t!("BACTION Enter action type [Move/Scale]:").into_owned();
}
if self.parameter.is_none() {
return crate::tf!(
"BACTION {} Select a parameter endpoint or enter its name:",
crate::t!(self.kind.unwrap().name())
)
.into_owned();
}
crate::t!(if self.selecting {
"BACTION Select objects for the action, then press Enter:"
} else {
"BACTION Specify action marker location:"
})
.into_owned()
}
fn options(&self) -> Vec<CmdOption> {
if self.kind.is_none() {
vec![
CmdOption::new("Move", "Move"),
CmdOption::new("Scale", "Scale"),
]
} else {
vec![]
}
}
fn on_text_input(&mut self, s: &str) -> Option<CmdResult> {
if self.kind.is_none() {
return Some(if let Some(k) = Kind::parse(s) {
self.kind = Some(k);
CmdResult::NeedPoint
} else {
CmdResult::ReportError(crate::t!("Choose Move or Scale.").into_owned())
});
}
if self.parameter.is_none() {
let matches = self
.candidates
.iter()
.filter(|p| p.name.eq_ignore_ascii_case(s))
.cloned()
.collect::<Vec<_>>();
return Some(if matches.len() == 1 {
self.choose(matches[0].clone())
} else {
CmdResult::ReportError(
crate::t!("Enter a unique compatible parameter name or pick its endpoint.")
.into_owned(),
)
});
}
None
}
fn wants_point_pick_context(&self) -> bool {
self.kind.is_some() && self.parameter.is_none()
}
fn set_point_pick_context(&mut self, c: Option<PointPickContext>) {
self.context = c;
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
if self.parameter.is_none() {
let Some(c) = self.context else {
return CmdResult::ReportError(crate::t!("Enter the parameter name.").into_owned());
};
let screen =
|p| crate::scene::pick::hit_test::world_to_screen(p, c.view, c.eye, c.bounds);
let click = screen(point);
let candidate = self
.candidates
.iter()
.filter(|p| self.kind != Some(Kind::Scale) || (!p.point && !p.xy))
.map(|p| (screen(p.end).distance(click), p))
.filter(|(d, _)| d.is_finite() && *d <= c.aperture_px.max(10.))
.min_by(|a, b| a.0.total_cmp(&b.0))
.map(|(_, p)| p.clone());
return candidate.map(|p| self.choose(p)).unwrap_or_else(|| {
CmdResult::ReportError(
crate::t!("Pick a compatible parameter endpoint.").into_owned(),
)
});
}
if self.selecting {
return CmdResult::NeedPoint;
}
if !point.is_finite() {
return CmdResult::ReportError(
crate::t!("Specify a finite action location.").into_owned(),
);
}
CmdResult::PlaceBlockAction {
document: self.document,
definition: self.definition,
kind: self.kind.unwrap(),
parameter: self.parameter.unwrap(),
targets: self.targets.clone(),
location: point,
}
}
fn is_selection_gathering(&self) -> bool {
self.selecting
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
// The host calls this after every pick/window/removal, not just Enter.
// Retain the current set while the user gathers additional objects.
self.targets = handles;
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.selecting {
if self.targets.is_empty() {
return CmdResult::ReportError(
crate::t!("Select at least one object for the action.").into_owned(),
);
}
self.selecting = false;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
}
pub(crate) struct TestParameterCommand {
pub document: u64,
pub definition: Handle,
pub parameter: Handle,
}
impl CadCommand for TestParameterCommand {
fn name(&self) -> &'static str {
"BTESTBLOCK"
}
fn prompt(&self) -> String {
crate::t!("Test Block Specify the new parameter endpoint (Escape cancels):").into_owned()
}
fn on_point(&mut self, position: DVec3) -> CmdResult {
CmdResult::EvaluateBlockParameter {
document: self.document,
definition: self.definition,
parameter: self.parameter,
position,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+2
View File
@@ -6,7 +6,9 @@ use crate::modules::parametric::ParametricModule;
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
pub mod visibility_parameter;
pub mod action;
pub mod point_parameter;
pub mod single_parameter;
pub mod distance_parameter;
pub mod orientation_parameter;
@@ -0,0 +1,71 @@
use crate::command::{CadCommand, CmdResult};
use codec::Handle;
use glam::DVec3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Basepoint,
Lookup,
}
impl Kind {
pub(crate) fn parse(value: &str) -> Option<Self> {
match value.to_ascii_uppercase().as_str() {
"BASE" | "BASEPOINT" | "B" => Some(Self::Basepoint),
"LOOKUP" | "K" => Some(Self::Lookup),
_ => None,
}
}
pub(crate) fn name(self) -> &'static str {
if self == Self::Basepoint {
"Basepoint"
} else {
"Lookup"
}
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["BLOOKUPTABLE"]
});
pub struct SingleParameterCommand {
document: u64,
definition: Handle,
kind: Kind,
}
impl SingleParameterCommand {
pub(crate) fn new(document: u64, definition: Handle, kind: Kind) -> Self {
Self {
document,
definition,
kind,
}
}
}
impl CadCommand for SingleParameterCommand {
fn name(&self) -> &'static str {
"BPARAMETER"
}
fn prompt(&self) -> String {
crate::tf!(
"BPARAMETER {} Specify parameter location:",
crate::t!(self.kind.name())
)
.into_owned()
}
fn on_point(&mut self, location: DVec3) -> CmdResult {
if !location.is_finite() {
return CmdResult::ReportError(
crate::t!("Specify a finite parameter location.").into_owned(),
);
}
CmdResult::PlaceBlockSingleParameter {
document_id: self.document,
definition: self.definition,
kind: self.kind,
location,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
@@ -28,7 +28,7 @@ impl CadCommand for VisibilityParameterCommand {
fn prompt(&self) -> String {
if self.choosing_type {
crate::t!(
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Rotation/Alignment/Flip/Visibility] <Visibility>:"
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Rotation/Alignment/Flip/Visibility/Lookup/Basepoint] <Visibility>:"
)
.into_owned()
} else {
@@ -47,6 +47,8 @@ impl CadCommand for VisibilityParameterCommand {
CmdOption::new(crate::t!("Alignment").as_ref(), "A"),
CmdOption::new(crate::t!("Flip").as_ref(), "F"),
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
CmdOption::new(crate::t!("Lookup").as_ref(), "K"),
CmdOption::new(crate::t!("Basepoint").as_ref(), "B"),
]
} else {
vec![]
@@ -74,13 +76,15 @@ impl CadCommand for VisibilityParameterCommand {
"R" | "ROTATION" => CmdResult::Dispatch("BPARAMETER ROTATION".into()),
"A" | "ALIGNMENT" => CmdResult::Dispatch("BPARAMETER ALIGNMENT".into()),
"F" | "FLIP" => CmdResult::Dispatch("BPARAMETER FLIP".into()),
"K"|"LOOKUP" => CmdResult::Dispatch("BPARAMETER LOOKUP".into()),
"B"|"BASE"|"BASEPOINT" => CmdResult::Dispatch("BPARAMETER BASEPOINT".into()),
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
"" | "V" | "VISIBILITY" => {
self.choosing_type = false;
CmdResult::NeedPoint
}
_ => CmdResult::ReportError(
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility.")
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility, Lookup, Basepoint.")
.into_owned(),
),
},
+568
View File
@@ -0,0 +1,568 @@
//! Native Move/Scale authoring and deterministic evaluation in Test Block.
use super::block_authoring::{self as authoring, *};
use codec::objects::{
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, DynamicBlockData, ObjectType,
};
use codec::types::{Transform, Vector3};
use codec::{CadDocument, EntityType, Handle};
use glam::DVec3;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Move,
Scale,
}
impl Kind {
pub(crate) fn parse(s: &str) -> Option<Self> {
match s.to_ascii_uppercase().as_str() {
"MOVE" | "M" => Some(Self::Move),
"SCALE" | "S" => Some(Self::Scale),
_ => None,
}
}
pub(crate) fn name(self) -> &'static str {
match self {
Self::Move => "Move",
Self::Scale => "Scale",
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Parameter {
pub handle: Handle,
pub id: i32,
pub grip: Handle,
pub name: String,
pub base: DVec3,
pub end: DVec3,
pub linear: bool,
pub point: bool,
pub xy: bool,
}
pub(crate) fn v(p: Vector3) -> DVec3 {
DVec3::new(p.x, p.y, p.z)
}
pub(crate) fn native(p: DVec3) -> Vector3 {
Vector3::new(p.x, p.y, p.z)
}
pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> Vec<Parameter> {
let Ok(Some(graph)) = authoring::graph(doc, definition) else {
return vec![];
};
let mut result = vec![];
for (handle, p) in super::block_point_parameter::parameters(doc, definition) {
if kind == Kind::Move {
result.push(Parameter {
handle,
id: p.parameter.parameter.element.eval.node_id,
grip: Handle::NULL,
name: p.position_name.clone(),
base: v(p.parameter.definition_point),
end: v(p.parameter.definition_point),
linear: false,
point: true,
xy: false,
});
}
}
for p in super::block_distance_parameter::parameters(doc, definition) {
let xy = p.kind == super::block_distance_parameter::Kind::XY;
if kind == Kind::Scale && xy {
continue;
}
result.push(Parameter {
handle: p.handle,
id: p.native.parameter.element.eval.node_id,
grip: Handle::NULL,
name: p.labels.first().cloned().unwrap_or_default(),
base: v(p.native.definition_base_point),
end: v(p.native.definition_end_point),
linear: p.kind == super::block_distance_parameter::Kind::Linear,
point: false,
xy,
});
}
// Resolve the actual linked grip, rather than assuming expression IDs are contiguous.
result.retain_mut(|p| {
let grip_id = if p.point {
super::block_point_parameter::parameters(doc, definition)
.into_iter()
.find(|(h, _)| *h == p.handle)
.unwrap()
.1
.parameter
.property_count
} else {
super::block_distance_parameter::parameters(doc, definition)
.into_iter()
.find(|q| q.handle == p.handle)
.unwrap()
.native
.properties[2]
.connections
.first()
.map(|c| c.code)
.unwrap_or(-1)
};
let grips = doc
.objects
.iter()
.filter_map(|(h, o)| {
let ObjectType::DynamicBlock(o) = o else {
return None;
};
if o.owner != graph {
return None;
}
let grip = match &o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => g,
DynamicBlockData::LinearGrip(g) => &g.grip,
_ => return None,
};
(grip.element.eval.node_id == grip_id).then_some(*h)
})
.collect::<Vec<_>>();
if grips.len() != 1
|| !p.base.is_finite()
|| !p.end.is_finite()
|| (!p.point && (p.end.distance(p.base) < 1e-9 || !p.end.distance(p.base).is_finite()))
{
return false;
}
p.grip = grips[0];
true
});
result.sort_by_key(|p| p.id);
result
}
pub(crate) fn supported(e: &EntityType) -> bool {
matches!(
e,
EntityType::Line(_)
| EntityType::Circle(_)
| EntityType::Arc(_)
| EntityType::LwPolyline(_)
| EntityType::Ellipse(_)
| EntityType::Point(_)
| EntityType::Text(_)
| EntityType::MText(_)
)
}
pub(crate) fn create(
doc: &mut CadDocument,
definition: Handle,
kind: Kind,
parameter: Handle,
targets: &[Handle],
location: DVec3,
) -> Result<Handle, &'static str> {
if !location.is_finite() {
return Err("Specify a finite action location.");
}
let p = parameters(doc, definition, kind)
.into_iter()
.find(|p| p.handle == parameter)
.ok_or("Select a compatible parameter belonging to this block.")?;
if targets.is_empty() {
return Err("Select at least one object for the action.");
}
let members = &doc
.block_records
.iter()
.find(|b| b.handle == definition)
.ok_or("The block definition is unavailable.")?
.entity_handles;
if targets.iter().any(|h| {
!members.contains(h)
|| doc
.get_entity(*h)
.is_none_or(|e| !supported(e) || e.common().owner_handle != definition)
}) {
return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block.");
}
let mut staged = doc.clone();
let (graph, slots) = authoring::prepare(&mut staged, definition)?;
let (dxf, cpp) = match kind {
Kind::Move => ("BLOCKMOVEACTION", "AcDbBlockMoveAction"),
Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"),
};
register_classes(&mut staged, &[(dxf, cpp, 55)]);
let handle = staged.allocate_handle();
let mut dependencies = targets.to_vec();
dependencies.sort();
dependencies.dedup();
let action = BlockAction {
element: BlockElement {
eval: expression(slots.eval, NO_VALUE, BlockEvalValue::None),
name: format!("{}{}", kind.name(), slots.eval),
major: MAJOR,
minor: MINOR,
eed_1071: 0,
},
display_location: native(location),
dependencies,
action_ids: vec![],
};
let connection = |name: &str| BlockConnection {
code: p.id,
name: name.into(),
};
let data = match kind {
Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction {
action,
connections: if p.point {
[connection("XDelta"), connection("YDelta")]
} else {
[connection("EndXDelta"), connection("EndYDelta")]
},
offsets: BlockActionOffsets {
distance_multiplier: 1.,
angle_offset: 0.,
flags: 0,
},
}),
Kind::Scale => DynamicBlockData::ScaleAction(BlockBasePointAction {
action: BlockActionWithBasePoint {
action,
offset: Vector3::ZERO,
connections: vec![connection("UpdatedBaseX"), connection("UpdatedBaseY")],
dependent: true,
base_point: Vector3::ZERO,
},
connections: vec![
connection("Scale"),
connection("XScale"),
connection("YScale"),
],
dwg_tail_byte: 0,
}),
};
staged.objects.insert(
handle,
ObjectType::DynamicBlock(dynamic(handle, graph, dxf, cpp, data)),
);
authoring::append_single(&mut staged, graph, slots, handle);
authoring::graph::bind_ids(&mut staged, graph, slots.eval, &[p.id])?;
*doc = staged;
Ok(handle)
}
pub(crate) fn actions(
doc: &CadDocument,
definition: Handle,
) -> Vec<(Handle, &BlockAction, Kind, i32)> {
let Ok(Some(graph)) = authoring::graph(doc, definition) else {
return vec![];
};
let ObjectType::DynamicBlock(o) = &doc.objects[&graph] else {
return vec![];
};
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
return vec![];
};
g.nodes
.iter()
.filter_map(|n| {
let ObjectType::DynamicBlock(o) = doc.objects.get(&n.expression)? else {
return None;
};
if o.owner != graph {
return None;
}
match &o.data {
DynamicBlockData::MoveAction(a) => {
Some((o.handle, &a.action, Kind::Move, a.connections[0].code))
}
DynamicBlockData::ScaleAction(a) => a
.connections
.first()
.map(|c| (o.handle, &a.action.action, Kind::Scale, c.code)),
_ => None,
}
})
.collect()
}
pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform) -> usize {
let handles = actions(doc, definition)
.iter()
.map(|(h, _, _, _)| *h)
.collect::<Vec<_>>();
for h in &handles {
let ObjectType::DynamicBlock(o) = doc.objects.get_mut(h).unwrap() else {
unreachable!()
};
match &mut o.data {
DynamicBlockData::MoveAction(a) => {
a.action.display_location = t.apply(a.action.display_location)
}
DynamicBlockData::ScaleAction(a) => {
a.action.action.display_location = t.apply(a.action.action.display_location);
a.action.base_point = t.apply(a.action.base_point);
a.action.offset = t.apply_rotation(a.action.offset);
}
_ => {}
}
}
handles.len()
}
pub(crate) fn current(doc: &CadDocument, p: &Parameter) -> DVec3 {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) else {
return p.end;
};
match &o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => v(g.location),
DynamicBlockData::LinearGrip(g) => v(g.grip.location),
_ => p.end,
}
}
/// Visibility can give the test INSERT an anonymous representation. Keep its
/// parallel members in sync, retaining their own handles and visibility flags.
pub(crate) fn sync_representation(
doc: &mut CadDocument,
definition: Handle,
name: &str,
) -> Result<(), &'static str> {
let original = doc
.block_records
.iter()
.find(|b| b.handle == definition)
.ok_or("The test definition is unavailable.")?;
if original.name == name {
return Ok(());
}
let source = original.entity_handles.clone();
let target = doc
.block_records
.get(name)
.ok_or("The test representation is unavailable.")?
.entity_handles
.clone();
if source.len() != target.len() {
return Err("The test representation no longer matches its definition.");
}
let affected = actions(doc, definition)
.iter()
.flat_map(|(_, a, _, _)| a.dependencies.iter().copied())
.collect::<std::collections::BTreeSet<_>>();
for (from, to) in source.into_iter().zip(target) {
if !affected.contains(&from) {
continue;
}
let mut entity = doc
.get_entity(from)
.cloned()
.ok_or("An action target is unavailable.")?;
let current = doc
.get_entity_mut(to)
.ok_or("An action representation target is unavailable.")?;
*entity.common_mut() = current.common().clone();
*current = entity;
}
Ok(())
}
/// Re-evaluate from the scratch document's immutable starting geometry. This
/// avoids incremental drift and makes repeated changes/order independent.
pub(crate) fn evaluate(
doc: &mut CadDocument,
baseline: &CadDocument,
definition: Handle,
parameter: Handle,
position: DVec3,
) -> Result<bool, &'static str> {
let graph = authoring::graph(baseline, definition)?
.ok_or("The test authoring graph is unavailable.")?;
if baseline.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data,
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::RotateAction(_)|DynamicBlockData::FlipAction(_)|DynamicBlockData::ArrayAction(_)))) {
return Err("This block contains action types not supported in Test Block yet.");
}
let p = parameters(baseline, definition, Kind::Move)
.into_iter()
.find(|p| p.handle == parameter)
.ok_or("The test parameter is unavailable.")?;
if !position.is_finite() {
return Err("Specify a finite parameter position.");
}
let mut position = position;
position.z = p.end.z;
if p.linear {
let axis = (p.end - p.base).normalize();
let length = (position - p.base).dot(axis);
if length < 1e-9 {
return Err("Specify a positive parameter length.");
}
position = p.base + axis * length;
}
if current(doc, &p).distance(position) < 1e-10 {
return Ok(false);
}
let mut staged = doc.clone();
let ObjectType::DynamicBlock(o) = staged
.objects
.get_mut(&p.grip)
.ok_or("The test grip is unavailable.")?
else {
return Err("The test grip is unavailable.");
};
match &mut o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => {
g.location = native(position)
}
DynamicBlockData::LinearGrip(g) => g.grip.location = native(position),
_ => return Err("The test grip is unavailable."),
}
let all = actions(baseline, definition);
let mut affected = std::collections::BTreeSet::new();
for (_, a, _, _) in &all {
affected.extend(a.dependencies.iter().copied());
}
for h in affected {
let mut entity = baseline
.get_entity(h)
.cloned()
.ok_or("An action target is unavailable.")?;
if !supported(&entity) {
return Err("This action contains an unsupported object type.");
}
// State flags may have changed through the existing visibility grip.
if let Some(e) = staged.get_entity(h) {
entity.common_mut().invisible = e.common().invisible;
}
for (action_handle, a, kind, id) in &all {
if !a.dependencies.contains(&h) {
continue;
}
let q = parameters(baseline, definition, *kind)
.into_iter()
.find(|p| p.id == *id)
.ok_or("An action has an unsupported parameter binding.")?;
if !test_parameter_supported(baseline, q.handle) {
return Err("This parameter uses value constraints or chaining not supported in Test Block yet.");
}
let end = current(&staged, &q);
let transform = match kind {
Kind::Move => crate::command::EntityTransform::Translate(end - q.end),
Kind::Scale => {
let factor = end.distance(q.base) / q.end.distance(q.base);
if !factor.is_finite() || factor < 1e-9 {
return Err("Specify a positive scale distance.");
}
crate::command::EntityTransform::Scale {
center: q.base,
factor,
}
}
};
// Imported actions with custom multipliers/offsets/chaining must
// not silently behave like the simple actions authored here.
if !a.action_ids.is_empty() {
return Err("Chained actions are not supported in Test Block yet.");
}
let ObjectType::DynamicBlock(o) = &baseline.objects[action_handle] else {
unreachable!()
};
match &o.data {
DynamicBlockData::MoveAction(m)
if m.offsets.distance_multiplier != 1.
|| m.offsets.angle_offset != 0.
|| m.offsets.flags != 0
|| m.connections
!= if q.point {
[
BlockConnection {
code: q.id,
name: "XDelta".into(),
},
BlockConnection {
code: q.id,
name: "YDelta".into(),
},
]
} else {
[
BlockConnection {
code: q.id,
name: "EndXDelta".into(),
},
BlockConnection {
code: q.id,
name: "EndYDelta".into(),
},
]
} =>
{
return Err("This Move action uses options not supported in Test Block yet.")
}
DynamicBlockData::ScaleAction(s)
if !s.action.dependent
|| s.action.offset != Vector3::ZERO
|| s.connections
!= ["Scale", "XScale", "YScale"].map(|name| BlockConnection {
code: q.id,
name: name.into(),
})
|| s.action.connections
!= ["UpdatedBaseX", "UpdatedBaseY"].map(|name| BlockConnection {
code: q.id,
name: name.into(),
}) =>
{
return Err("This Scale action uses options not supported in Test Block yet.")
}
_ => {}
}
super::view::dispatch::apply_transform(&mut entity, &transform);
let bounds = entity.as_entity().bounding_box();
if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() {
return Err("The parameter value produces non-finite geometry.");
}
}
*staged
.get_entity_mut(h)
.ok_or("An action target was removed.")? = entity;
}
*doc = staged;
Ok(true)
}
fn test_parameter_supported(doc: &CadDocument, handle: Handle) -> bool {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else {
return false;
};
let distance = |s: &codec::objects::BlockParameterValueSet| {
s.flags == 1
&& s.minimum == 0.
&& s.maximum == 0.
&& s.increment == 0.
&& s.values.is_empty()
};
match &o.data {
DynamicBlockData::PointParameter(p) => !p.parameter.parameter.chain_actions,
DynamicBlockData::LinearParameter(p) => {
!p.parameter.parameter.chain_actions && distance(&p.value_set)
}
DynamicBlockData::XYParameter(p) => {
!p.parameter.parameter.chain_actions
&& distance(&p.x_value_set)
&& distance(&p.y_value_set)
}
DynamicBlockData::PolarParameter(p) => {
!p.parameter.parameter.chain_actions
&& distance(&p.distance_value_set)
&& p.angle_value_set.flags == 3
&& p.angle_value_set.minimum == 0.
&& p.angle_value_set.maximum == std::f64::consts::TAU
&& p.angle_value_set.increment == 0.
&& p.angle_value_set.values.is_empty()
}
_ => false,
}
}
+214
View File
@@ -0,0 +1,214 @@
use super::*;
use crate::scene::{block_distance_parameter as distance, block_point_parameter as point, Scene};
use codec::{DwgReader, DwgWriter, DxfReader, DxfWriter};
use std::io::Cursor;
fn fixture() -> (CadDocument, Handle, Vec<Handle>) {
let mut scene = Scene::new();
scene.define_block_raw(
"ACTIONS",
Vector3::ZERO,
vec![
EntityType::Line(codec::entities::Line::from_points(
Vector3::UNIT_X,
Vector3::new(2., 0., 0.),
)),
EntityType::Circle(codec::entities::Circle::from_center_radius(
Vector3::new(4., 3., 0.),
2.,
)),
],
);
let block = scene.document.block_records.get("ACTIONS").unwrap();
let (definition, handles) = (block.handle, block.entity_handles.clone());
(scene.document, definition, handles)
}
fn linear(doc: &mut CadDocument, def: Handle) -> Handle {
distance::create(
doc,
def,
distance::Kind::Linear,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
Vector3::new(5., 2., 0.),
)
.unwrap()
}
fn start(doc: &CadDocument, h: Handle) -> Vector3 {
let EntityType::Line(l) = doc.get_entity(h).unwrap() else {
panic!()
};
l.start
}
#[test]
fn block_actions_native_bindings_roundtrip_and_append_preserves_neighbors() {
let (mut doc, def, handles) = fixture();
let p = linear(&mut doc, def);
for kind in [Kind::Move, Kind::Scale] {
create(
&mut doc,
def,
kind,
p,
&[handles[0]],
DVec3::new(3., 4., 0.),
)
.unwrap();
}
let graph = authoring::graph(&doc, def).unwrap().unwrap();
let ObjectType::DynamicBlock(o) = &doc.objects[&graph] else {
panic!()
};
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
panic!()
};
assert!(g
.nodes
.iter()
.all(|n| authoring::graph::valid_lists(g, n.id)));
let dwg = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
.read()
.unwrap();
let dxf = DxfReader::from_reader(Cursor::new(DxfWriter::new(&doc).write_to_vec().unwrap()))
.unwrap()
.read()
.unwrap();
for saved in [dwg, dxf] {
assert_eq!(actions(&saved, def).len(), 2);
for (h, _, _, _) in actions(&doc, def) {
assert_eq!(saved.objects[&h], doc.objects[&h]);
}
let mut test = saved.clone();
assert!(evaluate(&mut test, &saved, def, p, DVec3::new(20., 5., 0.)).unwrap());
assert_eq!(start(&test, handles[0]), Vector3::new(22., 0., 0.));
assert_eq!(test.get_entity(handles[1]), saved.get_entity(handles[1]));
}
}
#[test]
fn block_actions_point_move_is_absolute_reversible_and_leaves_unselected_objects() {
let (mut doc, def, handles) = fixture();
let p = point::create(
&mut doc,
def,
Vector3::new(2., 3., 0.),
Vector3::new(5., 6., 0.),
)
.unwrap();
create(&mut doc, def, Kind::Move, p, &[handles[0]], DVec3::ZERO).unwrap();
let baseline = doc.clone();
for _ in 0..8 {
assert!(evaluate(&mut doc, &baseline, def, p, DVec3::new(12., -7., 0.)).unwrap());
assert_eq!(start(&doc, handles[0]), Vector3::new(11., -10., 0.));
assert!(!evaluate(&mut doc, &baseline, def, p, DVec3::new(12., -7., 0.)).unwrap());
assert!(evaluate(&mut doc, &baseline, def, p, DVec3::new(2., 3., 0.)).unwrap());
assert_eq!(start(&doc, handles[0]), start(&baseline, handles[0]));
}
assert_eq!(doc.get_entity(handles[1]), baseline.get_entity(handles[1]));
}
#[test]
fn block_actions_scale_uses_distance_ratio_and_parameter_base() {
let (mut doc, def, handles) = fixture();
let p = distance::create(
&mut doc,
def,
distance::Kind::Polar,
Vector3::new(4., 3., 0.),
Vector3::new(14., 3., 0.),
Vector3::new(9., 5., 0.),
)
.unwrap();
create(&mut doc, def, Kind::Scale, p, &[handles[1]], DVec3::ZERO).unwrap();
let baseline = doc.clone();
evaluate(&mut doc, &baseline, def, p, DVec3::new(4., 23., 0.)).unwrap();
let EntityType::Circle(c) = doc.get_entity(handles[1]).unwrap() else {
panic!()
};
assert_eq!(c.center, Vector3::new(4., 3., 0.));
assert_eq!(c.radius, 4.);
let before = doc.clone();
assert!(evaluate(&mut doc, &baseline, def, p, DVec3::new(4., 3., 0.)).is_err());
assert_eq!(doc, before);
}
#[test]
fn block_actions_reject_invalid_selection_binding_and_nonfinite_without_mutation() {
let (mut doc, def, handles) = fixture();
let p = linear(&mut doc, def);
let before = doc.clone();
for (parameter, targets, location) in [
(Handle::NULL, vec![handles[0]], DVec3::ZERO),
(p, vec![], DVec3::ZERO),
(p, vec![Handle::new(0xff_ff_ff)], DVec3::ZERO),
(p, vec![handles[0]], DVec3::splat(f64::NAN)),
] {
assert!(create(&mut doc, def, Kind::Move, parameter, &targets, location).is_err());
assert_eq!(doc, before);
}
let action = create(&mut doc, def, Kind::Move, p, &[handles[0]], DVec3::ZERO).unwrap();
let baseline = doc.clone();
assert!(evaluate(&mut doc, &baseline, def, p, DVec3::splat(f64::NAN)).is_err());
assert_eq!(doc, baseline);
let mut restricted = doc.clone();
let ObjectType::DynamicBlock(object) = restricted.objects.get_mut(&p).unwrap() else {
panic!()
};
let DynamicBlockData::LinearParameter(parameter) = &mut object.data else {
panic!()
};
parameter.value_set.maximum = 15.;
parameter.value_set.flags |= 2;
let restricted_baseline = restricted.clone();
assert!(evaluate(
&mut restricted,
&restricted_baseline,
def,
p,
DVec3::new(20., 0., 0.)
)
.is_err());
assert_eq!(
restricted, restricted_baseline,
"unsupported value constraints are not silently ignored"
);
let ObjectType::DynamicBlock(o) = doc.objects.get_mut(&action).unwrap() else {
panic!()
};
let DynamicBlockData::MoveAction(m) = &mut o.data else {
panic!()
};
m.offsets.distance_multiplier = 0.5;
let baseline = doc.clone();
assert!(evaluate(&mut doc, &baseline, def, p, DVec3::new(15., 0., 0.)).is_err());
assert_eq!(doc, baseline);
}
#[test]
fn block_actions_reframe_moves_markers_and_retains_targets_and_connections() {
let (mut doc, def, handles) = fixture();
let p = linear(&mut doc, def);
let h = create(
&mut doc,
def,
Kind::Scale,
p,
&[handles[0]],
DVec3::new(3., 4., 0.),
)
.unwrap();
let before = doc.objects[&h].clone();
let t = Transform::from_translation(Vector3::new(-2., -3., 0.));
assert_eq!(reframe(&mut doc, def, &t), 1);
let ObjectType::DynamicBlock(o) = &doc.objects[&h] else {
panic!()
};
let DynamicBlockData::ScaleAction(s) = &o.data else {
panic!()
};
assert_eq!(s.action.action.display_location, Vector3::new(1., 1., 0.));
let ObjectType::DynamicBlock(o) = before else {
panic!()
};
let DynamicBlockData::ScaleAction(b) = o.data else {
panic!()
};
assert_eq!(s.action.action.dependencies, b.action.action.dependencies);
assert_eq!(s.connections, b.connections);
}
+27
View File
@@ -1,4 +1,6 @@
//! Shared native graph construction for Block Editor authoring objects.
pub(crate) mod graph;
pub(crate) mod edit;
use codec::objects::{
BlockEvalExpression, BlockEvalValue, DynamicBlockData, DynamicBlockObject, ObjectType,
};
@@ -219,6 +221,31 @@ pub(super) fn append(
append_components(document, handle, slots, &expressions, &[2, 3]);
}
/// Basepoint parameters and action objects can be standalone expressions.
pub(super) fn append_single(
document: &mut CadDocument,
handle: Handle,
slots: Slots,
expression: Handle,
) {
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
unreachable!()
};
let DynamicBlockData::EvaluationGraph(graph) = &mut object.data else {
unreachable!()
};
graph.nodes.push(codec::objects::BlockEvaluationNode {
id: slots.node,
next_id: slots.eval,
expression,
edge_flags: 32,
node_data: [-1; 4],
active_cycles: None,
});
graph.first_node_id = slots.eval;
graph.first_node_id_copy = slots.eval;
}
/// Append a parameter and grip, followed by its location/state expressions.
/// `components` gives the expression offsets in native dependency order.
pub(super) fn append_components(
+523
View File
@@ -0,0 +1,523 @@
//! Editor picking and native definition-point edits. No proxy drawing entities.
use super::*;
use crate::scene::block_distance_parameter::{self as distance, Graphic};
use codec::objects::{BlockGrip, BlockTwoPointParameter};
use codec::types::Vector3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Part {
Point,
Base,
End,
Label(usize),
Reference,
}
pub struct Item {
pub handle: Handle,
pub name: String,
pub graphic: Graphic,
pub grips: Vec<(Part, Vector3)>,
}
pub fn items(doc: &CadDocument, definition: Handle) -> Vec<Item> {
let mut result = vec![];
for (handle, p) in crate::scene::block_point_parameter::parameters(doc, definition) {
let mut graphic = Graphic::default();
graphic
.lines
.push([p.parameter.definition_point, p.definition_label_point]);
graphic
.labels
.push((p.definition_label_point, p.position_name.clone()));
result.push(Item {
handle,
name: p.position_name.clone(),
graphic,
grips: vec![
(Part::Point, p.parameter.definition_point),
(Part::Label(0), p.definition_label_point),
],
});
}
for p in distance::parameters(doc, definition) {
let graphic = distance::graphic(&p);
let mut grips = vec![
(Part::Base, p.native.definition_base_point),
(Part::End, p.native.definition_end_point),
];
// Polar's second label is derived from its angle arc, not an editable offset.
grips.extend(
graphic
.labels
.iter()
.take(if p.kind == distance::Kind::XY { 2 } else { 1 })
.enumerate()
.map(|(i, (p, _))| (Part::Label(i), *p)),
);
result.push(Item {
handle: p.handle,
name: p.labels.join(" / "),
graphic,
grips,
});
}
for p in crate::scene::block_orientation_parameter::parameters(doc, definition) {
use crate::scene::block_orientation_parameter::Kind;
let graphic = crate::scene::block_orientation_parameter::graphic(&p);
let mut grips = vec![
(Part::Base, p.native.definition_base_point),
(Part::End, p.native.definition_end_point),
];
if p.kind == Kind::Rotation {
grips.push((Part::Reference, p.reference));
}
if p.kind != Kind::Alignment {
grips.extend(
graphic
.labels
.iter()
.take(1)
.map(|(p, _)| (Part::Label(0), *p)),
);
}
result.push(Item {
handle: p.handle,
name: p.label.clone(),
graphic,
grips,
});
}
for (handle, p) in crate::scene::block_lookup::parameters(doc, definition) {
result.push(single(
handle,
p.lookup_name.clone(),
p.parameter.definition_point,
));
}
if let Some((handle, p)) = crate::scene::block_basepoint::parameter(doc, definition) {
result.push(single(
handle,
"Basepoint".into(),
p.parameter.definition_point,
));
}
if let Some(p) = crate::scene::block_visibility::parameter(doc, definition) {
result.push(single(p.handle, p.name.clone(), p.def_point));
}
for (handle, p, _, _) in crate::scene::block_actions::actions(doc, definition) {
result.push(single(handle, p.element.name.clone(), p.display_location));
}
result
}
fn single(handle: Handle, name: String, point: Vector3) -> Item {
let mut graphic = Graphic::default();
graphic.labels.push((point, name.clone()));
Item {
handle,
name,
graphic,
grips: vec![(Part::Point, point)],
}
}
fn two(data: &mut DynamicBlockData) -> Option<&mut BlockTwoPointParameter> {
Some(match data {
DynamicBlockData::LinearParameter(p) => &mut p.parameter,
DynamicBlockData::PolarParameter(p) => &mut p.parameter,
DynamicBlockData::XYParameter(p) => &mut p.parameter,
DynamicBlockData::RotationParameter(p) => &mut p.parameter,
DynamicBlockData::AlignmentParameter(p) => &mut p.parameter,
DynamicBlockData::FlipParameter(p) => &mut p.parameter,
_ => return None,
})
}
fn grip(data: &mut DynamicBlockData) -> Option<&mut BlockGrip> {
Some(match data {
DynamicBlockData::LinearGrip(p) | DynamicBlockData::AlignmentGrip(p) => &mut p.grip,
DynamicBlockData::XYGrip(p)
| DynamicBlockData::PolarGrip(p)
| DynamicBlockData::RotationGrip(p)
| DynamicBlockData::LookupGrip(p)
| DynamicBlockData::VisibilityGrip(p) => p,
DynamicBlockData::FlipGrip(p) => &mut p.grip,
_ => return None,
})
}
pub fn move_part(
doc: &mut CadDocument,
definition: Handle,
handle: Handle,
part: Part,
at: Vector3,
) -> Result<bool, &'static str> {
if ![at.x, at.y, at.z].iter().all(|v| v.is_finite()) {
return Err("Specify a finite authoring location.");
}
let item = items(doc, definition)
.into_iter()
.find(|i| i.handle == handle)
.ok_or("The authoring object is no longer available.")?;
let old = item
.grips
.iter()
.find(|(p, _)| *p == part)
.map(|(_, p)| *p)
.ok_or("This authoring grip is unavailable.")?;
if (at.z - old.z).abs() > 1e-8 {
return Err("Keep the authoring grip in its block XY plane.");
}
if (at - old).length() < 1e-10 {
return Ok(false);
}
let graph = graph(doc, definition)?.ok_or("The authoring graph is unavailable.")?;
let mut staged = doc.clone();
if crate::scene::block_basepoint::parameter(doc, definition).is_some_and(|(h, _)| h == handle) {
crate::scene::block_basepoint::create(&mut staged, definition, at)?;
*doc = staged;
return Ok(true);
}
let mut moved_grips = vec![];
match staged
.objects
.get_mut(&handle)
.ok_or("The authoring object is unavailable.")?
{
ObjectType::BlockVisibilityParameter(p) => {
p.def_point = at;
moved_grips.extend(
p.property_info
.iter()
.flat_map(|p| p.connections.iter().map(|c| (c.code, at))),
);
}
ObjectType::DynamicBlock(object) => {
if let Some(p) = two(&mut object.data) {
match part {
Part::Base => {
p.definition_base_point = at;
p.base_point = p.base_point.map(|_| at);
p.updated_base_point = p.updated_base_point.map(|_| at);
moved_grips.push((p.property_states[0], at));
}
Part::End => {
p.definition_end_point = at;
p.end_point = p.end_point.map(|_| at);
p.updated_end_point = p.updated_end_point.map(|_| at);
moved_grips.push((p.property_states[1], at));
}
_ => {}
}
let length = (p.definition_end_point - p.definition_base_point).length();
if !length.is_finite() || length < 1e-10 {
return Err("Parameter endpoints must remain distinct and finite.");
}
}
match &mut object.data {
DynamicBlockData::PointParameter(p) => match part {
Part::Point => {
p.parameter.definition_point = at;
moved_grips.push((p.parameter.property_count, at))
}
Part::Label(_) => p.definition_label_point = at,
_ => {}
},
DynamicBlockData::LookupParameter(p) => {
p.parameter.definition_point = at;
moved_grips.push((p.parameter.property_count, at))
}
DynamicBlockData::BasePointParameter(p) => {
p.parameter.definition_point = at;
p.point = at;
p.base_point = at;
}
DynamicBlockData::LinearParameter(p) => {
if let Part::Label(_) = part {
p.distance = label_offset(&p.parameter, at)
}
}
DynamicBlockData::PolarParameter(p) => {
if let Part::Label(_) = part {
p.offset = label_offset(&p.parameter, at)
}
}
DynamicBlockData::XYParameter(p) => {
if let Part::Label(index) = part {
if index == 0 {
p.x_value = at.y - p.parameter.definition_base_point.y
} else {
p.y_value = at.x - p.parameter.definition_base_point.x
}
}
if (p.parameter.definition_end_point.x - p.parameter.definition_base_point.x)
.abs()
< 1e-10
|| (p.parameter.definition_end_point.y
- p.parameter.definition_base_point.y)
.abs()
< 1e-10
{
return Err("XY parameter endpoints need both X and Y distances.");
}
}
DynamicBlockData::RotationParameter(p) => match part {
Part::Reference => {
if (at - p.parameter.definition_base_point).length() < 1e-10 {
return Err("The rotation reference must differ from its base.");
}
p.definition_base_angle_point = at
}
Part::Label(_) => {
let b = p.parameter.definition_base_point;
p.angle =
(at - b).length() - (p.parameter.definition_end_point - b).length()
}
_ => {}
},
DynamicBlockData::FlipParameter(p) => {
if let Part::Label(_) = part {
p.definition_label_point = at
}
}
DynamicBlockData::MoveAction(p) => p.action.display_location = at,
DynamicBlockData::ScaleAction(p) => p.action.action.display_location = at,
DynamicBlockData::AlignmentParameter(_) => {}
_ => return Err("This authoring object cannot be edited yet."),
}
if let DynamicBlockData::RotationParameter(p) = &mut object.data {
let base = p.parameter.definition_base_point;
if part == Part::Reference {
let radius = (p.definition_base_angle_point - base).length();
let delta = p.parameter.definition_end_point - base;
let end = base + delta * (radius / delta.length());
p.parameter.definition_end_point = end;
p.parameter.end_point = p.parameter.end_point.map(|_| end);
p.parameter.updated_end_point = p.parameter.updated_end_point.map(|_| end);
moved_grips.push((p.parameter.property_states[1], end));
} else if matches!(part, Part::Base | Part::End) {
let old_base = item.grips.iter().find(|(p, _)| *p == Part::Base).unwrap().1;
let direction = p.definition_base_angle_point - old_base;
let radius = (p.parameter.definition_end_point - base).length();
p.definition_base_angle_point =
base + direction * (radius / direction.length());
}
if let Some(e) = crate::scene::block_orientation_parameter::points_error(
crate::scene::block_orientation_parameter::Kind::Rotation,
base,
p.parameter.definition_end_point,
p.definition_base_angle_point,
) {
return Err(e);
}
}
}
_ => return Err("This is not an authoring object."),
}
for object in staged.objects.values_mut() {
let ObjectType::DynamicBlock(object) = object else {
continue;
};
if object.owner != graph {
continue;
}
let Some(g) = grip(&mut object.data) else {
continue;
};
if let Some((_, location)) = moved_grips
.iter()
.find(|(id, _)| *id > 0 && *id == g.element.eval.node_id)
{
g.location = *location;
}
}
// Keep the Linear grip's native direction consistent after endpoint edits.
if matches!(part, Part::Base | Part::End) {
if let Some(p) = distance::parameters(&staged, definition)
.into_iter()
.find(|p| p.handle == handle)
{
let delta = p.native.definition_end_point - p.native.definition_base_point;
let orientation = delta * (1.0 / delta.length());
let ids = p.native.property_states;
for o in staged.objects.values_mut() {
if let ObjectType::DynamicBlock(o) = o {
if o.owner == graph {
if let DynamicBlockData::LinearGrip(g) = &mut o.data {
if ids.contains(&g.grip.element.eval.node_id) {
g.orientation = orientation;
}
}
}
}
}
}
if let Some(p) = crate::scene::block_orientation_parameter::parameters(&staged, definition)
.into_iter()
.find(|p| p.handle == handle)
{
let orientation = p.native.definition_end_point - p.native.definition_base_point;
let ids = p.native.property_states;
for o in staged.objects.values_mut() {
if let ObjectType::DynamicBlock(o) = o {
if o.owner == graph {
match &mut o.data {
DynamicBlockData::AlignmentGrip(g)
if ids.contains(&g.grip.element.eval.node_id) =>
{
g.orientation = orientation
}
DynamicBlockData::FlipGrip(g)
if ids.contains(&g.grip.element.eval.node_id) =>
{
g.orientation = orientation
}
_ => {}
}
}
}
}
}
}
if let Some(ObjectType::BlockVisibilityParameter(p)) = staged.objects.get(&handle) {
staged.block_visibility_params.insert(handle, p.clone());
}
if let Some(item) = items(&staged, definition)
.into_iter()
.find(|i| i.handle == handle)
{
let finite = |p: Vector3| [p.x, p.y, p.z].iter().all(|v| v.is_finite());
if !item.grips.iter().all(|(_, p)| finite(*p))
|| !item.graphic.lines.iter().flatten().all(|p| finite(*p))
{
return Err("The authoring location produces non-finite geometry.");
}
}
*doc = staged;
Ok(true)
}
fn label_offset(p: &BlockTwoPointParameter, at: Vector3) -> f64 {
let d = p.definition_end_point - p.definition_base_point;
let n = Vector3::new(-d.y, d.x, 0.) * (1.0 / d.x.hypot(d.y));
let delta = at - (p.definition_base_point + p.definition_end_point) * 0.5;
delta.x * n.x + delta.y * n.y
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scene::{
block_orientation_parameter as orientation, block_point_parameter as point, Scene,
};
#[test]
fn authoring_edit_all_parameter_kinds_keep_native_grips_and_graph_identity() {
let mut scene = Scene::new();
scene.define_block_raw("EDIT", Vector3::ZERO, vec![]);
let doc = &mut scene.document;
let def = doc.block_records.get("EDIT").unwrap().handle;
let b = Vector3::new(2., 3., 0.);
let e = Vector3::new(6., 7., 0.);
let l = Vector3::new(1., 8., 0.);
point::create(doc, def, b, l).unwrap();
for kind in [
distance::Kind::Linear,
distance::Kind::Polar,
distance::Kind::XY,
] {
distance::create(doc, def, kind, b, e, l).unwrap();
}
for kind in [
orientation::Kind::Rotation,
orientation::Kind::Alignment,
orientation::Kind::Flip,
] {
orientation::create(
doc,
def,
kind,
b,
e,
Vector3::new(b.x + (e - b).length(), b.y, 0.),
)
.unwrap();
}
crate::scene::block_lookup::create(doc, def, b).unwrap();
crate::scene::block_basepoint::create(doc, def, b).unwrap();
let visibility = crate::scene::block_visibility::create(doc, def, b).unwrap();
let graph_handle = graph(doc, def).unwrap().unwrap();
let graph_before = doc.objects[&graph_handle].clone();
let inventory = items(doc, def);
assert_eq!(inventory.len(), 10);
for item in inventory {
for (part, _) in item.grips {
let old = items(doc, def)
.into_iter()
.find(|i| i.handle == item.handle)
.unwrap()
.grips
.into_iter()
.find(|(p, _)| *p == part)
.unwrap()
.1;
let next = old + Vector3::new(0.3, 0.5, 0.);
assert!(
move_part(doc, def, item.handle, part, next).unwrap(),
"{} {part:?}",
item.name
);
let after = items(doc, def)
.into_iter()
.find(|i| i.handle == item.handle)
.unwrap();
let actual = after.grips.iter().find(|(p, _)| *p == part).unwrap().1;
// Dimension labels project onto their normal/radius; definition points are exact.
if !matches!(part, Part::Label(_)) {
assert!((actual - next).length() < 1e-10);
}
let snapshot = doc.clone();
assert!(!move_part(doc, def, item.handle, part, actual).unwrap());
assert!(
move_part(doc, def, item.handle, part, Vector3::new(f64::NAN, 0., 0.)).is_err()
);
assert_eq!(*doc, snapshot);
}
}
assert_eq!(
doc.objects[&graph_handle], graph_before,
"editing positions must preserve graph lists and node identities"
);
assert_eq!(
crate::scene::block_visibility::parameter(doc, def)
.unwrap()
.def_point,
items(doc, def)
.into_iter()
.find(|i| i.handle == visibility)
.unwrap()
.grips[0]
.1
);
for p in distance::parameters(doc, def) {
let ids = p.native.property_states;
let expected = p.native.definition_end_point;
for o in doc.objects.values() {
if let ObjectType::DynamicBlock(o) = o {
let mut data = o.data.clone();
if let Some(g) = grip(&mut data) {
if g.element.eval.node_id == ids[1] {
assert_eq!(g.location, expected);
}
}
}
}
}
let bytes = codec::DxfWriter::new(doc).write_to_vec().unwrap();
let saved = codec::DxfReader::from_reader(std::io::Cursor::new(bytes))
.unwrap()
.read()
.unwrap();
let a = items(doc, def);
let b = items(&saved, def);
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b) {
assert_eq!(a.handle, b.handle);
assert_eq!(a.grips, b.grips);
}
}
}
+220
View File
@@ -0,0 +1,220 @@
//! Maintain native adjacency lists while adding/replacing action dependencies.
use codec::objects::{BlockEvaluationEdge, BlockEvaluationGraph, DynamicBlockData, ObjectType};
use codec::{CadDocument, Handle};
pub(crate) fn valid_lists(g: &BlockEvaluationGraph, node: i32) -> bool {
let Some(n) = g.nodes.iter().find(|n| n.id == node) else {
return false;
};
for (head, tail, previous_slot, next_slot, outgoing) in
[(0, 1, 0, 1, false), (2, 3, 2, 3, true)]
{
let mut current = n.node_data[head];
let mut previous = -1;
let mut seen = std::collections::BTreeSet::new();
while current >= 0 {
if !seen.insert(current) {
return false;
}
let Some(edge) = g.edges.iter().find(|e| e.id == current) else {
return false;
};
if (if outgoing {
edge.source_node
} else {
edge.destination_node
}) != node
|| edge.outgoing_edges[previous_slot] != previous
{
return false;
}
previous = current;
current = edge.outgoing_edges[next_slot];
}
if previous != n.node_data[tail]
|| seen.len()
!= g.edges
.iter()
.filter(|e| {
(if outgoing {
e.source_node
} else {
e.destination_node
}) == node
})
.count()
{
return false;
}
}
true
}
fn remove(g: &mut BlockEvaluationGraph, id: i32) {
let edge = g.edges.iter().find(|e| e.id == id).unwrap().clone();
let [pi, ni, po, no, _] = edge.outgoing_edges;
for (neighbor, field, value) in [(pi, 1, ni), (ni, 0, pi), (po, 3, no), (no, 2, po)] {
if let Some(e) = g.edges.iter_mut().find(|e| e.id == neighbor) {
e.outgoing_edges[field] = value;
}
}
for n in &mut g.nodes {
if n.id == edge.destination_node {
if n.node_data[0] == id {
n.node_data[0] = ni;
}
if n.node_data[1] == id {
n.node_data[1] = pi;
}
}
if n.id == edge.source_node {
if n.node_data[2] == id {
n.node_data[2] = no;
}
if n.node_data[3] == id {
n.node_data[3] = po;
}
}
}
g.edges.retain(|e| e.id != id);
}
fn add(g: &mut BlockEvaluationGraph, id: i32, source: i32, destination: i32, inverse: i32) {
let incoming = g
.nodes
.iter()
.find(|n| n.id == destination)
.unwrap()
.node_data[1];
let outgoing = g.nodes.iter().find(|n| n.id == source).unwrap().node_data[3];
if let Some(e) = g.edges.iter_mut().find(|e| e.id == incoming) {
e.outgoing_edges[1] = id;
}
if let Some(e) = g.edges.iter_mut().find(|e| e.id == outgoing) {
e.outgoing_edges[3] = id;
}
for n in &mut g.nodes {
if n.id == destination {
if n.node_data[0] < 0 {
n.node_data[0] = id;
}
n.node_data[1] = id;
}
if n.id == source {
if n.node_data[2] < 0 {
n.node_data[2] = id;
}
n.node_data[3] = id;
}
}
g.edges.push(BlockEvaluationEdge {
id,
next_id: 4,
incoming_edge: 1,
source_node: source,
destination_node: destination,
outgoing_edges: [incoming, -1, outgoing, -1, inverse],
});
}
pub(crate) fn bind_ids(
document: &mut CadDocument,
graph: Handle,
action: i32,
parameters: &[i32],
) -> Result<(), &'static str> {
let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&graph) else {
return Err("The authoring graph is unavailable.");
};
let DynamicBlockData::EvaluationGraph(original) = &object.data else {
return Err("The authoring graph is unavailable.");
};
let mut g = original.clone();
if std::iter::once(action)
.chain(parameters.iter().copied())
.any(|id| g.nodes.iter().filter(|n| n.next_id == id).count() != 1)
{
return Err("An action binding has an invalid or ambiguous expression ID.");
}
let action = g
.nodes
.iter()
.find(|n| n.next_id == action)
.ok_or("The action node is unavailable.")?
.id;
let mut bindings = vec![];
for parameter in parameters {
let node = g
.nodes
.iter()
.find(|n| n.next_id == *parameter)
.ok_or("A bound parameter no longer belongs to this block.")?
.id;
if node == action {
return Err("An action cannot bind to itself.");
}
if !bindings.contains(&node) {
bindings.push(node);
}
}
let old = g
.edges
.iter()
.filter(|e| e.source_node == action || e.destination_node == action)
.collect::<Vec<_>>();
if std::iter::once(action)
.chain(bindings.iter().copied())
.chain(old.iter().flat_map(|e| [e.source_node, e.destination_node]))
.any(|n| !valid_lists(&g, n))
{
return Err("The authoring graph has invalid adjacency links.");
}
if old.iter().any(|e| e.next_id != 4) {
return Err("This action has unsupported graph dependencies.");
}
if old.iter().any(|e| {
!old.iter().any(|inverse| {
inverse.id == e.outgoing_edges[4]
&& inverse.outgoing_edges[4] == e.id
&& inverse.source_node == e.destination_node
&& inverse.destination_node == e.source_node
})
}) {
return Err("The authoring graph has invalid inverse dependencies.");
}
let expected = bindings
.iter()
.flat_map(|n| [(action, *n), (*n, action)])
.collect::<std::collections::BTreeSet<_>>();
if old.len() == expected.len()
&& old
.iter()
.map(|e| (e.source_node, e.destination_node))
.collect::<std::collections::BTreeSet<_>>()
== expected
{
return Ok(());
}
let mut id = g
.edges
.iter()
.map(|e| e.id)
.max()
.unwrap_or(-1)
.checked_add(1)
.ok_or("The graph has no free edge IDs.")?;
id.checked_add(bindings.len() as i32 * 2 - 1)
.ok_or("The graph has no free edge IDs.")?;
let old = old.iter().map(|e| e.id).collect::<Vec<_>>();
for edge in old {
remove(&mut g, edge);
}
for node in bindings {
add(&mut g, id, node, action, id + 1);
add(&mut g, id + 1, action, node, id);
id = id.checked_add(2).unwrap_or(i32::MAX);
}
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&graph).unwrap() else {
unreachable!()
};
object.data = DynamicBlockData::EvaluationGraph(g);
Ok(())
}
+154
View File
@@ -0,0 +1,154 @@
//! The native authoring anchor, separate from the BLOCK definition origin.
use super::block_authoring::{self as authoring, *};
use codec::objects::{
BlockBasePointParameter, BlockElement, BlockEvalValue, BlockOnePointParameter, BlockParameter,
DynamicBlockData, ObjectType,
};
use codec::{
types::{Transform, Vector3},
CadDocument, Handle,
};
pub(crate) fn parameter(
document: &CadDocument,
definition: Handle,
) -> Option<(Handle, &BlockBasePointParameter)> {
let ObjectType::Dictionary(dictionary) = document
.objects
.get(&document.extension_dictionary_handle(definition)?)?
else {
return None;
};
let graph = dictionary.get("ACAD_ENHANCEDBLOCK")?;
let ObjectType::DynamicBlock(object) = document.objects.get(&graph)? else {
return None;
};
let DynamicBlockData::EvaluationGraph(data) = &object.data else {
return None;
};
data.nodes
.iter()
.find_map(|node| match document.objects.get(&node.expression) {
Some(ObjectType::DynamicBlock(object)) if object.owner == graph => match &object.data {
DynamicBlockData::BasePointParameter(p) => Some((object.handle, p)),
_ => None,
},
_ => None,
})
}
pub(crate) fn placement_error(document: &CadDocument, definition: Handle) -> Option<&'static str> {
authoring::graph(document, definition).err()
}
pub(crate) fn create(
document: &mut CadDocument,
definition: Handle,
location: Vector3,
) -> Result<Handle, &'static str> {
if ![location.x, location.y, location.z]
.iter()
.all(|v| v.is_finite())
{
return Err("Specify a finite parameter location.");
}
authoring::graph(document, definition)?;
let name = document
.block_records
.iter()
.find(|b| b.handle == definition)
.unwrap()
.name
.clone();
let old_base = super::render_graph::block_base_point(document, &name);
if let Some((handle, _)) = parameter(document, definition) {
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
unreachable!()
};
let DynamicBlockData::BasePointParameter(p) = &mut object.data else {
unreachable!()
};
p.parameter.definition_point = location;
p.point = location;
p.base_point = location;
move_attributes(document, &name, old_base, location);
return Ok(handle);
}
let (graph, slots) = authoring::prepare(document, definition)?;
register_classes(
document,
&[("BLOCKBASEPOINTPARAMETER", "AcDbBlockBasepointParameter", 55)],
);
let handle = document.allocate_handle();
let native = BlockBasePointParameter {
parameter: BlockOnePointParameter {
parameter: BlockParameter {
element: BlockElement {
eval: expression(slots.eval, NO_VALUE, BlockEvalValue::None),
name: "Base Point".into(),
major: MAJOR,
minor: MINOR,
eed_1071: 0,
},
show_properties: true,
chain_actions: false,
},
definition_point: location,
..Default::default()
},
point: location,
base_point: location,
};
document.objects.insert(
handle,
ObjectType::DynamicBlock(dynamic(
handle,
graph,
"BLOCKBASEPOINTPARAMETER",
"AcDbBlockBasepointParameter",
DynamicBlockData::BasePointParameter(native),
)),
);
authoring::append_single(document, graph, slots, handle);
move_attributes(document, &name, old_base, location);
Ok(handle)
}
fn move_attributes(document: &mut CadDocument, name: &str, old: Vector3, new: Vector3) {
let handles = document
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Insert(i)
if i.block_name.eq_ignore_ascii_case(name) && !i.attributes.is_empty() =>
{
Some(i.common.handle)
}
_ => None,
})
.collect::<Vec<_>>();
for handle in handles {
if let Some(codec::EntityType::Insert(insert)) = document.get_entity_mut(handle) {
let shift = insert.get_transform().apply_rotation(old - new);
let transform = Transform::from_translation(shift);
for attribute in &mut insert.attributes {
codec::Entity::apply_transform(attribute, &transform);
}
}
}
}
pub(crate) fn reframe(
document: &mut CadDocument,
definition: Handle,
transform: &Transform,
) -> usize {
let Some((handle, _)) = parameter(document, definition) else {
return 0;
};
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&handle) {
if let DynamicBlockData::BasePointParameter(p) = &mut object.data {
p.parameter.definition_point = transform.apply(p.parameter.definition_point);
p.point = transform.apply(p.point);
p.base_point = transform.apply(p.base_point);
return 1;
}
}
0
}
+2
View File
@@ -335,6 +335,8 @@ pub(crate) fn create(
pub(crate) struct Graphic {
pub lines: Vec<[Vector3; 2]>,
pub round_grips: Vec<Vector3>,
pub base_grips: Vec<Vector3>,
pub lookup_grips: Vec<Vector3>,
pub dimensions: Vec<[Vector3; 2]>,
pub labels: Vec<(Vector3, String)>,
pub grips: Vec<(Vector3, bool)>, // Linear uses a direction arrow; others a square.
+411
View File
@@ -0,0 +1,411 @@
//! Native Lookup parameter and property-table authoring.
use super::block_authoring::{self as authoring, *};
use codec::objects::{
BlockAction, BlockElement, BlockEvalValue, BlockGrip, BlockGripExpression, BlockLookupAction,
BlockLookupColumn, BlockLookupParameter, BlockOnePointParameter, BlockParameter,
DynamicBlockData, ObjectType,
};
use codec::{
types::{Transform, Vector3},
CadDocument, Handle,
};
use super::block_authoring::graph;
#[cfg(test)]
mod tests;
fn graph_handle(document: &CadDocument, definition: Handle) -> Option<Handle> {
let ObjectType::Dictionary(d) = document
.objects
.get(&document.extension_dictionary_handle(definition)?)?
else {
return None;
};
d.get("ACAD_ENHANCEDBLOCK")
}
pub(crate) fn parameters(
document: &CadDocument,
definition: Handle,
) -> Vec<(Handle, &BlockLookupParameter)> {
let Some(graph) = graph_handle(document, definition) else {
return vec![];
};
let Some(ObjectType::DynamicBlock(o)) = document.objects.get(&graph) else {
return vec![];
};
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
return vec![];
};
g.nodes
.iter()
.filter_map(|n| match document.objects.get(&n.expression) {
Some(ObjectType::DynamicBlock(o)) if o.owner == graph => match &o.data {
DynamicBlockData::LookupParameter(p) => Some((o.handle, p)),
_ => None,
},
_ => None,
})
.collect()
}
pub(crate) fn placement_error(document: &CadDocument, definition: Handle) -> Option<&'static str> {
authoring::graph(document, definition).err()
}
pub(crate) fn create(
document: &mut CadDocument,
definition: Handle,
location: Vector3,
) -> Result<Handle, &'static str> {
if ![location.x, location.y, location.z]
.iter()
.all(|v| v.is_finite())
{
return Err("Specify a finite parameter location.");
}
let names = parameters(document, definition)
.iter()
.map(|(_, p)| p.lookup_name.clone())
.collect::<Vec<_>>();
let name = (1..)
.map(|n| format!("Lookup{n}"))
.find(|s| names.iter().all(|n| !n.eq_ignore_ascii_case(s)))
.unwrap();
let (graph, slots) = authoring::prepare(document, definition)?;
register_classes(
document,
&[
("BLOCKLOOKUPPARAMETER", "AcDbBlockLookUpParameter", 55),
("BLOCKLOOKUPGRIP", "AcDbBlockLookUpGrip", 55),
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
],
);
let handles = [(); 4].map(|_| document.allocate_handle());
let element = |offset, name: &str| BlockElement {
eval: expression(slots.eval + offset, NO_VALUE, BlockEvalValue::None),
name: name.into(),
major: MAJOR,
minor: MINOR,
eed_1071: 0,
};
let parameter = BlockLookupParameter {
parameter: BlockOnePointParameter {
parameter: BlockParameter {
element: element(0, "Lookup"),
show_properties: true,
chain_actions: false,
},
definition_point: location,
property_count: slots.eval + 1,
..Default::default()
},
lookup_name: name,
..Default::default()
};
document.objects.insert(
handles[0],
ObjectType::DynamicBlock(dynamic(
handles[0],
graph,
"BLOCKLOOKUPPARAMETER",
"AcDbBlockLookUpParameter",
DynamicBlockData::LookupParameter(parameter),
)),
);
let grip = BlockGrip {
element: element(1, "Grip"),
flags_91: slots.eval + 2,
flags_92: slots.eval + 3,
location,
insert_cycling: false,
insert_cycling_weight: -1,
};
document.objects.insert(
handles[1],
ObjectType::DynamicBlock(dynamic(
handles[1],
graph,
"BLOCKLOOKUPGRIP",
"AcDbBlockLookUpGrip",
DynamicBlockData::LookupGrip(grip),
)),
);
for (offset, axis) in [(2, "UpdatedX"), (3, "UpdatedY")] {
let h = handles[offset as usize];
document.objects.insert(
h,
ObjectType::DynamicBlock(dynamic(
h,
graph,
"BLOCKGRIPLOCATIONCOMPONENT",
"AcDbBlockGripExpr",
DynamicBlockData::GripLocationComponent(BlockGripExpression {
eval: expression(slots.eval + offset, 40, BlockEvalValue::Real(0.0)),
grip_type: slots.eval,
expression: axis.into(),
}),
)),
);
}
authoring::append(document, graph, slots, handles);
Ok(handles[0])
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Property {
pub label: String,
pub column: BlockLookupColumn,
pub value: String,
}
/// These distance bindings are verified against the supplied native tables.
pub(crate) fn properties(document: &CadDocument, definition: Handle) -> Vec<Property> {
super::block_distance_parameter::parameters(document, definition)
.iter()
.filter(|p| {
matches!(
p.kind,
super::block_distance_parameter::Kind::Linear
| super::block_distance_parameter::Kind::Polar
)
})
.map(|p| Property {
label: p.labels[0].clone(),
column: BlockLookupColumn {
node_id: p.native.parameter.element.eval.node_id,
value_type: 40,
property_type: 2,
connection_name: "UpdatedDistance".into(),
unmatched_name: "Custom".into(),
..Default::default()
},
value: (p.native.definition_end_point - p.native.definition_base_point)
.length()
.to_string(),
})
.collect()
}
pub(crate) fn table(
document: &CadDocument,
definition: Handle,
parameter: Handle,
) -> Result<Option<(Handle, BlockLookupAction)>, &'static str> {
let p = parameters(document, definition)
.into_iter()
.find(|(h, _)| *h == parameter)
.ok_or("The Lookup parameter is no longer available.")?
.1;
if p.index == 0 {
return Ok(None);
}
let graph = graph_handle(document, definition).ok_or("The authoring graph is unavailable.")?;
document
.objects
.values()
.find_map(|o| match o {
ObjectType::DynamicBlock(o) if o.owner == graph => match &o.data {
DynamicBlockData::LookupAction(a) if a.action.element.eval.node_id == p.index => {
Some((o.handle, a.clone()))
}
_ => None,
},
_ => None,
})
.map(Some)
.ok_or("The Lookup action cannot be edited.")
}
pub(crate) fn empty_table(parameter: &BlockLookupParameter) -> BlockLookupAction {
BlockLookupAction {
column_count: 1,
columns: vec![BlockLookupColumn {
node_id: parameter.parameter.parameter.element.eval.node_id,
value_type: 1,
property_type: 0,
lookup_property: true,
writable: true,
connection_name: "lookupString".into(),
unmatched_name: "Custom".into(),
}],
flag_280: true,
..Default::default()
}
}
pub(crate) fn validate(
table: &BlockLookupAction,
original: Option<&BlockLookupAction>,
) -> Result<(), String> {
let rows = usize::try_from(table.row_count).map_err(|_| "Invalid table size.")?;
let cols = table.columns.len();
if rows == 0
|| rows > 1000
|| cols < 2
|| cols > 64
|| table.column_count != cols as i32
|| table.expressions.len() != rows * cols
{
return Err("Add at least one input property and one complete row.".into());
}
if !table.columns.iter().any(|c| !c.lookup_property)
|| !table.columns.iter().any(|c| c.lookup_property)
{
return Err("A table needs input and lookup properties.".into());
}
let mut inputs = std::collections::BTreeSet::new();
let mut outputs = vec![std::collections::BTreeSet::new(); cols];
for row in 0..rows {
let mut key = vec![];
for (col, property) in table.columns.iter().enumerate() {
let cell = &table.expressions[row * cols + col];
if cell.trim().is_empty() || cell.chars().any(char::is_control) {
return Err("Complete each cell without control characters.".into());
}
if property.lookup_property {
if property.writable && !outputs[col].insert(cell.trim().to_lowercase()) {
return Err("Reverse lookup requires unique lookup values.".into());
}
} else {
let numeric = cell.trim().parse::<f64>().ok().filter(|v| v.is_finite());
if property.value_type == 40
&& numeric.is_none()
&& !original.is_some_and(|old| {
old.columns.iter().enumerate().any(|(index, column)| {
column == property
&& old
.expressions
.iter()
.skip(index)
.step_by(old.columns.len())
.any(|value| value == cell)
})
})
{
return Err("Enter finite decimal input values. Existing expressions can be retained unchanged.".into());
}
key.push(
numeric
.map(|v| if v == 0.0 { "0".into() } else { v.to_string() })
.unwrap_or_else(|| cell.trim().to_lowercase()),
);
}
}
if !inputs.insert(key) {
return Err("Each input-property row must be unique.".into());
}
}
Ok(())
}
pub(crate) fn apply_table(
document: &mut CadDocument,
definition: Handle,
parameter: Handle,
edited: BlockLookupAction,
) -> Result<Handle, String> {
let mut working = document.clone();
let handle = apply_table_inner(&mut working, definition, parameter, edited)?;
*document = working;
Ok(handle)
}
fn apply_table_inner(
document: &mut CadDocument,
definition: Handle,
parameter: Handle,
mut edited: BlockLookupAction,
) -> Result<Handle, String> {
let previous = table(document, definition, parameter).map_err(str::to_string)?;
validate(&edited, previous.as_ref().map(|(_, t)| t))?;
let (graph, slots) = authoring::prepare(document, definition).map_err(str::to_string)?;
let handle = if let Some((handle, original)) = &previous {
edited.action = original.action.clone();
*handle
} else {
register_classes(
document,
&[("BLOCKLOOKUPACTION", "AcDbBlockLookupAction", 55)],
);
let h = document.allocate_handle();
let p = parameters(document, definition)
.into_iter()
.find(|(h, _)| *h == parameter)
.unwrap()
.1;
edited.action = BlockAction {
element: BlockElement {
eval: expression(slots.eval, NO_VALUE, BlockEvalValue::None),
name: p.lookup_name.clone(),
major: MAJOR,
minor: MINOR,
eed_1071: 0,
},
display_location: p.parameter.definition_point,
..Default::default()
};
authoring::append_single(document, graph, slots, h);
document.objects.insert(
h,
ObjectType::DynamicBlock(dynamic(
h,
graph,
"BLOCKLOOKUPACTION",
"AcDbBlockLookupAction",
DynamicBlockData::LookupAction(edited.clone()),
)),
);
h
};
let node_id = edited.action.element.eval.node_id;
graph::bind_ids(
document,
graph,
node_id,
&edited.columns.iter().map(|c| c.node_id).collect::<Vec<_>>(),
)
.map_err(str::to_string)?;
if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(&handle) {
o.data = DynamicBlockData::LookupAction(edited);
}
if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(&parameter) {
if let DynamicBlockData::LookupParameter(p) = &mut o.data {
p.index = node_id;
}
}
Ok(handle)
}
pub(crate) fn reframe(
document: &mut CadDocument,
definition: Handle,
transform: &Transform,
) -> usize {
let Some(graph) = graph_handle(document, definition) else {
return 0;
};
let owned = parameters(document, definition)
.iter()
.map(|(h, p)| (*h, p.parameter.property_count, p.index))
.collect::<Vec<_>>();
let mut changed = 0;
for (handle, grip_id, action_id) in owned {
if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(&handle) {
if let DynamicBlockData::LookupParameter(p) = &mut o.data {
p.parameter.definition_point = transform.apply(p.parameter.definition_point);
changed += 1;
}
}
for o in document.objects.values_mut() {
if let ObjectType::DynamicBlock(o) = o {
if o.owner != graph {
continue;
}
match &mut o.data {
DynamicBlockData::LookupGrip(g) if g.element.eval.node_id == grip_id => {
g.location = transform.apply(g.location);
changed += 1;
}
DynamicBlockData::LookupAction(a)
if action_id > 0 && a.action.element.eval.node_id == action_id =>
{
a.action.display_location = transform.apply(a.action.display_location);
changed += 1;
}
_ => {}
}
}
}
}
changed
}
+365
View File
@@ -0,0 +1,365 @@
use super::*;
use crate::scene::{block_basepoint, block_distance_parameter as distance, render_graph, Scene};
use codec::{
entities::{Insert, Line},
EntityType,
};
fn fixture() -> (CadDocument, Handle) {
let mut scene = Scene::new();
scene.define_block_raw(
"LOOKUP",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let definition = scene.document.block_records.get("LOOKUP").unwrap().handle;
(scene.document, definition)
}
fn add_distance(document: &mut CadDocument, definition: Handle) {
distance::create(
document,
definition,
distance::Kind::Linear,
Vector3::ZERO,
Vector3::new(2.0, 0.0, 0.0),
Vector3::UNIT_Y,
)
.unwrap();
}
fn draft(document: &CadDocument, definition: Handle, parameter: Handle) -> BlockLookupAction {
let p = parameters(document, definition)
.into_iter()
.find(|(h, _)| *h == parameter)
.unwrap()
.1;
let mut table = empty_table(p);
table
.columns
.insert(0, properties(document, definition)[0].column.clone());
table.column_count = 2;
table.row_count = 2;
table.expressions = vec!["2".into(), "Small".into(), "4".into(), "Large".into()];
table
}
fn graph_data(document: &CadDocument, definition: Handle) -> codec::objects::BlockEvaluationGraph {
let ObjectType::DynamicBlock(o) =
&document.objects[&graph_handle(document, definition).unwrap()]
else {
panic!()
};
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
panic!()
};
g.clone()
}
fn assert_links(document: &CadDocument, definition: Handle) {
let g = graph_data(document, definition);
for n in &g.nodes {
assert!(graph::valid_lists(&g, n.id), "invalid node {}", n.id);
}
for edge in g.edges.iter().filter(|e| e.next_id == 4) {
let inverse = g
.edges
.iter()
.find(|e| e.id == edge.outgoing_edges[4])
.unwrap();
assert_eq!(
(
inverse.source_node,
inverse.destination_node,
inverse.outgoing_edges[4]
),
(edge.destination_node, edge.source_node, edge.id)
);
}
}
fn roundtrip(document: &CadDocument, definition: Handle) {
let graph = graph_handle(document, definition).unwrap();
for extension in ["dwg", "dxf"] {
let bytes = crate::io::save_to_bytes(document, extension, document.version).unwrap();
let read = crate::io::load_bytes(&format!("lookup.{extension}"), bytes).unwrap();
for (h, o) in &document.objects {
if let ObjectType::DynamicBlock(o) = o {
if o.owner == graph || o.handle == graph {
let ObjectType::DynamicBlock(actual) = &read.objects[h] else {
panic!()
};
assert_eq!(actual.data, o.data, "{extension}: {h}");
assert_eq!(actual.owner, o.owner);
}
}
}
assert_links(&read, definition);
assert_eq!(
block_basepoint::parameter(&read, definition),
block_basepoint::parameter(document, definition)
);
}
}
#[test]
fn lookup_basepoint_native_roundtrip_rebinding_and_preservation() {
let (mut document, definition) = fixture();
add_distance(&mut document, definition);
let original = document.clone();
let base =
block_basepoint::create(&mut document, definition, Vector3::new(3.0, 4.0, 0.0)).unwrap();
let parameter = create(&mut document, definition, Vector3::UNIT_Y).unwrap();
let edited = draft(&document, definition, parameter);
let action = apply_table(&mut document, definition, parameter, edited).unwrap();
assert_eq!(document.entities().count(), original.entities().count());
assert_eq!(
document.block_records.get("LOOKUP"),
original.block_records.get("LOOKUP")
);
let gh = graph_handle(&document, definition).unwrap();
for (h, o) in &original.objects {
if *h != gh {
assert_eq!(document.objects.get(h), Some(o), "preserved {h}");
}
}
let first = graph_data(&document, definition);
assert_eq!(first.nodes.len(), 10); // distance four, base one, lookup four, action one
assert_eq!(first.edges.iter().filter(|e| e.next_id == 4).count(), 4);
assert_links(&document, definition);
let mut edited = table(&document, definition, parameter).unwrap().unwrap().1;
edited.expressions[1] = "Compact".into();
assert_eq!(
apply_table(&mut document, definition, parameter, edited.clone()).unwrap(),
action
);
assert_eq!(
graph_data(&document, definition),
first,
"row edits preserve graph IDs and links"
);
add_distance(&mut document, definition);
edited
.columns
.insert(1, properties(&document, definition)[1].column.clone());
edited.column_count = 3;
edited.expressions = vec![
"2".into(),
"2".into(),
"Compact".into(),
"4".into(),
"2".into(),
"Large".into(),
];
apply_table(&mut document, definition, parameter, edited.clone()).unwrap();
assert_links(&document, definition);
assert_eq!(
graph_data(&document, definition)
.edges
.iter()
.filter(|e| e.next_id == 4)
.count(),
6
);
edited.columns.remove(0);
edited.column_count = 2;
edited.expressions = vec!["2".into(), "Compact".into(), "4".into(), "Large".into()];
apply_table(&mut document, definition, parameter, edited).unwrap();
assert_links(&document, definition);
assert_eq!(
graph_data(&document, definition)
.edges
.iter()
.filter(|e| e.next_id == 4)
.count(),
4
);
assert_eq!(
block_basepoint::create(&mut document, definition, Vector3::UNIT_X).unwrap(),
base
);
roundtrip(&document, definition);
}
#[test]
fn lookup_audit_and_failed_binding_are_atomic() {
let (mut document, definition) = fixture();
add_distance(&mut document, definition);
let parameter = create(&mut document, definition, Vector3::ZERO).unwrap();
let good = draft(&document, definition, parameter);
for (cell, value) in [(0, "NaN"), (0, "inf"), (0, ""), (2, "2.0"), (3, "small")] {
let mut bad = good.clone();
bad.expressions[cell] = value.into();
let before = document.clone();
assert!(apply_table(&mut document, definition, parameter, bad).is_err());
assert_eq!(document, before);
}
let mut bad = good.clone();
bad.columns[0].node_id = i32::MAX;
let before = document.clone();
assert!(apply_table(&mut document, definition, parameter, bad).is_err());
assert_eq!(document, before);
let mut forward_only = good;
forward_only.columns[1].writable = false;
forward_only.expressions[3] = "Small".into();
assert!(validate(&forward_only, None).is_ok());
}
#[test]
fn lookup_malformed_dependencies_are_rejected_without_mutation() {
let (mut document, definition) = fixture();
add_distance(&mut document, definition);
let parameter = create(&mut document, definition, Vector3::ZERO).unwrap();
let edited = draft(&document, definition, parameter);
apply_table(&mut document, definition, parameter, edited).unwrap();
let mut edited = table(&document, definition, parameter).unwrap().unwrap().1;
edited.expressions[1] = "Changed".into();
let handle = graph_handle(&document, definition).unwrap();
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
panic!()
};
let DynamicBlockData::EvaluationGraph(g) = &mut object.data else {
panic!()
};
g.edges
.iter_mut()
.find(|e| e.next_id == 4)
.unwrap()
.outgoing_edges[4] = -1;
let before = document.clone();
assert!(apply_table(&mut document, definition, parameter, edited).is_err());
assert_eq!(document, before);
}
#[test]
fn lookup_existing_expressions_survive_row_reordering_and_output_edits() {
let (mut document, definition) = fixture();
add_distance(&mut document, definition);
let parameter = create(&mut document, definition, Vector3::ZERO).unwrap();
let mut original = draft(&document, definition, parameter);
original.expressions[0] = "[1,3]".into();
original.expressions[2] = "[4,6]".into();
let mut edited = original.clone();
edited.expressions.rotate_left(2);
edited.expressions[1] = "Renamed".into();
assert!(validate(&edited, Some(&original)).is_ok());
edited.expressions[0] = "[7,9]".into();
assert!(validate(&edited, Some(&original)).is_err());
}
#[test]
fn lookup_basepoint_anchor_transform_and_reframe() {
let (mut document, definition) = fixture();
let original = document.clone();
let point = Vector3::new(3.0, 4.0, 2.0);
let base = block_basepoint::create(&mut document, definition, point).unwrap();
let objects = document.objects.len();
assert_eq!(
block_basepoint::create(&mut document, definition, point).unwrap(),
base
);
assert_eq!(document.objects.len(), objects);
let mut insert = Insert::new("LOOKUP", Vector3::new(11.0, 13.0, 17.0));
insert.rotation = 1.2;
insert.set_x_scale(2.0);
insert.set_y_scale(3.0);
insert.set_z_scale(4.0);
assert!(
(render_graph::insert_transform(&document, &insert).apply(point) - insert.insert_point)
.length()
< 1e-9
);
assert_eq!(document.entities().count(), original.entities().count());
for original in original.entities() {
let EntityType::Line(before) = original else {
panic!()
};
let EntityType::Line(after) = document.get_entity(before.common.handle).unwrap() else {
panic!()
};
assert_eq!(
(after.start, after.end, after.normal, after.thickness),
(before.start, before.end, before.normal, before.thickness),
"native definition tags may be added, geometry stays intact"
);
}
add_distance(&mut document, definition);
let parameter = create(&mut document, definition, Vector3::UNIT_Y).unwrap();
let edited = draft(&document, definition, parameter);
apply_table(&mut document, definition, parameter, edited).unwrap();
let old = table(&document, definition, parameter).unwrap().unwrap().1;
let transform = Transform::from_translation(Vector3::new(10.0, 20.0, 0.0));
assert_eq!(
block_basepoint::reframe(&mut document, definition, &transform),
1
);
assert_eq!(reframe(&mut document, definition, &transform), 3);
let new = table(&document, definition, parameter).unwrap().unwrap().1;
assert_eq!(old.columns, new.columns);
assert_eq!(old.expressions, new.expressions);
assert_eq!(
new.action.display_location,
transform.apply(old.action.display_location)
);
assert_eq!(
block_basepoint::parameter(&document, definition)
.unwrap()
.1
.point,
transform.apply(point)
);
let before = document.clone();
assert!(
block_basepoint::create(&mut document, definition, Vector3::new(f64::NAN, 0.0, 0.0))
.is_err()
);
assert!(create(
&mut document,
definition,
Vector3::new(0.0, f64::INFINITY, 0.0)
)
.is_err());
assert_eq!(document, before);
}
#[test]
fn lookup_basepoint_existing_attributes_follow_anchor_and_editor_frame() {
use codec::entities::{AttributeDefinition, AttributeEntity};
let (document, definition) = fixture();
let mut scene = Scene::new();
scene.document = document;
let local = Vector3::new(7.0, 9.0, 0.0);
let mut attdef = AttributeDefinition::new("TAG".into(), "Prompt".into(), "Value".into());
attdef.insertion_point = local;
attdef.common.owner_handle = definition;
scene
.document
.add_entity(EntityType::AttributeDefinition(attdef))
.unwrap();
let mut insert = Insert::new("LOOKUP", Vector3::new(10.0, 20.0, 0.0)).with_scale(2.0, 3.0, 1.0);
insert.rotation = 0.5;
let mut attribute = AttributeEntity::simple("TAG", "Value");
attribute.insertion_point =
render_graph::insert_transform(&scene.document, &insert).apply(local);
insert.attributes.push(attribute);
let handle = scene
.document
.add_entity(EntityType::Insert(insert))
.unwrap();
block_basepoint::create(&mut scene.document, definition, Vector3::new(3.0, 4.0, 0.0)).unwrap();
let Some(EntityType::Insert(i)) = scene.document.get_entity(handle) else {
panic!()
};
assert!(
(i.attributes[0].insertion_point
- render_graph::insert_transform(&scene.document, i).apply(local))
.length()
< 1e-9
);
let transform = Transform::from_translation(Vector3::new(-1.0, -2.0, 0.0));
scene.reframe_block_definition(definition, &transform);
let Some(EntityType::Insert(i)) = scene.document.get_entity(handle) else {
panic!()
};
assert!(
(i.attributes[0].insertion_point
- render_graph::insert_transform(&scene.document, i).apply(transform.apply(local)))
.length()
< 1e-9
);
}
+1 -12
View File
@@ -499,18 +499,7 @@ impl Scene {
.map_or(1, |n| n + 1);
format!("*U{next}")
};
let base = self
.document
.block_records
.iter()
.find(|record| record.handle == source)
.and_then(
|record| match self.document.get_entity(record.block_entity_handle) {
Some(EntityType::Block(block)) => Some(block.base_point),
_ => None,
},
)
.unwrap_or(Vector3::ZERO);
let base = super::render_graph::block_base_point(&self.document, &reference.block_name);
let originals = members(&self.document, source);
let copies: Vec<EntityType> = originals
.iter()
+17
View File
@@ -1703,6 +1703,23 @@ impl Scene {
fold!(Some((point, point)));
}
}
let locations = super::block_basepoint::parameter(&self.document, block)
.map(|(_, p)| p.parameter.definition_point)
.into_iter()
.chain(
super::block_lookup::parameters(&self.document, block)
.into_iter()
.map(|(_, p)| p.parameter.definition_point),
)
.chain(
super::block_actions::actions(&self.document, block)
.into_iter()
.map(|(_, a, _, _)| a.display_location),
);
for point in locations {
let point = glam::DVec3::new(point.x, point.y, point.z);
fold!(Some((point, point)));
}
if let Some(parameter) = super::block_visibility::parameter(&self.document, block) {
let point = parameter.def_point;
let point = glam::DVec3::new(point.x, point.y, point.z);
+4
View File
@@ -11,6 +11,9 @@ pub(crate) mod block_metadata;
pub mod block_service;
pub mod block_visibility;
pub(crate) mod block_authoring;
pub(crate) mod block_basepoint;
pub(crate) mod block_lookup;
pub(crate) mod block_actions;
pub(crate) mod block_point_parameter;
pub(crate) mod block_distance_parameter;
pub(crate) mod block_orientation_parameter;
@@ -62,6 +65,7 @@ mod layout;
mod limits;
mod modify;
pub(crate) use modify::remap_copied_reactors;
pub(crate) use modify::inverse_affine;
mod mspace;
pub mod named_parameters;
mod page_setup;
+16 -3
View File
@@ -9,7 +9,9 @@ struct TextOrient {
x_scale: f64,
}
fn inverse_affine(transform: &codec::types::Transform) -> Option<codec::types::Transform> {
pub(crate) fn inverse_affine(
transform: &codec::types::Transform,
) -> Option<codec::types::Transform> {
use codec::types::{Matrix3, Matrix4, Transform, Vector3};
let matrix = &transform.matrix.m;
let linear = Matrix3::from_rows(
@@ -507,9 +509,14 @@ impl Scene {
}
}
let old_base = crate::scene::render_graph::block_base_point(&self.document, &block_name);
let basepoint_changes =
super::block_basepoint::reframe(&mut self.document, block_record, local_from_old);
// ATTRIBs live inline on each INSERT in that reference's owner space.
// Conjugate the block-local reframe through the INSERT transform:
// owner' = M · local_from_old · M⁻¹ · owner. M is the block-local →
// owner' = M_new · local_from_old · M_old⁻¹ · owner. A native
// Basepoint follows the frame too, so the two maps can differ.
// M is the block-local →
// world map the render path uses (`insert_transform`), because that is
// the map the attribute positions on the INSERT were placed through;
// the codec's base-ignoring transform would conjugate them through a
@@ -523,9 +530,11 @@ impl Scene {
&& !reference.attributes.is_empty()
&& !owned.contains(&reference.common.handle) =>
{
let old_insertion =
crate::scene::render_graph::insert_transform_with_base(reference, old_base);
let insertion =
crate::scene::render_graph::insert_transform(&self.document, reference);
let inverse = inverse_affine(&insertion)?;
let inverse = inverse_affine(&old_insertion)?;
Some((
reference.common.handle,
codec::types::Transform::from_matrix(
@@ -571,6 +580,10 @@ impl Scene {
block_record,
local_from_old,
);
let parameter_changes = parameter_changes
+ basepoint_changes
+ super::block_lookup::reframe(&mut self.document, block_record, local_from_old)
+ super::block_actions::reframe(&mut self.document, block_record, local_from_old);
if !changed.is_empty() || parameter_changes > 0 {
self.rebuild_derived_caches();
}
+4
View File
@@ -621,6 +621,10 @@ pub fn block_base_point(document: &CadDocument, block_name: &str) -> Vector3 {
.iter()
.find(|record| record.name.eq_ignore_ascii_case(block_name))
.map(|record| {
if let Some((_, parameter)) = super::block_basepoint::parameter(document, record.handle)
{
return parameter.point;
}
document
.get_entity(record.block_entity_handle)
.and_then(|entity| match entity {
+31
View File
@@ -1023,6 +1023,8 @@ pub(crate) fn selection_overlay<'a>(
pub(crate) struct AuthoringDimension {
pub lines: Vec<[Point; 2]>,
pub round_grips: Vec<Point>,
pub base_grips: Vec<Point>,
pub lookup_grips: Vec<Point>,
pub dimensions: Vec<[Point; 2]>,
pub labels: Vec<(Point, String)>,
pub grips: Vec<(Point, Option<Point>)>,
@@ -2414,6 +2416,35 @@ impl canvas::Program<Message> for SelectionCanvas {
..Default::default()
});
}
for point in &dimension.base_grips {
let stroke = canvas::Stroke::default().with_color(color);
frame.stroke(&canvas::Path::circle(*point, 6.0), stroke);
frame.stroke(
&canvas::Path::line(
Point::new(point.x - 9.0, point.y),
Point::new(point.x + 9.0, point.y),
),
stroke,
);
frame.stroke(
&canvas::Path::line(
Point::new(point.x, point.y - 9.0),
Point::new(point.x, point.y + 9.0),
),
stroke,
);
}
for point in &dimension.lookup_grips {
frame.fill(
&canvas::Path::new(|p| {
p.move_to(Point::new(point.x - 5.0, point.y - 4.0));
p.line_to(Point::new(point.x + 5.0, point.y - 4.0));
p.line_to(Point::new(point.x, point.y + 5.0));
p.close();
}),
color,
);
}
for point in &dimension.round_grips {
frame.fill(&canvas::Path::circle(*point, 3.5), color);
}
+34 -8
View File
@@ -193,6 +193,9 @@ fn catalog(
visibility_error: Option<&'static str>,
point_error: Option<&'static str>,
flip_error: Option<&'static str>,
lookup_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
) -> Element<'static, Message> {
let (entries, unavailable) = match tab {
Tab::Parameters => (
@@ -212,7 +215,11 @@ fn catalog(
let distance = tab == Tab::Parameters && matches!(*label, "Linear" | "Polar" | "XY");
let orientation = tab == Tab::Parameters && matches!(*label,"Rotation"|"Alignment"|"Flip");
let orientation_error = if *label=="Flip" {flip_error} else {point_error};
let enabled = (orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint");
let lookup_action=tab==Tab::Actions&&*label=="Lookup";
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale");
let action_error=if *label=="Scale" {scale_error}else{move_error};
let enabled = (geometry_action&&action_error.is_none())||(single&&point_error.is_none())||(lookup_action&&lookup_error.is_none())||(orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let base = if enabled {
icons::semantic(icon, 28.0)
} else {
@@ -221,10 +228,11 @@ fn catalog(
let icon = if tab != Tab::Parameters && *label != "Block Properties Table" {
stack![
base,
container(icons::semantic_disabled(
include_bytes!("../../../assets/icons/ui/bolt.svg"),
14.0
))
container(if enabled {
icons::semantic(include_bytes!("../../../assets/icons/ui/bolt.svg"), 14.0)
} else {
icons::semantic_disabled(include_bytes!("../../../assets/icons/ui/bolt.svg"), 14.0)
})
.width(28)
.height(28)
.align_right(Fill)
@@ -235,8 +243,8 @@ fn catalog(
base
};
let message = enabled.then(|| Message::RibbonToolClick {
tool_id: format!("BPARAMETER_{}", label.to_ascii_uppercase()),
event: crate::modules::ModuleEvent::Command(format!("BPARAMETER {}", label.to_ascii_uppercase())),
tool_id: if geometry_action {format!("BACTION_{}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else{format!("BPARAMETER_{}", label.to_ascii_uppercase())},
event: crate::modules::ModuleEvent::Command(if geometry_action {format!("BACTION {}",label.to_ascii_uppercase())}else if lookup_action {"BLOOKUPTABLE".into()}else{format!("BPARAMETER {}", label.to_ascii_uppercase())}),
});
let description = if visibility {
crate::t!(visibility_error
@@ -253,6 +261,12 @@ fn catalog(
"Alignment" => "Place the alignment base point and tangent direction.",
_ => "Place a reflection line and its flip-state label. Associate a Flip action to mirror geometry.",
})).into_owned()
} else if single {
crate::t!(point_error.unwrap_or(if *label=="Basepoint" {"Place or relocate the dynamic block insertion anchor."}else{"Place a Lookup parameter, then use the Lookup action to edit its property table."})).into_owned()
} else if geometry_action {
crate::t!(action_error.unwrap_or("Select a parameter endpoint, select the objects it controls, then place the action marker. Test it using the parameter grip in Test Block.")).into_owned()
} else if lookup_action {
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
} else {
unavailable.to_string()
};
@@ -265,6 +279,12 @@ fn catalog(
container(entry).id(match *label { "Linear" => "authoring-tool-linear", "Polar" => "authoring-tool-polar", _ => "authoring-tool-xy" }).into()
} else if orientation {
container(entry).id(match *label {"Rotation"=>"authoring-tool-rotation","Alignment"=>"authoring-tool-alignment",_=>"authoring-tool-flip"}).into()
} else if single {
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
} else if geometry_action {
container(entry).id(if *label=="Move" {"authoring-tool-move-action"}else{"authoring-tool-scale-action"}).into()
} else if lookup_action {
container(entry).id("authoring-tool-lookup-table").into()
} else {
entry
}
@@ -378,6 +398,9 @@ pub fn view(
visibility_error: Option<&'static str>,
point_error: Option<&'static str>,
flip_error: Option<&'static str>,
lookup_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
) -> Element<'static, Message> {
let tabs = column(Tab::ALL.into_iter().map(|category| {
let label = category.label();
@@ -431,7 +454,10 @@ pub fn view(
selected_tab,
visibility_error,
point_error,
flip_error
flip_error,
lookup_error,
move_error,
scale_error
))
.width(Fill)
.height(Fill),
+319
View File
@@ -0,0 +1,319 @@
use crate::app::Message;
use crate::scene::block_lookup::{self, Property};
use codec::{
objects::{BlockLookupAction, BlockLookupParameter},
Handle,
};
use iced::{
widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
},
Element, Fill,
};
#[derive(Debug, Clone)]
pub enum Action {
Select(usize),
Property(usize),
AddProperty,
RemoveProperty(usize),
Cell(usize, usize, String),
AddRow,
DeleteRow(usize),
MoveRow(usize, bool),
Reverse(usize, bool),
Audit,
Apply,
}
pub(crate) struct Draft {
pub parameter: Handle,
pub original_parameter: BlockLookupParameter,
pub original: Option<(Handle, BlockLookupAction)>,
pub edited: BlockLookupAction,
}
impl Draft {
pub(crate) fn changed(&self) -> bool {
match &self.original {
Some((_, original)) => original != &self.edited,
None => self.edited != block_lookup::empty_table(&self.original_parameter),
}
}
}
pub(crate) struct State {
pub document: u64,
pub definition: Handle,
pub epoch: u64,
pub drafts: Vec<Draft>,
pub selected: usize,
pub properties: Vec<Property>,
pub selected_property: Option<usize>,
pub error: Option<String>,
pub notice: Option<String>,
}
impl State {
pub(crate) fn edit(&mut self, action: Action) {
self.error = None;
self.notice = None;
match action {
Action::Select(index) if index < self.drafts.len() => {
self.selected = index;
self.selected_property = None;
return;
}
Action::Property(index) if index < self.properties.len() => {
self.selected_property = Some(index);
return;
}
_ => {}
}
let Some(draft) = self.drafts.get_mut(self.selected) else {
return;
};
let table = &mut draft.edited;
let cols = table.columns.len();
let rows = table.row_count.max(0) as usize;
match action {
Action::AddProperty => {
let Some(property) = self.selected_property.and_then(|i| self.properties.get(i))
else {
return;
};
if cols >= 64
|| table.columns.iter().any(|c| {
c.node_id == property.column.node_id
&& c.connection_name == property.column.connection_name
})
{
return;
}
let index = table
.columns
.iter()
.position(|c| c.lookup_property)
.unwrap_or(cols);
for row in (0..rows).rev() {
table
.expressions
.insert(row * cols + index, property.value.clone());
}
table.columns.insert(index, property.column.clone());
table.column_count += 1;
self.selected_property = None;
}
Action::RemoveProperty(index)
if table.columns.get(index).is_some_and(|c| !c.lookup_property) =>
{
for row in (0..rows).rev() {
table.expressions.remove(row * cols + index);
}
table.columns.remove(index);
table.column_count -= 1;
}
Action::Cell(row, col, value) if row < rows && col < cols => {
table.expressions[row * cols + col] = value;
}
Action::AddRow if rows < 1000 => {
for column in &table.columns {
let value = if column.lookup_property {
format!("Value{}", rows + 1)
} else {
self.properties
.iter()
.find(|p| {
p.column.node_id == column.node_id
&& p.column.connection_name == column.connection_name
})
.map(|p| p.value.clone())
.unwrap_or_default()
};
table.expressions.push(value);
}
table.row_count += 1;
}
Action::DeleteRow(row) if row < rows => {
table.expressions.drain(row * cols..(row + 1) * cols);
table.row_count -= 1;
}
Action::MoveRow(row, up) if row < rows => {
let next = if up {
row.checked_sub(1)
} else {
(row + 1 < rows).then_some(row + 1)
};
if let Some(next) = next {
for col in 0..cols {
table.expressions.swap(row * cols + col, next * cols + col);
}
}
}
Action::Reverse(index, value)
if table.columns.get(index).is_some_and(|c| c.lookup_property) =>
{
table.columns[index].writable = value;
}
Action::Audit => {
match block_lookup::validate(table, draft.original.as_ref().map(|(_, t)| t)) {
Ok(()) => self.notice = Some(crate::t!("Table audit passed.").into_owned()),
Err(error) => self.error = Some(crate::t!(error.as_str()).into_owned()),
}
}
_ => {}
}
}
}
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let send = |a| Message::BlockLookup(a);
let draft = &state.drafts[state.selected];
let table = &draft.edited;
let cols = table.columns.len();
let rows = table.row_count.max(0) as usize;
let mut headings = row![text(" ").width(28)].spacing(6);
for (index, column) in table.columns.iter().enumerate() {
let name = if column.lookup_property {
state
.drafts
.iter()
.find(|d| {
d.original_parameter
.parameter
.parameter
.element
.eval
.node_id
== column.node_id
})
.map(|d| d.original_parameter.lookup_name.clone())
.unwrap_or_else(|| crate::t!("Lookup value").into_owned())
} else {
state
.properties
.iter()
.find(|p| {
p.column.node_id == column.node_id
&& p.column.connection_name == column.connection_name
})
.map(|p| p.label.clone())
.unwrap_or_else(|| crate::t!("Input property").into_owned())
};
let mut header = column![text(name).size(13)];
if column.lookup_property {
header = header.push(
row![
checkbox(column.writable).on_toggle(move |v| send(Action::Reverse(index, v))),
text(crate::t!("Reverse lookup").into_owned()).size(11)
]
.spacing(4),
);
} else {
header = header.push(
button(text(crate::t!("Remove").into_owned()).size(11))
.on_press(send(Action::RemoveProperty(index))),
);
}
headings = headings.push(container(header.spacing(4)).width(160));
}
let mut grid = column![headings].spacing(7);
for row_index in 0..rows {
let mut cells = row![text((row_index + 1).to_string()).width(28)].spacing(6);
for col_index in 0..cols {
cells = cells.push(
text_input("", &table.expressions[row_index * cols + col_index])
.on_input(move |v| send(Action::Cell(row_index, col_index, v)))
.width(160)
.style(crate::ui::style::form::field_style),
);
}
cells =
cells
.push(button(text("↑")).on_press_maybe(
(row_index > 0).then_some(send(Action::MoveRow(row_index, true))),
))
.push(button(text("↓")).on_press_maybe(
(row_index + 1 < rows).then_some(send(Action::MoveRow(row_index, false))),
))
.push(
button(text(crate::t!("Delete").into_owned()))
.on_press(send(Action::DeleteRow(row_index))),
);
grid = grid.push(cells);
}
let available = state
.properties
.iter()
.enumerate()
.filter(|(_, p)| {
!table.columns.iter().any(|c| {
c.node_id == p.column.node_id && c.connection_name == p.column.connection_name
})
})
.map(|(i, _)| i)
.collect::<Vec<_>>();
container(
column![
row![
text(crate::t!("Lookup parameter").into_owned()),
pick_list(
Some(state.selected),
(0..state.drafts.len()).collect::<Vec<_>>(),
|i: &usize| state.drafts[*i].original_parameter.lookup_name.clone()
)
.on_select(move |i| send(Action::Select(i)))
.width(Fill)
]
.spacing(10),
row![
pick_list(state.selected_property, available, |i: &usize| state
.properties[*i]
.label
.clone())
.on_select(move |i| send(Action::Property(i)))
.placeholder(crate::t!("Choose input property").into_owned())
.width(Fill),
button(text(crate::t!("Add Property").into_owned()))
.on_press_maybe(state.selected_property.map(|_| send(Action::AddProperty)))
]
.spacing(8),
text(
crate::t!(
"Add Linear or Polar distance inputs. Existing table columns are retained."
)
.into_owned()
)
.size(12),
scrollable(grid)
.direction(scrollable::Direction::Both {
vertical: Default::default(),
horizontal: Default::default()
})
.height(Fill)
.width(Fill),
row![
button(text(crate::t!("Add Row").into_owned()))
.on_press_maybe((rows < 1000).then_some(send(Action::AddRow))),
button(text(crate::t!("Audit").into_owned())).on_press(send(Action::Audit)),
Space::new().width(Fill)
]
.spacing(8),
text(state.error.clone().unwrap_or_default())
.size(12)
.style(text::danger),
text(state.notice.clone().unwrap_or_default()).size(12),
row![
Space::new().width(Fill),
button(text(crate::t!("OK").into_owned()))
.on_press(send(Action::Apply))
.style(button::primary),
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal)
]
.spacing(8),
]
.spacing(12),
)
.padding(16)
.width(sizing.width)
.height(sizing.height)
.into()
}
+1
View File
@@ -11,6 +11,7 @@ pub mod block_edit_picker;
pub mod block_palette;
pub mod block_save_as;
pub mod block_visibility;
pub mod block_lookup;
pub mod browser;
pub mod count_palette;
pub mod drafting_settings;