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