Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion assets/icons/ui/resize_left.svg

This file was deleted.

19 changes: 18 additions & 1 deletion src/app/block_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ impl OpenCADStudio {
};
let source_name = session.block_name.clone();
let source_handle = session.br_handle;
let fresh = !self.block_palette_stale();
let name = match self.validate_block_save_as_name(i, raw) {
Ok(name) => name,
Err(error) => {
Expand Down Expand Up @@ -554,7 +555,12 @@ impl OpenCADStudio {
}
let parameters = self.tabs[i].scene.named_parameters.clone();
let active_layer = self.tabs[i].active_layer.clone();
self.tabs[i].scene.document = copied.document;
let before = std::mem::replace(&mut self.tabs[i].scene.document, copied.document);
let touched = crate::app::update::blocks_palette::changed_block_names(
&before,
&self.tabs[i].scene.document,
);
drop(before);
self.tabs[i].scene.parametric_constraints = constraints;
self.tabs[i].scene.named_parameters = parameters;
self.tabs[i].active_layer = active_layer;
Expand Down Expand Up @@ -585,6 +591,7 @@ impl OpenCADStudio {
self.tabs[i].block_edit_history = Some(history);
self.tabs[i].scene.bump_geometry();
self.tabs[i].refresh_active_ucs();
self.note_block_changes(fresh, &touched);
self.refresh_block_palette();
self.refresh_properties();
self.sync_ribbon_layers();
Expand Down Expand Up @@ -643,6 +650,10 @@ impl OpenCADStudio {
return false;
};
let name = session.block_name.clone();
// The palette keeps the other blocks' previews when it was up to
// date before the save.
let fresh = !self.block_palette_stale();
let mut touched = vec![name.clone()];
if let Err(error) = crate::scene::block_service::validate_block_name(&name) {
// Existing anonymous definitions are editable; name validation
// applies to named definitions rather than generated variants.
Expand Down Expand Up @@ -694,6 +705,11 @@ impl OpenCADStudio {
Ok(document) => {
self.apply_action_document(i, document);
self.coalesce_saved_representations(i, &before_sync);
// A dynamic block's references are drawn again too.
touched.extend(crate::app::update::blocks_palette::changed_block_names(
&before_sync,
&self.tabs[i].scene.document,
));
}
Err(error) => {
self.command_line.push_error(crate::t!(error).as_ref());
Expand All @@ -711,6 +727,7 @@ impl OpenCADStudio {
history.checkpoint = self.block_edit_checkpoint(i);
self.tabs[i].block_edit_history = Some(history);
self.tabs[i].scene.bump_geometry();
self.note_block_changes(fresh, &touched);
}
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
session.unpersisted_pre_document = None;
Expand Down
27 changes: 27 additions & 0 deletions src/app/block_editor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ fn edit_action_set(app: &mut OpenCADStudio, action: Handle, mode: &str, targets:
assert!(app.tabs[app.active_tab].active_cmd.is_none());
}

#[test]
fn block_editor_save_redraws_only_the_saved_blocks_palette_preview() {
let (mut app, reference) = fixture();
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"OTHER",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_Y,
))],
);
app.show_block_palette = true;
let preview = |app: &OpenCADStudio, name: &str| {
std::rc::Rc::clone(&app.block_palette.current_entry(name).unwrap().preview)
};
begin(&mut app, reference);
circle(&mut app, 3.);
// Up to date right before the save, as after any handled message.
app.refresh_block_palette_if_stale();
let (edited, other) = (preview(&app, "EDIT_ME"), preview(&app, "OTHER"));
let _ = app.dispatch_command("BSAVE");
app.refresh_block_palette_if_stale();
assert!(!std::rc::Rc::ptr_eq(&preview(&app, "EDIT_ME"), &edited));
assert!(std::rc::Rc::ptr_eq(&preview(&app, "OTHER"), &other));
}

#[test]
fn block_constraint_display_variable_is_transient_validated_and_resets_on_entry() {
let (mut app, reference) = fixture();
Expand Down
3 changes: 3 additions & 0 deletions src/app/block_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ impl OpenCADStudio {
if current == value {
return Task::none();
}
let fresh = !self.block_palette_stale();
self.push_undo_snapshot(i, "Block properties");
let doc = &mut self.tabs[i].scene.document;
match field {
Expand Down Expand Up @@ -231,6 +232,8 @@ impl OpenCADStudio {
self.tabs[i].scene.bump_geometry();
self.finish_pending_history(i);
self.refresh_properties();
// Only this block's definition changed.
self.note_block_changes(fresh, [&name]);
self.refresh_block_palette();
Task::none()
}
Expand Down
3 changes: 3 additions & 0 deletions src/app/command_driver/entity_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,11 @@ impl OpenCADStudio {
.block_records
.get(&name)
.is_some();
let fresh = !self.block_palette_stale();
match self.tabs[i].scene.create_block_with_options(*options) {
Ok(insert_handle) => {
// Only this block's definition changed.
self.note_block_changes(fresh, [&name]);
self.tabs[i].dirty = true;
self.tabs[i].scene.deselect_all();
if !insert_handle.is_null() {
Expand Down
209 changes: 209 additions & 0 deletions src/app/commands/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,215 @@ mod tests {
assert!(app.block_palette.blocks.iter().any(|b| b.name == "Widget"));
}

#[test]
fn blockpalette_click_on_a_drawing_block_keeps_the_previews() {
let mut app = fresh_app();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
let mut line = codec::entities::Line::new();
line.end = codec::types::Vector3::new(10.0, 0.0, 0.0);
app.tabs[i]
.scene
.define_block_from_owned_entities(
vec![codec::EntityType::Line(line)],
"Widget",
glam::DVec3::ZERO,
)
.unwrap();
app.refresh_block_palette();
let before = std::rc::Rc::clone(&app.block_palette.blocks[0].preview);
let _ = app.palette_insert(
crate::ui::window::block_palette::Item::Current("Widget".into()),
None,
);
assert_eq!(app.block_palette.placing.as_deref(), Some("Widget"));
// No rebuild: the same preview (and its drawn thumbnail) stays.
assert!(std::rc::Rc::ptr_eq(
&app.block_palette.blocks[0].preview,
&before
));
}

#[test]
fn blockpalette_refresh_keeps_previews_of_blocks_a_change_did_not_touch() {
use std::rc::Rc;
let mut app = fresh_app();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
let line = |x: f64| {
let mut line = codec::entities::Line::new();
line.end = codec::types::Vector3::new(x, 0.0, 0.0);
codec::EntityType::Line(line)
};
let insert = |name: &str| {
codec::EntityType::Insert(codec::entities::Insert::new(
name.to_string(),
codec::types::Vector3::ZERO,
))
};
let scene = &mut app.tabs[i].scene;
scene.define_block_raw("Part", codec::types::Vector3::ZERO, vec![line(1.0)]);
scene.define_block_raw("Holder", codec::types::Vector3::ZERO, vec![insert("Part")]);
scene.define_block_raw("Outer", codec::types::Vector3::ZERO, vec![insert("Holder")]);
scene.define_block_raw("Other", codec::types::Vector3::ZERO, vec![line(2.0)]);
app.refresh_block_palette();
let preview = |app: &crate::app::OpenCADStudio, name: &str| {
Rc::clone(&app.block_palette.current_entry(name).unwrap().preview)
};
// A preview is worked out once shown, and the one kept for Recent /
// Favorites is the same, not a copy.
let kept = |app: &crate::app::OpenCADStudio, name: &str| {
let key = format!("|{}", name.to_ascii_uppercase());
app.block_palette
.ref_wires
.iter()
.find(|(k, _)| k.ends_with(&key))
.map(|(_, p)| Rc::clone(p))
.expect("kept preview")
};
assert!(preview(&app, "Part").wires().is_none());
app.ensure_block_previews();
assert!(preview(&app, "Part").wires().is_some_and(|w| !w.is_empty()));
assert!(Rc::ptr_eq(&kept(&app, "Part"), &preview(&app, "Part")));
let before: Vec<_> = ["Part", "Holder", "Outer", "Other"]
.iter()
.map(|n| preview(&app, n))
.collect();
// Redefining Part redraws it and the blocks holding it, directly or
// nested; Other keeps its preview.
let fresh = !app.block_palette_stale();
app.tabs[i]
.scene
.redefine_block_raw("Part", codec::types::Vector3::ZERO, vec![line(5.0)])
.unwrap();
app.note_block_changes(fresh, ["Part"]);
app.refresh_block_palette();
let after: Vec<_> = ["Part", "Holder", "Outer", "Other"]
.iter()
.map(|n| preview(&app, n))
.collect();
for k in 0..3 {
assert!(!Rc::ptr_eq(&before[k], &after[k]), "{k}");
}
assert!(Rc::ptr_eq(&before[3], &after[3]));
assert!(Rc::ptr_eq(&kept(&app, "Part"), &after[0]));
// A change made while the palette was out of date rebuilds all.
app.note_block_changes(false, ["Part"]);
app.refresh_block_palette();
assert!(!Rc::ptr_eq(&preview(&app, "Other"), &after[3]));
// Without a hint every preview is rebuilt, as before.
let other = preview(&app, "Other");
app.refresh_block_palette();
assert!(!Rc::ptr_eq(&preview(&app, "Other"), &other));
// A hint only lives for the message that made it.
let fresh = !app.block_palette_stale();
app.note_block_changes(fresh, ["Part"]);
let _ = app.update(crate::app::Message::Dock(
crate::ui::dock::DockMsg::HoverExit,
));
assert_eq!(
app.block_palette.change_hint,
crate::ui::window::block_palette::ChangeHint::Unknown
);
}

#[test]
fn blockpalette_works_out_only_the_previews_it_shows() {
use crate::ui::window::block_palette::{BlockPaletteMsg, ViewMode};
let mut app = fresh_app();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
for k in 0..300 {
let mut line = codec::entities::Line::new();
line.end = codec::types::Vector3::new(1.0 + k as f64, 0.0, 0.0);
app.tabs[i].scene.define_block_raw(
&format!("B{k:03}"),
codec::types::Vector3::ZERO,
vec![codec::EntityType::Line(line)],
);
}
app.show_block_palette = true;
app.block_palette.view = ViewMode::Large;
let built = |app: &crate::app::OpenCADStudio| {
app.block_palette
.blocks
.iter()
.filter(|b| b.preview.wires().is_some())
.map(|b| b.name.clone())
.collect::<Vec<_>>()
};
// Before the list reports its viewport: about a screenful.
let _ = app.update(crate::app::Message::BlockPalette(
BlockPaletteMsg::Scrolled {
offset: 0.0,
height: 0.0,
},
));
let first = built(&app);
assert!(first.contains(&"B000".to_string()));
assert!(first.len() < 100, "{}", first.len());
// Scrolled far down: those rows are worked out too, the rest not.
let _ = app.update(crate::app::Message::BlockPalette(
BlockPaletteMsg::Scrolled {
offset: 5000.0,
height: 400.0,
},
));
let now = built(&app);
assert!(now.len() > first.len() && now.len() < 150, "{}", now.len());
assert!(!now.contains(&"B299".to_string()));
// The List view shows no thumbnails and works out none.
app.refresh_block_palette();
app.block_palette.view = ViewMode::List;
let _ = app.update(crate::app::Message::BlockPalette(BlockPaletteMsg::Refresh));
assert!(built(&app).is_empty());
// Placing a block works out its preview for the drag.
let _ = app.palette_insert(
crate::ui::window::block_palette::Item::Current("B299".into()),
None,
);
assert_eq!(built(&app), vec!["B299".to_string()]);
}

#[test]
fn blockpalette_library_thumbnails_arrive_in_the_background() {
use crate::ui::window::block_palette::BlockPaletteMsg;
let dir = std::env::temp_dir().join(format!(
"ocs_palette_thumbs_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let drawing = dir.join("part.dwg");
std::fs::write(&drawing, b"not a drawing").unwrap();
let mut app = fresh_app();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.update(crate::app::Message::BlockPalette(
BlockPaletteMsg::LibrarySelect(dir.display().to_string()),
));
// Listed at once, without its thumbnail: that is read in the
// background once the message is handled.
let entry = |app: &crate::app::OpenCADStudio| {
app.block_palette
.library_entries
.iter()
.find(|e| e.path == drawing)
.map(|e| e.thumb.is_some())
};
assert_eq!(entry(&app), Some(false));
assert!(app.block_palette.thumb_requests.is_empty());
// The read thumbnail lands on its entry.
let handle = iced::widget::image::Handle::from_rgba(1, 1, vec![0; 4]);
let _ = app.update(crate::app::Message::BlockPalette(
BlockPaletteMsg::Thumbnails(vec![(drawing.clone(), Some(handle))]),
));
assert_eq!(entry(&app), Some(true));
std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn xref_dash_alias_lists_like_xref() {
// `-XREF` asks on the command line: `?` then Enter prints the
Expand Down
Loading
Loading