Files
OneCAD-Draft/src/io/xref.rs
T
Hakan SevenandClaude Opus 4.8 70702d063f perf(hash): swap std HashMap/HashSet for rustc-hash FxHash
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.

Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-05 00:35:53 +03:00

292 lines
11 KiB
Rust

// 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::tables::TableEntry;
use acadrust::types::{Handle, Vector3};
use acadrust::{CadDocument, EntityType};
use rustc_hash::FxHashMap as HashMap;
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,
/// XRef is marked Unloaded in the host DWG — we honor that and
/// skip resolving the external file. The user can re-load via UI.
#[allow(dead_code)]
Unloaded,
}
/// 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, plus the
/// number of corrupt xref entities dropped during merge. Purging happens
/// inline as each xref's entities are merged — xref content is parser output
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
/// it in here avoids a second full-document `entities()` walk after resolve.
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>, usize) {
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
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();
let mut dropped = 0usize;
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);
dropped += merge_xref_into_block(doc, &block_name, 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, dropped)
}
/// 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()));
}
/// Merge an external-reference document into `doc`'s xref block.
///
/// Copies the xref's model-space entities into the host xref block, AND
/// (crucially for correct rendering) merges the xref's layer / linetype
/// tables and any *nested* block records into the host doc under prefixed
/// names ("{xref_name}|{symbol_name}"). Without this remapping, every
/// xref entity using ByLayer resolves against the host doc's layer table
/// — which doesn't know about the xref's layers — and silently falls
/// back to WHITE / 1 px line weight. AutoCAD's BIND command uses the
/// same naming scheme.
/// Returns the number of corrupt entities skipped during the merge.
fn merge_xref_into_block(
doc: &mut CadDocument,
xref_block_name: &str,
br_handle: Handle,
xref_doc: CadDocument,
) -> usize {
let prefix = xref_block_name;
// Carry the xref's INSUNITS onto the host BlockRecord. Used at INSERT
// time so the inserted xref scales to the host's units (INSUNITS).
let src_insunits = xref_doc.header.insertion_units;
if let Some(br) = doc.block_records.iter_mut().find(|b| b.handle == br_handle) {
br.units = src_insunits;
}
// ── Layers ──────────────────────────────────────────────────────────
// Prefix every xref layer (including "0"). Entity layer references are
// remapped below so the resolver finds the merged copy. Host layers
// (incl. its own "0") are untouched — no collisions.
let mut layer_map: HashMap<String, String> = HashMap::default();
for layer in xref_doc.layers.iter() {
let old = layer.name.clone();
let new = format!("{}|{}", prefix, old);
let mut cloned = layer.clone();
cloned.name = new.clone();
cloned.set_handle(doc.allocate_handle());
doc.layers.add_or_replace(cloned);
layer_map.insert(old.to_uppercase(), new);
}
// ── Linetypes ───────────────────────────────────────────────────────
// Skip the three sentinel names — "ByLayer" / "ByBlock" / "Continuous"
// are magic strings the resolver matches verbatim in both docs.
let mut linetype_map: HashMap<String, String> = HashMap::default();
for lt in xref_doc.line_types.iter() {
let old = lt.name.clone();
if is_sentinel_linetype(&old) {
continue;
}
let new = format!("{}|{}", prefix, old);
let mut cloned = lt.clone();
cloned.name = new.clone();
cloned.set_handle(doc.allocate_handle());
doc.line_types.add_or_replace(cloned);
linetype_map.insert(old.to_uppercase(), new);
}
// ── Block records (nested blocks inside the xref) ───────────────────
// First pass: create each prefixed BR with its host handle + empty
// entity_handles. Second pass (in the entity loop below) populates
// them by inserting entities with `owner_handle` set.
//
// Tracks (xref-doc BR handle → host BR handle) so entities owned by
// a nested xref block can be routed to the right host block_record.
let mut br_handle_map: HashMap<Handle, Handle> = HashMap::default();
let mut block_name_map: HashMap<String, String> = HashMap::default();
for br in xref_doc.block_records.iter() {
// Skip layout block records (*Model_Space, *Paper_Space, *Paper_Space0…)
// and any further-nested xrefs (we don't recurse into xref-of-xref).
if br.name.starts_with('*') || br.flags.is_xref || br.flags.is_xref_overlay {
continue;
}
let old = br.name.clone();
let new = format!("{}|{}", prefix, old);
let mut cloned = br.clone();
cloned.name = new.clone();
cloned.entity_handles.clear();
cloned.insert_handles.clear();
// Detach foreign layout pointer — it refers to a Layout in xref_doc
// we don't import.
cloned.layout = Handle::NULL;
let new_h = doc.allocate_handle();
cloned.set_handle(new_h);
cloned.block_entity_handle = doc.allocate_handle();
cloned.block_end_handle = doc.allocate_handle();
br_handle_map.insert(br.handle, new_h);
block_name_map.insert(old.to_uppercase(), new);
doc.block_records.add_or_replace(cloned);
}
// ── Entities ────────────────────────────────────────────────────────
let xref_ms_handle = xref_doc.header.model_space_block_handle;
let entities: Vec<EntityType> = xref_doc
.entities()
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
.cloned()
.collect();
let mut dropped = 0usize;
for mut entity in entities {
// Drop parser-garbage entities (bad normals / vertex counts / inf
// coords) before they enter the host doc — they trigger huge
// allocations and unbounded recursion in the wire pipeline.
if super::is_entity_corrupt(&entity) {
dropped += 1;
continue;
}
// Remap layer / linetype names so the host's resolver hits the
// copies we just inserted.
{
let c = entity.common_mut();
if let Some(new_layer) = layer_map.get(&c.layer.to_uppercase()) {
c.layer = new_layer.clone();
}
if !is_sentinel_linetype(&c.linetype) {
if let Some(new_lt) = linetype_map.get(&c.linetype.to_uppercase()) {
c.linetype = new_lt.clone();
}
}
}
// INSERTs reference blocks by name, not handle — remap.
if let EntityType::Insert(ins) = &mut entity {
if let Some(new_name) = block_name_map.get(&ins.block_name.to_uppercase()) {
ins.block_name = new_name.clone();
}
}
// Route entity to the correct host block record. Entities owned by
// the xref's model_space land in the host xref block (br_handle);
// entities owned by one of the nested BRs land in its prefixed
// counterpart. Paper-space / unknown owners are skipped.
let old_owner = entity.common().owner_handle;
let new_owner = if old_owner == xref_ms_handle {
br_handle
} else if let Some(&h) = br_handle_map.get(&old_owner) {
h
} else {
continue;
};
entity.common_mut().owner_handle = new_owner;
// Clear the foreign handle so acadrust assigns a new one.
set_handle(&mut entity, Handle::NULL);
let _ = doc.add_entity(entity);
}
dropped
}
fn is_sentinel_linetype(name: &str) -> bool {
name.eq_ignore_ascii_case("ByLayer")
|| name.eq_ignore_ascii_case("ByBlock")
|| name.eq_ignore_ascii_case("Continuous")
}
/// Set the handle field of any entity variant.
fn set_handle(entity: &mut EntityType, h: Handle) {
entity.common_mut().handle = h;
}