feat(xref): detach, unload, reload, bind, overlay, and path operations

Detach, unload, reload preserving host layer overrides, bind, overlay, and saved-path editing, with per-tab session state and a read-only web list.
This commit is contained in:
Karim Jerbi committed 2026-09-15 23:56:31 +01:00
1 parent 42219a4b8c
commit d0d946a568
16 files changed
+4856 -499

No files matched your search

+9 -5
View File
@@ -2301,7 +2301,7 @@ impl OpenCADStudio {
self.sync_dyn_fields();
self.refresh_area_preview(i);
}
CmdResult::CommitAndExit(entity) => {
CmdResult::CommitAndExit(mut entity) => {
// For XATTACH: ensure the xref block definition exists before
// committing the INSERT entity that references it.
// Extract path early to avoid borrow conflicts.
@@ -2332,9 +2332,7 @@ impl OpenCADStudio {
&path,
host_base.as_deref(),
) {
self.command_line.push_error(
"XATTACH: cannot attach the host drawing into itself.",
);
self.command_line.push_error(crate::t!("XATTACH: cannot attach the host drawing into itself.").as_ref());
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
@@ -2342,11 +2340,17 @@ impl OpenCADStudio {
return Task::none();
}
}
crate::modules::insert::xattach::prepare_xref_block(
let prepared_name = crate::modules::insert::xattach::prepare_xref_block(
&mut self.tabs[i].scene,
&path,
host_base.as_deref(),
);
// `prepare_xref_block` may suffix a colliding stem. The
// command was created before that collision was known, so
// retarget its pending INSERT to the actual new block.
if let acadrust::EntityType::Insert(insert) = &mut entity {
insert.block_name = prepared_name;
}
// Resolving the xref merged its layer / linetype tables
// into the document — mirror them into the Layers panel
// and ribbon dropdowns now, not on the next reopen (#407).
File diff suppressed because it is too large. Load diff
+1
View File
@@ -713,6 +713,7 @@ inventory::submit!(crate::command::CommandRegistration {
"XDATA",
"XR",
"XREF",
"-XREF",
"XREFMAN",
"XRELOAD",
"ZOOM",
+13
View File
@@ -195,6 +195,16 @@ pub(super) struct DocumentTab {
pub(super) active_mleader_style: String,
/// Last camera_generation value written back to the document.
pub(super) last_synced_camera_gen: u64,
/// Session set of unloaded reference keys (Task 8b). Owns the set that
/// `collect_entries` takes as `unloaded`, so CLI and palette agree.
pub(super) xref_unloaded: crate::io::xref_model::UnloadSet,
/// Load-time mtimes per reference key (Task 8b). Written on every
/// palette refresh; `Stale` is detectable from the second refresh on.
pub(super) xref_stat_cache: crate::io::xref_model::RefStatCache,
/// NotFound count from the last file-open xref resolution (Task 8b).
/// The palette renders a neutral "open XREFMAN" notice while non-zero;
/// never auto-opens a modal. Cleared by a clean palette refresh.
pub(super) xref_missing: usize,
/// Sentinel "Welcome / Start" tab. Always at index 0 when present.
/// Cannot be closed; the viewport area renders a welcome page instead
/// of the model-space shader. The scene is still constructed so the
@@ -615,6 +625,9 @@ impl DocumentTab {
active_block_edit: None,
active_mleader_style: "Standard".to_string(),
last_synced_camera_gen: 0,
xref_unloaded: crate::io::xref_model::UnloadSet::default(),
xref_stat_cache: crate::io::xref_model::RefStatCache::default(),
xref_missing: 0,
is_start: false,
pan_mode: false,
orbit_mode: false,
+11
View File
@@ -2357,6 +2357,17 @@ pub enum Message {
XrefManagerToggleTree,
/// Expand/collapse one tree parent (block-record handle key).
XrefManagerToggleExpand(u64),
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
/// Details-pane "new path" draft text.
XrefManagerPathInput(String),
/// Apply the "new path" draft to the anchor entry.
XrefManagerPathApply,
/// Find & Replace row drafts.
XrefManagerFindInput(String),
XrefManagerReplaceInput(String),
/// Apply Find & Replace across all direct references.
XrefManagerFindReplaceApply,
LayerToggleVisible(usize),
LayerToggleLock(usize),
LayerToggleFreeze(usize),
+608 -5
View File
@@ -759,7 +759,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
/// Rebuild the Reference Manager's entry list from the active drawing.
/// Display-only: `collect_entries` stats files but mutates nothing.
/// Uses the tab's session sets (unloaded + stat baseline) so CLI and
/// palette agree; writes fresh load-time mtimes back into the tab's stat
/// cache (`Stale` detectable from the second refresh on). Records the
/// tab's `edit_revision` so the palette auto-rescans on doc changes.
pub(crate) fn refresh_xref_manager(&mut self) {
let i = self.active_tab;
let base_dir: std::path::PathBuf = self.tabs[i]
@@ -768,21 +771,466 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
let tab_id = self.tabs[i].id;
self.xref_manager
.refresh(&self.tabs[i].scene.document, &base_dir);
let revision = self.tabs[i].edit_revision;
let baselines = {
let tab = &self.tabs[i];
self.xref_manager.refresh(
&tab.scene.document,
&base_dir,
tab.xref_unloaded.as_set(),
&tab.xref_stat_cache.0,
)
};
let tab = &mut self.tabs[i];
for (key, mtime) in baselines {
tab.xref_stat_cache.insert(key, mtime);
}
// A clean rescan clears the open-time missing notice.
if !self
.xref_manager
.entries
.iter()
.any(|e| e.status == crate::io::xref_model::RefStatus::NotFound)
{
tab.xref_missing = 0;
}
self.xref_manager.source_tab_id = Some(tab_id);
self.xref_manager.source_edit_revision = revision;
}
/// Re-scan when the palette is open but showing another tab's drawing.
/// Re-scan when the palette is open but showing another tab's drawing or
/// the active drawing changed since the last scan (CLI/palette mutation,
/// XOPEN-return edit, undo/redo — all bump `edit_revision`). Cheap:
/// `collect_entries` stats files without re-parsing the host.
pub(crate) fn refresh_xref_manager_if_stale(&mut self) {
if self.active_modal != Some(crate::app::ModalKind::XrefManager) {
return;
}
let i = self.active_tab;
if self.xref_manager.source_tab_id != Some(self.tabs[i].id) {
if self.xref_manager.source_tab_id != Some(self.tabs[i].id)
|| self.xref_manager.source_edit_revision != self.tabs[i].edit_revision
{
self.refresh_xref_manager();
}
}
/// Shared post-mutation sequence for reference ops (CLI + palette):
/// repopulate scene caches, mirror xref layers into the Layers panel,
/// and mark the tab dirty. Both call sites use it so the 5-line
/// sequence cannot drift.
pub(crate) fn post_ref_op(&mut self, i: usize) {
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
// Image/PDF unload is intentionally session-only (the file formats do
// not persist an unloaded bit), so keep their source entities but
// remove their decoded render models. A reload repopulates the cache
// after clearing the corresponding session key.
let hidden_images: Vec<acadrust::types::Handle> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| match entity {
acadrust::EntityType::RasterImage(image)
if image.definition_handle.is_some_and(|key| self.tabs[i].xref_unloaded.is_unloaded(key.value())) =>
{
Some(entity.common().handle)
}
acadrust::EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf)
&& self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
{
Some(entity.common().handle)
}
_ => None,
})
.collect();
for handle in hidden_images {
self.tabs[i].scene.images.remove(&handle);
}
self.tabs[i].scene.populate_meshes_from_document();
self.refresh_layer_panel();
self.tabs[i].dirty = true;
}
/// Execute a toolbar [`XrefPaletteOp`] on the palette's actionable
/// selection. Same engine fns as the CLI arms, batched with per-item
/// report lines in the CLI wording; finishes with [`post_ref_op`] plus
/// a palette rescan (mirror of the CLI post-op sequence).
pub(crate) fn xref_manager_op(
&mut self,
op: crate::ui::window::xref_manager::XrefPaletteOp,
) {
if cfg!(target_arch = "wasm32") {
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
return;
}
use crate::ui::window::xref_manager::XrefPaletteOp;
let i = self.active_tab;
// Full selection (nested rows included) with per-entry metadata, so
// nested rows report per-entry errors instead of being silently
// dropped by `actionable_selection` (mirrors the CLI arms).
let picked: Vec<(u64, String, crate::io::xref_model::RefKind, bool)> = {
let mut idx: Vec<usize> = self.xref_manager.selected.iter().copied().collect();
idx.sort_unstable();
idx.iter()
.filter_map(|idx| {
self.xref_manager.entries.get(*idx).map(|e| {
(
e.key,
e.name.clone(),
e.kind,
e.parent_key.is_some(),
)
})
})
.collect()
};
if picked.is_empty() {
return;
}
let needs_host = matches!(
op,
XrefPaletteOp::Reload | XrefPaletteOp::Bind | XrefPaletteOp::Pathtype(_)
);
let host: Option<std::path::PathBuf> = self.tabs[i].current_path.clone();
if needs_host && host.is_none() {
self.command_line
.push_error(crate::t!("XREF Save the drawing first to resolve relative XREF paths.").as_ref());
return;
}
let label = match op {
XrefPaletteOp::Detach => "XREF-DETACH",
XrefPaletteOp::Unload => "XREF-UNLOAD",
XrefPaletteOp::Reload => "XREF-RELOAD",
XrefPaletteOp::Bind => "XREF-BIND",
XrefPaletteOp::Overlay => "XREF-OVERLAY",
XrefPaletteOp::Attach => "XREF-ATTACH",
XrefPaletteOp::Pathtype(_) => "XREF-PATHTYPE",
};
self.push_undo_snapshot(i, label);
let mut done = 0usize;
match op {
XrefPaletteOp::Detach => {
for (key, _, _, is_nested) in &picked {
if *is_nested {
continue;
}
match crate::io::xref::detach_reference(
&mut self.tabs[i].scene.document,
*key,
) {
Ok(name) => {
self.command_line.push_output(crate::tf!(
"XREF: detached \"{}\".",
name
).as_ref());
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
XrefPaletteOp::Unload => {
for (key, name, _, is_nested) in &picked {
if *is_nested {
self.command_line.push_error(crate::tf!(
"XREF: cannot unload nested reference '{}'. Unload it in its host drawing.",
name
).as_ref());
continue;
}
match crate::io::xref::unload_reference(
&mut self.tabs[i].scene.document,
*key,
) {
Ok(name) => {
self.command_line.push_output(crate::tf!(
"XREF: unloaded \"{}\".",
name
).as_ref());
self.tabs[i].xref_unloaded.add(*key);
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
XrefPaletteOp::Reload => {
let base_dir: std::path::PathBuf = host
.as_ref()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
// Drawing references only: image/PDF rows keep their flags
// untouched and report per-entry (their names never match the
// DWG-only `XrefInfo` list, so matching after the flag clear
// would spuriously report no-match).
let dwg_picked: Vec<(u64, String)> = picked
.iter()
.filter(|(_, _, kind, is_nested)| {
if *is_nested {
return false;
}
*kind == crate::io::xref_model::RefKind::DwgXref
})
.map(|(key, name, _, _)| (*key, name.clone()))
.collect();
for (_, name, kind, is_nested) in &picked {
if *is_nested {
self.command_line.push_error(crate::tf!(
"XREF: cannot reload nested reference '{}'. Reload it in its host drawing.",
name
).as_ref());
} else if *kind != crate::io::xref_model::RefKind::DwgXref {
self.command_line.push_error(crate::tf!(
"{}: reload applies to drawing references only.",
name
).as_ref());
}
}
for (key, _) in &dwg_picked {
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let handles: rustc_hash::FxHashSet<acadrust::types::Handle> = self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| dwg_picked.iter().any(|(key, _)| *key == br.handle.value()))
.map(|br| br.handle)
.collect();
let (infos, _dropped) = crate::io::xref::resolve_xrefs_for_keys(
&mut self.tabs[i].scene.document,
&base_dir,
&handles,
);
// Targeted reload updates baselines only for selected rows.
{
let fresh = crate::io::xref::collect_entries_with_prev(
&self.tabs[i].scene.document,
&base_dir,
self.tabs[i].xref_unloaded.as_set(),
&self.tabs[i].xref_stat_cache.0,
);
for e in &fresh {
if !dwg_picked.iter().any(|(key, _)| *key == e.key) {
continue;
}
if e.status == crate::io::xref_model::RefStatus::Loaded {
if let Some(m) = e.modified {
self.tabs[i].xref_stat_cache.insert(e.key, m);
}
}
}
}
for (_, name) in &dwg_picked {
match infos
.iter()
.find(|n| n.name.eq_ignore_ascii_case(name))
{
Some(info) => {
self.report_xref_status(info);
done += 1;
}
None => self.command_line.push_error(crate::tf!(
"XREF: no references match '{}'.",
name
).as_ref()),
}
}
}
XrefPaletteOp::Overlay => {
for (key, name, _, is_nested) in &picked {
if *is_nested {
self.command_line.push_error(crate::tf!(
"XREF: cannot overlay nested reference '{}'. Overlay it in its host drawing.",
name
).as_ref());
continue;
}
match crate::io::xref::set_ref_type(
&mut self.tabs[i].scene.document,
*key,
crate::io::xref_model::RefType::Overlay,
) {
Ok(name) => {
self.command_line.push_output(crate::tf!(
"XREF: \"{}\" set to Overlay.",
name
).as_ref());
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
XrefPaletteOp::Attach => {
for (key, name, _, is_nested) in &picked {
if *is_nested {
self.command_line.push_error(crate::tf!(
"XREF: cannot attach nested reference '{}'. Attach it in its host drawing.",
name
).as_ref());
continue;
}
match crate::io::xref::set_ref_type(
&mut self.tabs[i].scene.document,
*key,
crate::io::xref_model::RefType::Attach,
) {
Ok(name) => {
self.command_line.push_output(crate::tf!(
"XREF: \"{}\" set to Attach.",
name
).as_ref());
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
XrefPaletteOp::Bind => {
let base_dir: std::path::PathBuf = host
.as_ref()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
let host_dir: std::path::PathBuf = host
.as_ref()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::path::PathBuf::from("."));
for (key, _, _, is_nested) in &picked {
if *is_nested {
continue;
}
match crate::io::xref::bind_reference(
&mut self.tabs[i].scene.document,
*key,
&base_dir,
&host_dir,
) {
Ok(outcome) => {
if outcome.unremapped == 0 {
self.command_line.push_output(crate::tf!(
"XREF: bound \"{}\".",
outcome.name
).as_ref());
} else {
self.command_line.push_output(crate::tf!(
"XREF: bound \"{}\" with {} unremapped style handles (see bind limitations).",
outcome.name, outcome.unremapped
).as_ref());
}
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
XrefPaletteOp::Pathtype(pathtype) => {
let host = host.unwrap_or_else(|| std::path::PathBuf::from("."));
for (key, name, _, is_nested) in &picked {
if *is_nested {
self.command_line.push_error(crate::tf!(
"XREF: cannot set path type for nested reference '{}'. Set it in its host drawing.",
name
).as_ref());
continue;
}
match crate::io::xref::apply_pathtype(
&mut self.tabs[i].scene.document,
*key,
pathtype,
&host,
) {
Ok(_) => {
self.command_line.push_output(crate::tf!(
"XREF: Path set for \"{}\" — Reload to apply.",
name
).as_ref());
done += 1;
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
}
}
if done > 0 {
self.post_ref_op(i);
}
self.refresh_xref_manager();
}
/// Apply the details-pane path draft to the anchor entry (verbatim
/// store, same engine fn as `XREF Path`).
pub(crate) fn xref_manager_path_apply(&mut self) {
if cfg!(target_arch = "wasm32") {
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
return;
}
let i = self.active_tab;
let anchor = self.xref_manager.anchor.and_then(|a| {
self.xref_manager
.entries
.get(a)
.filter(|_| !self.xref_manager.nested.contains(&a))
.map(|e| (e.key, e.name.clone()))
});
let Some((key, _)) = anchor else {
return;
};
let new_raw = self.xref_manager.path_input.clone();
if new_raw.is_empty() {
return;
}
self.push_undo_snapshot(i, "XREF-PATH");
match crate::io::xref::set_ref_path(&mut self.tabs[i].scene.document, key, &new_raw)
{
Ok(name) => {
self.command_line.push_output(crate::tf!(
"XREF: Path set for \"{}\" — Reload to apply.",
name
).as_ref());
self.post_ref_op(i);
}
Err(msg) => self.command_line.push_error(msg.as_str()),
}
self.refresh_xref_manager();
}
/// Apply Find & Replace across all direct references (same engine fn
/// as `XREF Path Find`).
pub(crate) fn xref_manager_find_replace_apply(&mut self) {
if cfg!(target_arch = "wasm32") {
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
return;
}
let i = self.active_tab;
let old = self.xref_manager.find_input.clone();
let new = self.xref_manager.replace_input.clone();
if old.is_empty() || new.is_empty() {
self.command_line
.push_error(crate::t!("Usage: XREF Path Find <old> <new>").as_ref());
return;
}
self.push_undo_snapshot(i, "XREF-PATH");
let n = crate::io::xref::replace_path_prefix(
&mut self.tabs[i].scene.document,
&old,
&new,
);
self.command_line.push_output(crate::tf!(
"XREF: updated {} reference(s).",
n
).as_ref());
if n > 0 {
self.post_ref_op(i);
}
self.refresh_xref_manager();
}
}
#[cfg(test)]
@@ -1280,4 +1728,159 @@ mod tests {
assert_eq!(app.block_name_from_file("Chair"), "Chair (2)");
assert_eq!(app.block_name_from_file("Table"), "Table");
}
#[test]
fn save_as_rebases_live_doc_paths() {
// F4: on Save-As completion the live document's relative reference
// paths are rebased onto the new base dir (same helper the save
// snapshot used), so the session agrees with the file just written.
let mut app = fresh();
let i = app.active_tab;
let dir = std::env::temp_dir().join(format!(
"ocs_saveas_rebase_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let old_dir = dir.join("old");
let new_dir = dir.join("new");
std::fs::create_dir_all(&old_dir).unwrap();
std::fs::create_dir_all(&new_dir).unwrap();
let mut br = acadrust::tables::BlockRecord::new("PLAN");
br.flags.is_xref = true;
br.xref_path = "refs/plan.dwg".to_string();
br.handle = app.tabs[i].scene.document.allocate_handle();
app.tabs[i]
.scene
.document
.block_records
.add(br)
.unwrap();
app.tabs[i].current_path = Some(old_dir.join("host.dwg"));
let tab_id = app.tabs[i].id;
let job_id = 4242u64;
app.active_save_jobs.insert(tab_id, job_id);
let outcome = crate::app::SaveOutcome {
job_id,
tab_id,
epoch: app.tabs[i].scene.geometry_epoch,
revision: app.tabs[i].edit_revision,
camera_generation: app.tabs[i].scene.camera_generation,
path: new_dir.join("host.dwg"),
version: app.tabs[i].scene.document.version,
previous_autosave: None,
set_current_path: true,
purpose: crate::app::SavePurpose::SaveAs,
continuation: crate::app::SaveContinuation::None,
refreshed_preview: None,
result: Ok(()),
};
let _ = app.on_save_finished(outcome);
assert_eq!(
app.tabs[i].current_path.as_deref(),
Some(new_dir.join("host.dwg").as_path())
);
let br = app.tabs[i]
.scene
.document
.block_records
.get("PLAN")
.unwrap();
assert_eq!(br.xref_path, "../old/refs/plan.dwg");
std::fs::remove_dir_all(&dir).ok();
}
fn palette_tmpdir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"ocs_palette_{tag}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn palette_output(app: &OpenCADStudio, start: usize) -> String {
app.command_line.history[start..]
.iter()
.map(|e| e.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn palette_unload_nested_reports_per_entry() {
// F6: palette Unload on a nested row reports the per-entry nested
// error (CLI wording) instead of the confusing 'no loaded reference'.
use acadrust::tables::BlockRecord;
use crate::ui::window::xref_manager::XrefPaletteOp;
let dir = palette_tmpdir("nested");
let mut host_doc = acadrust::CadDocument::new();
let mut inner = BlockRecord::new("INNER");
inner.flags.is_xref = true;
inner.xref_path = "inner.dwg".to_string();
host_doc.block_records.add(inner).unwrap();
let bytes = crate::io::save_to_bytes(&host_doc, "dwg", host_doc.version).unwrap();
std::fs::write(dir.join("host.dwg"), &bytes).unwrap();
let mut app = fresh();
let i = app.active_tab;
let mut br = BlockRecord::new("HOST");
br.flags.is_xref = true;
br.xref_path = dir.join("host.dwg").to_string_lossy().into_owned();
br.handle = app.tabs[i].scene.document.allocate_handle();
app.tabs[i].scene.document.block_records.add(br).unwrap();
app.tabs[i].current_path = Some(dir.join("app.dwg"));
app.refresh_xref_manager();
let idx = app.xref_manager.entries.iter().position(|e| e.name == "INNER").expect("nested INNER listed");
assert!(app.xref_manager.entries[idx].parent_key.is_some());
app.xref_manager.selected.insert(idx);
let start = app.command_line.history.len();
app.xref_manager_op(XrefPaletteOp::Unload);
let out = palette_output(&app, start);
assert!(out.contains("cannot unload nested reference 'INNER'"), "got: {out:?}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn palette_reload_skips_images_without_clearing_flags() {
// F7: palette Reload on an image/PDF row reports the drawing-only
// error and leaves its unloaded flag untouched (no spurious
// no-match after a flag clear).
use acadrust::objects::{ImageDefinition, ObjectType};
use crate::io::xref_model::RefKind;
use crate::ui::window::xref_manager::XrefPaletteOp;
let dir = palette_tmpdir("imgreload");
let mut app = fresh();
let i = app.active_tab;
let h = app.tabs[i].scene.document.allocate_handle();
let mut def = ImageDefinition::with_dimensions("img.png", 8, 8);
def.handle = h;
app.tabs[i].scene.document.objects.insert(h, ObjectType::ImageDefinition(def));
let mut img = acadrust::entities::RasterImage::new(
"img.png",
acadrust::types::Vector3::ZERO,
8.0,
8.0,
);
img.definition_handle = Some(h);
app.tabs[i].scene.document.add_entity(acadrust::EntityType::RasterImage(img)).unwrap();
app.tabs[i].current_path = Some(dir.join("host.dwg"));
app.refresh_xref_manager();
let idx = app.xref_manager.entries.iter().position(|e| e.kind == RefKind::Image).expect("image listed");
let key = app.xref_manager.entries[idx].key;
app.xref_manager.selected.insert(idx);
app.tabs[i].xref_unloaded.add(key);
let start = app.command_line.history.len();
app.xref_manager_op(XrefPaletteOp::Reload);
let out = palette_output(&app, start);
assert!(out.contains("reload applies to drawing references only"), "got: {out:?}");
assert!(!out.contains("no references match"), "spurious no-match, got: {out:?}");
assert!(app.tabs[i].xref_unloaded.is_unloaded(key), "image flag must stay untouched");
std::fs::remove_dir_all(&dir).ok();
}
}
+54 -1
View File
@@ -1420,6 +1420,21 @@ impl OpenCADStudio {
idx
};
// Missing-on-open: neutral command-line notice plus a flag
// the Reference Manager reads. Never an auto-open modal.
let missing = caches
.xrefs
.iter()
.filter(|item| item.status == crate::io::xref::XrefStatus::NotFound)
.count();
self.tabs[i].xref_missing = missing;
if missing > 0 {
self.command_line.push_output(crate::tf!(
"{} reference(s) not found — open the reference manager with EXTERNALREFERENCES.",
missing
).as_ref());
}
let mut recovery_report = recovery_needed.then(|| {
crate::io::recovery::RecoveryReport::recovered(
self.tabs[i].id,
@@ -2037,7 +2052,25 @@ impl OpenCADStudio {
crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID)
});
let clone_started = iced::time::Instant::now();
let snapshot = self.tabs[i].scene.document.clone();
let mut snapshot = self.tabs[i].scene.document.clone();
// Save-As across folders: rebase relative reference paths onto the
// new base dir inside the snapshot only (live strings are untouched).
if purpose == crate::app::SavePurpose::SaveAs {
let old_base = self.tabs[i]
.current_path
.as_ref()
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
let new_base = path.parent().map(|p| p.to_path_buf());
if let (Some(old), Some(new)) = (old_base, new_base) {
if old != new {
crate::io::xref::rebase_relative_paths_for_save_as(
&mut snapshot,
&old,
&new,
);
}
}
}
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
if crate::perf::enabled() {
crate::perf_record!(
@@ -2321,10 +2354,30 @@ impl OpenCADStudio {
}
}
if outcome.set_current_path {
let old_path = self.tabs[i].current_path.clone();
self.tabs[i].current_path = Some(outcome.path.clone());
self.tabs[i].scene.document.version = outcome.version;
if outcome.purpose == crate::app::SavePurpose::SaveAs {
self.tabs[i].recovery_save_as_required = false;
// Save-As across folders: the save snapshot was rebased
// in `queue_native_save`; rebase the live document too
// so the running session agrees with the file just
// written. Saves are not undoable, so no undo snapshot
// is pushed.
if let Some(old) = old_path.as_deref() {
let old_base = old.parent().map(|p| p.to_path_buf());
let new_base =
outcome.path.parent().map(|p| p.to_path_buf());
if let (Some(old), Some(new)) = (old_base, new_base) {
if old != new {
crate::io::xref::rebase_relative_paths_for_save_as(
&mut self.tabs[i].scene.document,
&old,
&new,
);
}
}
}
}
}
tasks.push(self.push_recent(outcome.path.clone()));
+24
View File
@@ -2156,6 +2156,30 @@ impl OpenCADStudio {
self.xref_manager.toggle_expand(key);
Task::none()
}
Message::XrefManagerOp(op) => {
self.xref_manager_op(op);
Task::none()
}
Message::XrefManagerPathInput(text) => {
self.xref_manager.path_input = text;
Task::none()
}
Message::XrefManagerPathApply => {
self.xref_manager_path_apply();
Task::none()
}
Message::XrefManagerFindInput(text) => {
self.xref_manager.find_input = text;
Task::none()
}
Message::XrefManagerReplaceInput(text) => {
self.xref_manager.replace_input = text;
Task::none()
}
Message::XrefManagerFindReplaceApply => {
self.xref_manager_find_replace_apply();
Task::none()
}
Message::LayerStateManagerOpen => {
let i = self.active_tab;
+4 -4
View File
@@ -20,8 +20,7 @@ impl OpenCADStudio {
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
// XREF-Task8: locale (hardcoded until Task 8 batches locale strings).
Some(K::XrefManager) => "Reference Manager".to_string(),
Some(K::XrefManager) => crate::t!("Reference Manager").into_owned(),
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
@@ -397,8 +396,9 @@ impl OpenCADStudio {
)
}
super::super::ModalKind::XrefManager => {
sized_flow(ex, 960, 520, |flow| {
self.xref_manager.view_window(flow)
sized_flow(ex, 960, 560, |flow| {
let missing = self.tabs[self.active_tab].xref_missing;
self.xref_manager.view_window(flow, missing)
})
}
super::super::ModalKind::LayerTranslator => {