Merge remote-tracking branch 'origin/main' into codex/import-formats

# Conflicts:
#	Cargo.lock
This commit is contained in:
ramox81 committed 2026-10-16 16:58:41 +03:00
commit d8959a4635
43 files changed
+189 -760

No files matched your search

+26 -403
View File
@@ -135,7 +135,7 @@ fn recovery_fingerprint_needed(caches: &DerivedCaches) -> bool {
crate::io::xref::XrefStatus::Recovered | crate::io::xref::XrefStatus::Failed
)
});
parser_issue || caches.corrupt_dropped > 0 || caches.xref_dropped > 0 || reference_issue
parser_issue || reference_issue
}
#[derive(Debug, Clone)]
@@ -444,17 +444,10 @@ async fn open_path_with_phase_attempt(
let parse_ms = t_parse.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
let t_purge = Instant::now();
let dropped = purge_corrupt_entities(&mut doc);
let purge_ms = t_purge.elapsed().as_millis() as u32;
if matches!(attempt, OpenAttempt::Strict) && dropped > 0 {
return Err(OpenAttemptFailure {
message: format!(
"normal read found {dropped} structurally invalid drawing records"
),
read_stats: Some(read_stats),
recoverable: true,
});
if matches!(attempt, OpenAttempt::Recovery(..)) {
repair_drawing(&mut doc);
}
let purge_ms = t_purge.elapsed().as_millis() as u32;
// Before the geometry is prepared below: repairing the
// drawing's stripped catalog linetypes after it would leave
// the first view drawn without their shapes. None is added:
@@ -462,7 +455,7 @@ async fn open_path_with_phase_attempt(
crate::io::linetypes::repair_document(&mut doc);
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
let t_xref = Instant::now();
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
let xref_infos = if let Some(base_dir) = path2.parent() {
let xref_progress = {
let progress = Arc::clone(&progress2);
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
@@ -484,7 +477,7 @@ async fn open_path_with_phase_attempt(
Some(xref_progress),
)
} else {
(Vec::new(), 0)
Vec::new()
};
let xref_ms = t_xref.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
@@ -511,9 +504,7 @@ async fn open_path_with_phase_attempt(
// Filled in after `prepare_open_geometry` below.
finalize_ms: 0,
};
caches.corrupt_dropped = dropped;
caches.read_stats = Some(read_stats);
caches.xref_dropped = xref_dropped;
caches.xrefs = xref_infos;
if recovery_fingerprint_needed(&caches) {
caches.source_sha256 =
@@ -805,25 +796,17 @@ async fn load_web_bytes(
fix_current_style_names(&mut doc);
crate::entities::field::local_header_dates(&mut doc);
progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1);
let dropped = purge_corrupt_entities(&mut doc);
if !recovery_mode && dropped > 0 {
return Err(OpenLoadError {
message: format!("normal read found {dropped} structurally invalid drawing records"),
source_sha256: None,
read_stats: Some(outcome.stats),
recovery_available: true,
});
if recovery_mode {
repair_drawing(&mut doc);
}
// XREFs inside the opened drawing resolve from the in-session bytes
// registry (see `resolve_web_xrefs`) before the caches snapshot the
// document, so merged xref geometry is part of the caches — the same
// phase order the native open uses.
let (xref_infos, xref_dropped) = resolve_web_xrefs(&mut doc, progress.clone()).await;
let xref_infos = resolve_web_xrefs(&mut doc, progress.clone()).await;
let mut caches = crate::scene::build_derived_caches(&doc);
caches.corrupt_dropped = dropped;
caches.read_stats = Some(outcome.stats);
caches.xrefs = xref_infos;
caches.xref_dropped = xref_dropped;
if source_sha256.is_none() && recovery_fingerprint_needed(&caches) {
source_sha256 = web_worker::sha256_document(bytes).await.ok();
}
@@ -896,7 +879,7 @@ pub async fn parse_drawing_bytes_web(name: &str, bytes: &[u8]) -> Result<CadDocu
async fn resolve_web_xrefs(
doc: &mut CadDocument,
progress: Arc<OpenProgressState>,
) -> (Vec<crate::io::xref::XrefInfo>, usize) {
) -> Vec<crate::io::xref::XrefInfo> {
let pending: Vec<String> = doc
.block_records
.iter()
@@ -1000,7 +983,7 @@ pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument, usize), String> {
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<CadDocument, String> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
@@ -1011,8 +994,7 @@ pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument,
doc.source_path = Some(path.to_string_lossy().into_owned());
// Opening is a field evaluation event (FIELDEVAL bit 1).
crate::entities::field::update_fields(&mut doc, 1, None);
let dropped = purge_corrupt_entities(&mut doc);
Ok((doc, dropped))
Ok(doc)
}
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
@@ -1259,7 +1241,6 @@ fn finalize_loaded_outcome(
let doc = &mut outcome.document;
normalize_block_origins(doc);
materialize_missing_block_markers(doc);
normalize_knotless_splines(doc);
if outcome.stats.source_format == Some(codec::SourceFormat::Dxf) {
fix_dxf_dimension_rotations(doc);
fix_dxf_layout_plot_settings(doc);
@@ -2684,174 +2665,24 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
set_sketch_settings(doc, sketch_type, increment, tolerance);
}
// ── Corrupt-entity guard ──────────────────────────────────────────────────
//
// opencadcodec's DWG parser occasionally desynchronises on certain files and
// produces entities with garbage fields: non-unit normals (components in
// 1e200+), nonsensical vertex counts (e.g. 100000), or infinite/NaN
// coordinates. Tessellating such entities triggers huge allocations and
// numerical blow-ups in the wire pipeline.
//
// `purge_corrupt_entities` scans the document and removes any entity that
// fails a cheap sanity check, returning the number dropped so the caller can
// surface it to the UI / log.
//
// Keep valid degenerate geometry: dropping it would trigger strict-open recovery.
fn finite_unit_normal(n: &codec::types::Vector3) -> bool {
let (x, y, z) = (n.x, n.y, n.z);
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
return false;
/// Recovery runs the codec's repair and reports what it did, and what it
/// left, through the drawing's notifications.
pub(crate) fn repair_drawing(doc: &mut CadDocument) {
let repair = doc.repair();
for action in &repair.actions {
doc.notifications.notify(
codec::notification::NotificationType::Warning,
format!("Recovery repair: {action:?}"),
);
}
let mag2 = x * x + y * y + z * z;
// Accept anything within ~10% of unit length. Real files sometimes
// store slightly denormalised normals from rounding.
(mag2 - 1.0).abs() < 0.21
}
fn finite_coord(v: f64) -> bool {
v.is_finite() && v.abs() < 1.0e12
}
fn finite_vec3(v: &codec::types::Vector3) -> bool {
finite_coord(v.x) && finite_coord(v.y) && finite_coord(v.z)
}
/// Returns true if the entity looks like parser garbage and should be dropped.
pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
use codec::entities::EntityType as E;
// Reject polylines at or above this vertex count. Even valid drawings
// rarely use this many — and parser desync produces exactly-100_000-vertex
// junk records.
const MAX_VERTS: usize = 100_000;
match e {
E::LwPolyline(p) => {
!finite_unit_normal(&p.normal)
|| p.vertices.len() >= MAX_VERTS
|| !finite_coord(p.elevation)
|| p.elevation.abs() > 1.0e10
|| !finite_coord(p.thickness)
|| p.thickness.abs() > 1.0e10
|| p.vertices
.iter()
.any(|v| !finite_coord(v.location.x) || !finite_coord(v.location.y))
}
E::Polyline2D(p) => {
!finite_unit_normal(&p.normal)
|| p.vertices.len() >= MAX_VERTS
|| !finite_coord(p.elevation)
|| p.elevation.abs() > 1.0e10
|| !finite_coord(p.thickness)
|| p.thickness.abs() > 1.0e10
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
}
E::Polyline3D(p) => {
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.position))
}
E::Polyline(p) => {
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.location))
}
E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end),
// Zero-radius curves are valid and have bounded kernel tessellation.
E::Circle(c) => {
!finite_vec3(&c.center) || !finite_coord(c.radius) || c.radius.abs() > 1.0e10
}
E::Arc(a) => {
!finite_vec3(&a.center)
|| !finite_coord(a.radius)
|| !a.start_angle.is_finite()
|| !a.end_angle.is_finite()
|| a.radius.abs() > 1.0e10
|| !finite_unit_normal(&a.normal)
}
E::Ellipse(e) => {
!finite_vec3(&e.center)
|| !finite_vec3(&e.major_axis)
|| !e.start_parameter.is_finite()
|| !e.end_parameter.is_finite()
|| (e.end_parameter - e.start_parameter).abs() < 1.0e-9
|| {
let m2 = e.major_axis.x * e.major_axis.x
+ e.major_axis.y * e.major_axis.y
+ e.major_axis.z * e.major_axis.z;
!m2.is_finite() || m2 < 1.0e-20 || m2 > 1.0e20
}
|| !e.minor_axis_ratio.is_finite()
|| e.minor_axis_ratio.abs() < 1.0e-10
}
E::Spline(s) => {
// Parser desync emits exactly-100_000-control-point splines with a
// garbage knot vector. Building a NURBS from one and
// tessellating it runs `parameter_division` into an unbounded
// allocation — single-threaded, 32 GB+ — long before the drawing
// finishes loading. Reject the desync signature plus any spline
// the kernel can't build: non-finite control points, or a knot vector
// that's non-finite, non-monotonic, or the wrong length
// (the kernel requires `knots.len() == ctrl.len() + degree + 1`).
let n = s.control_points.len();
let degree_bad = s.degree < 1;
let deg = s.degree.max(0) as usize;
let compact_periodic_knots = s.flags.periodic && s.knots.len() == n.saturating_add(1);
let knots_bad = !s.knots.is_empty()
&& (s.knots.iter().any(|k| !k.is_finite())
|| s.knots.windows(2).any(|w| w[1] < w[0])
|| (!compact_periodic_knots && s.knots.len() != n + deg + 1));
// A single control point with no knots and no fit points cannot
// form a curve, and no knot vector exists for it.
let lone_point = n == 1 && s.knots.is_empty() && s.fit_points.is_empty();
n >= MAX_VERTS
|| degree_bad
|| lone_point
|| s.control_points.iter().any(|p| !finite_vec3(p))
|| knots_bad
}
_ => false,
for issue in &repair.unresolved {
doc.notifications.notify(
codec::notification::NotificationType::Warning,
format!("Recovery left unresolved: {}", issue.message),
);
}
}
/// Give control-point splines stored without knots a degree their control
/// points can carry, and the matching clamped knot vector.
///
/// A degree-p curve needs at least p + 1 control points. Knot generation
/// (here, and in the DWG writer for knot-less splines) subtracts in `usize`
/// and underflows when there are fewer; in release builds the wrapped count
/// drives an unbounded allocation. Lowering the degree keeps the geometry the
/// points describe: two points are a straight segment at any degree.
fn normalize_knotless_splines(doc: &mut CadDocument) {
for entity in doc.entities_mut() {
let EntityType::Spline(spline) = entity else {
continue;
};
let n = spline.control_points.len();
if !spline.knots.is_empty() || n < 2 || spline.degree < 1 {
continue;
}
let degree = (spline.degree as usize).min(n - 1);
spline.degree = degree as i32;
spline.knots = codec::entities::Spline::generate_clamped_knots(degree, n);
}
}
pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize {
use crate::par::prelude::*;
normalize_knotless_splines(doc);
// Detection is pure and read-only; the per-vertex finite/extent checks on
// large polylines dominate, so fan the scan out across cores. Gather
// entity references in one pass, test in parallel, then remove serially
// (`remove_entity` needs `&mut doc`).
let entities: Vec<&EntityType> = doc.entities().collect();
let bad: Vec<codec::Handle> = entities
.par_iter()
.filter(|e| is_entity_corrupt(e))
.map(|e| e.common().handle)
.collect();
let n = bad.len();
for h in bad {
doc.remove_entity(h);
}
n
}
/// opencadcodec's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked,
/// but the real DXF/DWG spec uses bit 15 (0x8000) → viewport on and bit 14 (0x4000) → locked.
/// Files from AutoCAD and other tools always set bit 15 for active viewports, leaving bit 0
@@ -3166,214 +2997,6 @@ mod layer_roundtrip_tests {
}
}
#[cfg(test)]
mod corrupt_guard_tests {
use super::*;
use codec::entities::{Arc, Circle, EntityType, Spline};
use codec::types::Vector3;
fn knotless_spline(degree: i32, points: usize) -> Spline {
let mut spline = Spline::new();
spline.degree = degree;
spline.control_points = (0..points)
.map(|index| Vector3::new(index as f64, index as f64, 0.0))
.collect();
spline
}
// Degree 3 over two control points with no knot vector: generating knots
// for it underflows, and writing it to DWG used to panic (debug) or
// allocate without bound (release).
#[test]
fn knotless_spline_with_too_few_points_is_lowered_and_saves_as_dwg() {
let mut doc = CadDocument::new();
doc.add_entity(EntityType::Spline(knotless_spline(3, 2)))
.unwrap();
doc.add_entity(EntityType::Spline(knotless_spline(3, 1)))
.unwrap();
assert_eq!(
purge_corrupt_entities(&mut doc),
1,
"the lone point is dropped"
);
let spline = doc
.entities()
.find_map(|entity| match entity {
EntityType::Spline(spline) => Some(spline.clone()),
_ => None,
})
.expect("the two-point spline is kept");
assert_eq!(spline.degree, 1);
assert_eq!(spline.knots, vec![0.0, 0.0, 1.0, 1.0]);
let path =
std::env::temp_dir().join(format!("ocs_knotless_spline_{}.dwg", std::process::id()));
save_as_version(&doc, &path, codec::DxfVersion::AC1032).expect("save");
let _ = std::fs::remove_file(&path);
}
// `load_file` (headless export, xrefs, block import) does not purge, so
// the spline repair has to happen when any load is finalized.
#[test]
fn knotless_spline_is_repaired_by_every_load_path() {
const DXF: &str =
"0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n0\n71\n3\n72\n0\n73\n2\n74\n0\n\
10\n0\n20\n0\n30\n0\n10\n1\n20\n1\n30\n0\n0\nENDSEC\n0\nEOF\n";
let dir = std::env::temp_dir();
let source = dir.join(format!("ocs_knotless_load_{}.dxf", std::process::id()));
let target = dir.join(format!("ocs_knotless_load_{}.dwg", std::process::id()));
std::fs::write(&source, DXF).unwrap();
let doc = load_file(&source).expect("load");
let _ = std::fs::remove_file(&source);
let spline = doc
.entities()
.find_map(|entity| match entity {
EntityType::Spline(spline) => Some(spline),
_ => None,
})
.expect("spline");
assert_eq!(spline.degree, 1);
assert_eq!(spline.knots.len(), 4);
save_as_version(&doc, &target, codec::DxfVersion::AC1032).expect("save");
let _ = std::fs::remove_file(&target);
}
// Small but finite arcs are valid records. Kernel tessellation is bounded,
// so opening must retain them instead of treating their size as corruption.
#[test]
fn keeps_small_finite_arc() {
let mut a = Arc::new();
a.center = Vector3::new(2880.84, 891.83, 0.0);
a.radius = 0.0038974142851181423;
a.start_angle = 1.0401656235942365;
a.end_angle = 1.0401671831670538;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
// A nearly straight arc still carries valid source geometry.
#[test]
fn keeps_nearly_straight_arc() {
let mut a = Arc::new();
a.center = Vector3::new(551435.3071786845, 4051623.7156955916, 0.0);
a.radius = 35.0;
a.start_angle = 5.823361856481176;
a.end_angle = 5.823362506017916;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
// A large-radius small-sweep arc is still a visible curve and must survive:
// radius 1e6 × sweep 1e-4 ≈ 100 units of arc.
#[test]
fn keeps_large_radius_small_sweep_arc() {
let mut a = Arc::new();
a.radius = 1.0e6;
a.start_angle = 0.0;
a.end_angle = 1.0e-4;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
#[test]
fn keeps_zero_radius_circle_and_arc() {
let mut c = Circle::new();
c.center = Vector3::new(206.2, 150.7, 0.0);
c.radius = 0.0;
assert!(!is_entity_corrupt(&EntityType::Circle(c)));
let mut a = Arc::new();
a.center = Vector3::new(223.5, 174.5, 0.0);
a.radius = 0.0;
a.start_angle = 0.0;
a.end_angle = 0.0;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
#[test]
fn drops_absurd_radius_circle() {
let mut c = Circle::new();
c.radius = 1.0e11;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
let mut c = Circle::new();
c.radius = f64::NAN;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
}
// Parser desync emits 100_000-control-point splines; building a kernel
// NURBS from one and tessellating it OOMs. The control-point cap rejects it.
#[test]
fn rejects_desync_spline() {
let pts = vec![Vector3::new(0.0, 0.0, 0.0); 100_000];
let s = Spline::from_control_points(3, pts);
assert!(is_entity_corrupt(&EntityType::Spline(s)));
}
// A collapsed spline is still a valid source record. Kernel subdivision
// has a fixed depth bound and can process it safely.
#[test]
fn keeps_degenerate_point_spline() {
let pts = vec![Vector3::new(1e-12, -1e-12, 0.0); 9];
let s = Spline::from_control_points(3, pts);
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
#[test]
fn keeps_compact_periodic_spline_knots() {
let pts = (0..12)
.map(|index| Vector3::new(index as f64, (index % 3) as f64, 0.0))
.collect();
let mut s = Spline::from_control_points(2, pts);
s.flags.closed = true;
s.flags.periodic = true;
s.knots = (0..13).map(|value| value as f64).collect();
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
// A normal cubic spline (4 control points, valid clamped knots) survives.
#[test]
fn keeps_valid_spline() {
let pts = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(2.0, -1.0, 0.0),
Vector3::new(3.0, 0.0, 0.0),
];
let s = Spline::from_control_points(3, pts);
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
// A corrupt or adversarial MINSERT row/column pair (u16, so its unchecked
// product can reach into the billions) must be rejected before it can
// drive the render graph's per-instance allocation and expansion.
#[test]
fn preserves_large_minsert_data_while_rendering_is_bounded() {
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
i.row_count = u16::MAX;
i.column_count = u16::MAX;
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
// An ordinary array insert, well under the budget, is valid source data.
#[test]
fn keeps_a_reasonable_minsert() {
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
i.row_count = 10;
i.column_count = 10;
i.row_spacing = 5.0;
i.column_spacing = 5.0;
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
// A plain (non-array) INSERT is never treated as a MINSERT-count problem.
#[test]
fn keeps_a_plain_insert() {
let i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
}
#[cfg(test)]
mod pre_r2000_layer_plot_tests {
use super::*;
-19
View File
@@ -24,8 +24,6 @@ pub struct RecoveryReport {
pub source_sha256: Option<String>,
pub read_stats: Option<codec::ReadStats>,
pub entities_scanned: usize,
pub entities_removed: usize,
pub referenced_entities_removed: usize,
pub parser_errors_recovered: usize,
pub diagnostics: Vec<(String, String)>,
pub references_checked: usize,
@@ -54,8 +52,6 @@ impl RecoveryReport {
source_sha256: Option<String>,
read_stats: Option<codec::ReadStats>,
entities_scanned: usize,
entities_removed: usize,
referenced_entities_removed: usize,
references: &[XrefInfo],
notifications: &codec::notification::NotificationCollection,
save_as_required: bool,
@@ -163,8 +159,6 @@ impl RecoveryReport {
source_sha256,
read_stats,
entities_scanned,
entities_removed,
referenced_entities_removed,
parser_errors_recovered,
diagnostics,
references_checked: references.len(),
@@ -211,8 +205,6 @@ impl RecoveryReport {
.unwrap_or(0),
read_stats,
entities_scanned: 0,
entities_removed: 0,
referenced_entities_removed: 0,
diagnostics: Vec::new(),
references_checked: 0,
references_loaded: 0,
@@ -233,17 +225,11 @@ impl RecoveryReport {
}
}
pub fn removed_total(&self) -> usize {
self.entities_removed
.saturating_add(self.referenced_entities_removed)
}
pub fn issues_found(&self) -> usize {
self.parser_errors_recovered
.saturating_add(self.references_recovered)
.saturating_add(self.references_failed)
.saturating_add(self.references_missing)
.saturating_add(self.removed_total())
.saturating_add(usize::from(self.error.is_some()))
}
@@ -315,11 +301,6 @@ impl RecoveryReport {
));
}
lines.push(format!("Entities scanned: {}", self.entities_scanned));
lines.push(format!("Entities removed: {}", self.entities_removed));
lines.push(format!(
"Referenced entities removed: {}",
self.referenced_entities_removed
));
lines.push(format!(
"Parser errors recovered: {}",
self.parser_errors_recovered
+16 -44
View File
@@ -277,12 +277,8 @@ fn read_outcome_from_bytes(
/// `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) {
/// Returns a list of [`XrefInfo`] describing each xref block found.
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, None, None)
}
@@ -295,7 +291,7 @@ pub fn resolve_xrefs_for_keys(
doc: &mut CadDocument,
base_dir: &Path,
keys: &HashSet<Handle>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, None, Some(keys))
}
@@ -308,7 +304,7 @@ pub fn resolve_xrefs_with_progress(
doc: &mut CadDocument,
base_dir: &Path,
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, progress, None)
}
@@ -317,7 +313,7 @@ fn resolve_xrefs_with_filter(
base_dir: &Path,
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
only: Option<&HashSet<Handle>>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
let xref_entries: Vec<(String, String, Handle)> = doc
.block_records
@@ -432,7 +428,6 @@ fn resolve_xrefs_with_filter(
// Phase 2 — merge each parsed xref into the host document, in the original
// block order (par_iter preserves it), so handle allocation is deterministic.
let mut result = Vec::with_capacity(parsed.len());
let mut dropped = 0usize;
#[allow(unused_mut, unused_variables)]
for (
merge_index,
@@ -468,19 +463,7 @@ fn resolve_xrefs_with_filter(
.iter()
.map(|diagnostic| diagnostic.message.clone()),
);
let invalid = super::purge_corrupt_entities(&mut outcome.document);
if invalid > 0 {
diagnostics.push(format!(
"normal read found {invalid} structurally invalid reference records"
));
#[cfg(not(target_arch = "wasm32"))]
if source_sha256.is_none() {
source_sha256 = resolved.as_ref().and_then(|path| {
super::stable_sha256_file(path, initial_fingerprint.as_ref())
});
}
}
if recovered || invalid > 0 {
if recovered {
(XrefStatus::Failed, diagnostics, Some(outcome.stats))
} else {
// Reload is replacement, never append. Keep the prior merge
@@ -502,7 +485,7 @@ fn resolve_xrefs_with_filter(
let _ = unload_reference(doc, br_handle.value());
remove_pipe_symbols(doc, &block_name);
ensure_block_entities(doc, &block_name);
dropped += merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
if visretain {
restore_dependent_layers(doc, &block_name, kept_layers);
}
@@ -533,7 +516,7 @@ fn resolve_xrefs_with_filter(
}
}
(result, dropped)
result
}
/// Underlay files named by a relative (or moved) path: read them from where
@@ -1218,13 +1201,12 @@ fn copy_xref_sortents(
/// — 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
@@ -1264,7 +1246,7 @@ fn merge_xref_into_block(
// ── Entities (shared helper) ────────────────────────────────────────
// The unremapped-handle count is ignored on the load path (BIND reports
// it per bind).
let (dropped, _, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
let (_, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
// ── Draw-order overrides (shared helper) ──────────────────────────────
copy_xref_sortents(
@@ -1274,7 +1256,6 @@ fn merge_xref_into_block(
&maps.br_handles,
&entity_handle_map,
);
dropped
}
/// Source entities in display order: each block record's `entity_handles`
@@ -1308,26 +1289,18 @@ fn ordered_source_entities(xref_doc: &CadDocument) -> Vec<(Handle, EntityType)>
/// Merge one source doc's entities into `target`, routing model-space content
/// into `target_br` and nested-block content into its imported counterpart
/// (`maps.br_handles`). Returns corrupt-drop count, unremapped-handle count,
/// and the old→new entity handle map for the sortents copy.
/// (`maps.br_handles`). Returns the unremapped-handle count and the old→new
/// entity handle map for the sortents copy.
fn merge_source_entities(
target: &mut CadDocument,
source: &CadDocument,
target_br: Handle,
maps: &XrefSymbolMaps,
) -> (usize, usize, HashMap<Handle, Handle>) {
) -> (usize, HashMap<Handle, Handle>) {
let source_ms_handle = source.header.model_space_block_handle;
let mut entity_handle_map: HashMap<Handle, Handle> = HashMap::default();
let mut dropped = 0usize;
let mut unremapped = 0usize;
for (old_h, mut entity) in ordered_source_entities(source) {
// 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 every symbol reference so the host's resolver hits the copies
// just imported (load path ignores the unremapped count; BIND reports
// it per bind).
@@ -1351,7 +1324,7 @@ fn merge_source_entities(
entity_handle_map.insert(old_h, new_h);
}
}
(dropped, unremapped, entity_handle_map)
(unremapped, entity_handle_map)
}
fn is_sentinel_linetype(name: &str) -> bool {
@@ -2170,8 +2143,7 @@ fn merge_bound_doc(
taken,
};
let maps = import_xref_symbols(target, source, &mut naming);
let (_, unremapped, entity_handle_map) =
merge_source_entities(target, source, target_br, &maps);
let (unremapped, entity_handle_map) = merge_source_entities(target, source, target_br, &maps);
copy_xref_sortents(
target,
source,
@@ -2970,7 +2942,7 @@ mod tests {
// 3. Targeted reload: back to Loaded.
let mut handles = HashSet::default();
handles.insert(doc.block_records.get("MID").unwrap().handle);
let (infos, _) = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
let infos = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
assert!(infos.iter().any(|i| i.name == "MID"));
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
assert!(listed