forked from 1b-agent/opencadstudio-upstream
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:
1 parent
42219a4b8c
commit
d0d946a568
16 files changed
+4856
-499
No files matched your search
@@ -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).
|
||||
|
||||
+993
-45
File diff suppressed because it is too large.
Load diff
@@ -713,6 +713,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"XDATA",
|
||||
"XR",
|
||||
"XREF",
|
||||
"-XREF",
|
||||
"XREFMAN",
|
||||
"XRELOAD",
|
||||
"ZOOM",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user