feat: XREF management — auto-resolve, XATTACH, XREF, XRELOAD
- src/io/xref.rs: resolve_xrefs() scans block_records for is_xref flags,
resolves paths relative to the opened file's directory, loads the
referenced DWG/DXF and merges model-space entities into the xref block
- src/modules/insert/xattach.rs: XATTACH command with file-picker flow
(XAttachPick/XAttachPickResult messages), prepare_xref_block() creates
BlockRecord + Block entities and immediately resolves content
- XREF command: lists all external references in the drawing
- XRELOAD command: re-resolves all xref paths and rebuilds scene caches
- Auto-resolve on FileOpened: reports Loaded/NotFound per xref block
- CadCommand trait: added optional xattach_path() method
- ROADMAP.md: 1.9 XREF management marked ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c1c13cab6
commit
64837e9aed
10 changed files with 487 additions and 3 deletions
|
|
@ -18,7 +18,7 @@ Durum simgeleri: ✅ Tamamlandı · 🔧 Kısmen yapıldı · ⬜ Yapılmadı
|
|||
| 1.6 | Undo / Redo (snapshot stack) | ✅ |
|
||||
| 1.7 | PDF dışa aktarma (CTB/STB plot style) | ✅ |
|
||||
| 1.8 | Fiziksel yazıcıya yazdırma | ⬜ |
|
||||
| 1.9 | XREF (dış referans) yönetimi | ⬜ |
|
||||
| 1.9 | XREF (dış referans) yönetimi | ✅ Auto-resolve on open, XATTACH/XREF/XRELOAD commands |
|
||||
| 1.10 | WBLOCK — bloğu dış dosyaya yazma | ⬜ |
|
||||
| 1.11 | Serde entegrasyonu (JSON/alternatif I/O) | ⬜ |
|
||||
| 1.12 | Bozuk DWG kurtarma (failsafe parse) | ⬜ |
|
||||
|
|
|
|||
|
|
@ -65,6 +65,27 @@ impl H7CAD {
|
|||
self.refresh_properties();
|
||||
}
|
||||
CmdResult::CommitAndExit(entity) => {
|
||||
// For XATTACH: ensure the xref block definition exists before
|
||||
// committing the INSERT entity that references it.
|
||||
// Extract path early to avoid borrow conflicts.
|
||||
let xattach_path: Option<String> = {
|
||||
let tab = &self.tabs[i];
|
||||
if let Some(cmd) = tab.active_cmd.as_ref() {
|
||||
if cmd.name() == "XATTACH" {
|
||||
cmd.xattach_path()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(path) = xattach_path {
|
||||
crate::modules::insert::xattach::prepare_xref_block(
|
||||
&mut self.tabs[i].scene,
|
||||
&path,
|
||||
);
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.commit_entity(entity);
|
||||
|
|
|
|||
|
|
@ -417,6 +417,74 @@ impl H7CAD {
|
|||
}
|
||||
}
|
||||
|
||||
"XATTACH" | "XA" => {
|
||||
// Launch the file picker; XAttachPickResult will start the command.
|
||||
return Task::done(Message::XAttachPick);
|
||||
}
|
||||
|
||||
"XREF" | "XR" => {
|
||||
// List all xref blocks in the current drawing.
|
||||
let xrefs: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.map(|br| {
|
||||
format!(
|
||||
" {} — {}",
|
||||
br.name,
|
||||
if br.xref_path.is_empty() {
|
||||
"(no path)".to_string()
|
||||
} else {
|
||||
br.xref_path.clone()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if xrefs.is_empty() {
|
||||
self.command_line.push_output("XREF No external references in this drawing.");
|
||||
} else {
|
||||
self.command_line.push_output("XREF External references:");
|
||||
for line in xrefs {
|
||||
self.command_line.push_output(&line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"XRELOAD" => {
|
||||
// Reload all xrefs for the current drawing.
|
||||
if let Some(path) = &self.tabs[i].current_path.clone() {
|
||||
if let Some(base_dir) = path.parent() {
|
||||
let infos = crate::io::xref::resolve_xrefs(
|
||||
&mut self.tabs[i].scene.document,
|
||||
base_dir,
|
||||
);
|
||||
for info in &infos {
|
||||
match info.status {
|
||||
crate::io::xref::XrefStatus::Loaded => {
|
||||
self.command_line.push_output(&format!(
|
||||
"XREF Reloaded \"{}\"",
|
||||
info.name
|
||||
));
|
||||
}
|
||||
crate::io::xref::XrefStatus::NotFound => {
|
||||
self.command_line.push_error(&format!(
|
||||
"XREF Not found: \"{}\" ({})",
|
||||
info.name, info.path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.populate_hatches_from_document();
|
||||
self.tabs[i].scene.populate_images_from_document();
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error("XREF Save the drawing first to resolve relative XREF paths.");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Draw commands ──────────────────────────────────────────────
|
||||
"LINE"|"L" => {
|
||||
use crate::modules::home::draw::line::LineCommand;
|
||||
|
|
|
|||
|
|
@ -412,6 +412,11 @@ pub enum Message {
|
|||
ImagePick,
|
||||
/// Result of the image file picker + pixel dimension decode.
|
||||
ImagePickResult(Result<(std::path::PathBuf, u32, u32), String>),
|
||||
// ── XREF ──────────────────────────────────────────────────────────────
|
||||
/// Open file-picker dialog for XATTACH command (async).
|
||||
XAttachPick,
|
||||
/// Result of the XATTACH file picker.
|
||||
XAttachPickResult(Result<std::path::PathBuf, String>),
|
||||
}
|
||||
|
||||
impl H7CAD {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,33 @@ impl H7CAD {
|
|||
idx
|
||||
};
|
||||
|
||||
self.tabs[i].current_path = Some(path);
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
self.tabs[i].scene.document = doc;
|
||||
|
||||
// Auto-resolve XREFs relative to the opened file's directory.
|
||||
if let Some(base_dir) = path.parent() {
|
||||
let xrefs = crate::io::xref::resolve_xrefs(
|
||||
&mut self.tabs[i].scene.document,
|
||||
base_dir,
|
||||
);
|
||||
for info in &xrefs {
|
||||
match info.status {
|
||||
crate::io::xref::XrefStatus::Loaded => {
|
||||
self.command_line.push_output(&format!(
|
||||
"XREF Loaded \"{}\"",
|
||||
info.name
|
||||
));
|
||||
}
|
||||
crate::io::xref::XrefStatus::NotFound => {
|
||||
self.command_line.push_error(&format!(
|
||||
"XREF Not found: \"{}\" ({})",
|
||||
info.name, info.path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].scene.populate_hatches_from_document();
|
||||
self.tabs[i].scene.populate_images_from_document();
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
|
|
@ -103,6 +128,41 @@ impl H7CAD {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
Message::XAttachPick => Task::perform(
|
||||
async {
|
||||
let handle = rfd::AsyncFileDialog::new()
|
||||
.set_title("Select External Reference File")
|
||||
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
||||
.add_filter("DWG Files", &["dwg", "DWG"])
|
||||
.add_filter("DXF Files", &["dxf", "DXF"])
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
Some(h) => Ok(h.path().to_path_buf()),
|
||||
None => Err("Cancelled".to_string()),
|
||||
}
|
||||
},
|
||||
Message::XAttachPickResult,
|
||||
),
|
||||
|
||||
Message::XAttachPickResult(Ok(path)) => {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::insert::xattach::XAttachCommand;
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let cmd = XAttachCommand::with_path(path_str);
|
||||
let i = self.active_tab;
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::XAttachPickResult(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
self.command_line.push_error(&format!("XATTACH: {e}"));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SaveFile => {
|
||||
let i = self.active_tab;
|
||||
if let Some(path) = &self.tabs[i].current_path {
|
||||
|
|
|
|||
|
|
@ -227,6 +227,12 @@ pub trait CadCommand: Send {
|
|||
false
|
||||
}
|
||||
|
||||
/// If this command is XATTACH, returns the file path to attach.
|
||||
/// Default: None.
|
||||
fn xattach_path(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Called instead of `on_point` when the command needs a tangent pick
|
||||
/// and the snap system found a tangent object.
|
||||
fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
pub mod pdf_export;
|
||||
pub mod plot_style;
|
||||
pub mod xref;
|
||||
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
|
|
|
|||
143
src/io/xref.rs
Normal file
143
src/io/xref.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// XREF resolution — scan a loaded document for external-reference blocks and
|
||||
// populate them with geometry from the referenced DWG/DXF files.
|
||||
|
||||
use acadrust::entities::{Block, BlockEnd};
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Status of an external reference block.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum XrefStatus {
|
||||
/// File was found and loaded successfully.
|
||||
Loaded,
|
||||
/// File path is set but the file could not be found or read.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// Describes a single external reference found in a document.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct XrefInfo {
|
||||
/// Block name (e.g. the filename stem).
|
||||
pub name: String,
|
||||
/// Resolved file path (or raw path if not found).
|
||||
pub path: String,
|
||||
pub status: XrefStatus,
|
||||
}
|
||||
|
||||
/// Scan `doc` for XREF block-records, resolve their paths relative to
|
||||
/// `base_dir`, and populate each xref block with entities from the
|
||||
/// referenced file.
|
||||
///
|
||||
/// Returns a list of [`XrefInfo`] describing each xref block found.
|
||||
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
|
||||
// Collect xref blocks: (name, raw_path, block_record_handle)
|
||||
let xref_entries: Vec<(String, String, Handle)> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| (br.flags.is_xref || br.flags.is_xref_overlay) && !br.xref_path.is_empty())
|
||||
.map(|br| (br.name.clone(), br.xref_path.clone(), br.handle))
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (block_name, raw_path, br_handle) in xref_entries {
|
||||
let resolved = resolve_path(&raw_path, base_dir);
|
||||
|
||||
let status = match &resolved {
|
||||
None => XrefStatus::NotFound,
|
||||
Some(p) => match super::load_file(p) {
|
||||
Err(_) => XrefStatus::NotFound,
|
||||
Ok(xref_doc) => {
|
||||
ensure_block_entities(doc, &block_name);
|
||||
merge_xref_into_block(doc, br_handle, xref_doc);
|
||||
XrefStatus::Loaded
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
result.push(XrefInfo {
|
||||
name: block_name,
|
||||
path: resolved
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or(raw_path),
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Try to build an absolute path from a raw xref path string.
|
||||
/// Handles absolute paths, relative paths, and Windows-style separators.
|
||||
fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||
let normalised = raw.replace('\\', "/");
|
||||
let p = PathBuf::from(&normalised);
|
||||
|
||||
if p.is_absolute() {
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
// Fallback: try the filename in base_dir.
|
||||
if let Some(fname) = p.file_name() {
|
||||
let c = base_dir.join(fname);
|
||||
if c.exists() {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Relative path against base_dir.
|
||||
let candidate = base_dir.join(&p);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
// Last resort: just the filename.
|
||||
if let Some(fname) = p.file_name() {
|
||||
let c = base_dir.join(fname);
|
||||
if c.exists() {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Make sure `doc` has BLOCK + ENDBLK entities for `block_name`.
|
||||
/// These are required so renderers can find the block content.
|
||||
fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let has_block = doc.entities().any(|e| {
|
||||
matches!(e, EntityType::Block(b) if b.name == block_name)
|
||||
});
|
||||
if has_block {
|
||||
return;
|
||||
}
|
||||
let b = Block::new(block_name, Vector3::zero());
|
||||
let _ = doc.add_entity(EntityType::Block(b));
|
||||
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||
}
|
||||
|
||||
/// Copy model-space entities from `xref_doc` into the xref block (`br_handle`)
|
||||
/// of `doc`.
|
||||
fn merge_xref_into_block(doc: &mut CadDocument, br_handle: Handle, xref_doc: CadDocument) {
|
||||
let entities: Vec<EntityType> = xref_doc
|
||||
.entities()
|
||||
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for mut entity in entities {
|
||||
// Clear the foreign handle so acadrust assigns a new one.
|
||||
set_handle(&mut entity, Handle::NULL);
|
||||
// Route to the xref block record.
|
||||
entity.common_mut().owner_handle = br_handle;
|
||||
let _ = doc.add_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the handle field of any entity variant.
|
||||
fn set_handle(entity: &mut EntityType, h: Handle) {
|
||||
entity.common_mut().handle = h;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ mod cylinder;
|
|||
pub(crate) mod insert_block;
|
||||
mod open_obj;
|
||||
mod sphere;
|
||||
pub(crate) mod xattach;
|
||||
|
||||
use crate::modules::{CadModule, RibbonGroup};
|
||||
|
||||
|
|
@ -36,7 +37,11 @@ impl CadModule for InsertModule {
|
|||
},
|
||||
RibbonGroup {
|
||||
title: "Block",
|
||||
tools: vec![create_block::tool().into(), insert_block::tool().into()],
|
||||
tools: vec![
|
||||
create_block::tool().into(),
|
||||
insert_block::tool().into(),
|
||||
xattach::tool().into(),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
175
src/modules/insert/xattach.rs
Normal file
175
src/modules/insert/xattach.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// XATTACH command — attach an external DWG/DXF file as an XREF block
|
||||
// and insert it at a picked point.
|
||||
//
|
||||
// Workflow:
|
||||
// Step 1 (text input): user types the file path (or the file-picker
|
||||
// message has already supplied it).
|
||||
// Step 2 (point pick): user clicks the insertion point.
|
||||
// Result: BlockRecord + Block entities are created with is_xref=true,
|
||||
// then an INSERT entity is committed.
|
||||
|
||||
use acadrust::entities::{Block, BlockEnd, Insert};
|
||||
use acadrust::tables::block_record::{BlockFlags, BlockRecord};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
use crate::scene::Scene;
|
||||
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "XATTACH",
|
||||
label: "Attach XREF",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")),
|
||||
event: ModuleEvent::Command("XATTACH".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum Step {
|
||||
/// Waiting for the user to type (or accept) a file path.
|
||||
FilePath,
|
||||
/// File path is confirmed; waiting for an insertion point.
|
||||
InsertionPoint { path: String, block_name: String },
|
||||
}
|
||||
|
||||
pub struct XAttachCommand {
|
||||
step: Step,
|
||||
/// Pre-supplied path (from the file-picker message).
|
||||
prefilled_path: Option<String>,
|
||||
}
|
||||
|
||||
impl XAttachCommand {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::FilePath,
|
||||
prefilled_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an XATTACH command with a path already filled in (from file-picker).
|
||||
pub fn with_path(path: String) -> Self {
|
||||
let block_name = path_to_block_name(&path);
|
||||
Self {
|
||||
step: Step::InsertionPoint {
|
||||
path: path.clone(),
|
||||
block_name,
|
||||
},
|
||||
prefilled_path: Some(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for XAttachCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"XATTACH"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::FilePath => "XATTACH Enter path to external DWG/DXF file:".to_string(),
|
||||
Step::InsertionPoint { block_name, .. } => {
|
||||
format!("XATTACH Specify insertion point for \"{}\":", block_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::FilePath)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if !matches!(self.step, Step::FilePath) {
|
||||
return None;
|
||||
}
|
||||
let path = text.trim().to_string();
|
||||
if path.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let block_name = path_to_block_name(&path);
|
||||
self.step = Step::InsertionPoint { path, block_name };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::FilePath => CmdResult::NeedPoint,
|
||||
Step::InsertionPoint { path: _, block_name } => {
|
||||
// We return the INSERT entity; the command handler in
|
||||
// commands.rs will call `prepare_xref_block` on the scene
|
||||
// before committing.
|
||||
CmdResult::CommitAndExit(EntityType::Insert(Insert::new(
|
||||
block_name.clone(),
|
||||
Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn xattach_path(&self) -> Option<String> {
|
||||
match &self.step {
|
||||
Step::InsertionPoint { path, .. } => Some(path.clone()),
|
||||
Step::FilePath => self.prefilled_path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a block name from the file path: take the file stem, uppercase it.
|
||||
pub fn path_to_block_name(path: &str) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
p.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_else(|| "XREF".to_string())
|
||||
}
|
||||
|
||||
/// Create the XREF BlockRecord + Block/EndBlock entities in the scene document
|
||||
/// for a given file path. Returns the block name.
|
||||
///
|
||||
/// This must be called before committing the INSERT so that the block
|
||||
/// definition exists when the renderer looks it up.
|
||||
pub fn prepare_xref_block(scene: &mut Scene, path: &str) -> String {
|
||||
let block_name = path_to_block_name(path);
|
||||
|
||||
// If a BlockRecord already exists with this name, skip creation.
|
||||
if scene.document.block_records.get(&block_name).is_some() {
|
||||
return block_name;
|
||||
}
|
||||
|
||||
// Create the BlockRecord.
|
||||
let mut br = BlockRecord::new(&block_name);
|
||||
br.handle = scene.document.allocate_handle();
|
||||
br.flags = BlockFlags {
|
||||
is_xref: true,
|
||||
is_xref_overlay: false,
|
||||
anonymous: false,
|
||||
has_attributes: false,
|
||||
is_external: false,
|
||||
};
|
||||
br.xref_path = path.to_string();
|
||||
let _ = scene.document.block_records.add(br);
|
||||
|
||||
// Create BLOCK entity.
|
||||
let b = Block::new(&block_name, Vector3::zero()).with_xref_path(path);
|
||||
let _ = scene.document.add_entity(EntityType::Block(b));
|
||||
let _ = scene.document.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||
|
||||
// Resolve the XREF content immediately.
|
||||
let path_buf = std::path::PathBuf::from(path);
|
||||
if let Some(base_dir) = path_buf.parent() {
|
||||
crate::io::xref::resolve_xrefs(&mut scene.document, base_dir);
|
||||
}
|
||||
|
||||
block_name
|
||||
}
|
||||
Loading…
Reference in a new issue