diff --git a/contributors/people.toml b/contributors/people.toml index 7000a27ad..18c344815 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -66,3 +66,6 @@ sync_github_name = true real_name = "Antoine Boxho" display_name = "Antoine Boxho" sync_github_name = true + +[people.gurparvez] +real_name = "Gurparvez" diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 7c7da02a6..bae5c6c06 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -83,12 +83,17 @@ mod native { let why = outcome.stats.diagnostics.first().map(|d| d.message.trim_start_matches("Failsafe: ").to_string()); return Err(format!("DWG: {}", why.unwrap_or_else(|| "the file's objects couldn't be read".to_string()))); } - let notes = read_notes(&outcome.stats); + let mut notes = read_notes(&outcome.stats); let mut doc = outcome.document; current_style_names(&mut doc); table_merges(&mut doc); table_style_rows(&mut doc, false); dimension_flags(&mut doc, false); + let orphans = drop_orphaned_dgn_components(&mut doc); + if orphans > 0 { + let (s, were) = if orphans == 1 { ("", "was") } else { ("s", "were") }; + notes.push(format!("DWG: {orphans} DGN line style component{s} that no line style definition uses {were} left out")); + } let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false }; let written = std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DxfWriter::new(&doc).write_to_writer(&mut out))) .map_err(|_| "the DWG→DXF conversion failed on this file".to_string())?; @@ -288,6 +293,62 @@ mod native { } } + /// Leave out DGN line style components (`LS*COMPONENT`) that no line style definition uses + /// (#1482). A drawing that once came from MicroStation can carry hundreds of thousands of + /// them, orphaned copies piled up by earlier DGN imports and copy-pastes (AutoCAD's PURGE + /// removes them as orphaned data): in a real 83 MB site plan they were 888,483 of 889,053 + /// components and made its DXF rendition 1.2 GB, over [`Limits::DEFAULT`], so the file + /// didn't open at all. No definition that was read refers to them (a definition the reader + /// skipped can't use them either) and CADCraft doesn't draw DGN line styles, so nothing + /// visible is lost. + /// Every component a definition reaches (directly, or through compound and point + /// components) is kept, as are the definitions; dictionary entries that named a dropped + /// component go with it. Returns how many were left out. + fn drop_orphaned_dgn_components(doc: &mut acadrust::CadDocument) -> usize { + use acadrust::objects::{DgnLineStyleData, ObjectType}; + use std::collections::{HashMap, HashSet}; + // Each component's references, and the definitions' root components to start from. + let mut refs: HashMap> = HashMap::new(); + let mut stack: Vec = Vec::new(); + for (h, o) in &doc.objects { + let ObjectType::DgnLineStyle(ls) = o else { continue }; + match &ls.data { + DgnLineStyleData::Definition { root_component, .. } => stack.push(*root_component), + DgnLineStyleData::Component { component, .. } => { + refs.insert(*h, component.references()); + } + DgnLineStyleData::Registered { .. } => {} + } + } + if refs.is_empty() { + return 0; + } + // Each component is visited once, so cycles and shared subtrees end. + let mut used: HashSet = HashSet::new(); + while let Some(h) = stack.pop() { + if let Some(r) = refs.get(&h) + && used.insert(h) + { + stack.extend(r.iter().copied()); + } + } + let orphans: HashSet = refs.into_keys().filter(|h| !used.contains(h)).collect(); + if orphans.is_empty() { + return 0; + } + doc.objects.retain(|h, _| !orphans.contains(h)); + doc.dgn_ls_components.retain(|h, _| !orphans.contains(h)); + for o in doc.objects.values_mut() { + let ObjectType::Dictionary(d) = o else { continue }; + if d.entries.iter().any(|(_, e)| orphans.contains(e)) { + d.entries.retain(|(_, e)| !orphans.contains(e)); + let keys: HashSet<&str> = d.entries.iter().map(|(k, _)| k.as_str()).collect(); + d.hard_owner_entries.retain(|k| keys.contains(k.as_str())); + } + } + orphans.len() + } + /// Leave out what acadrust can't write as this DWG version: other programs' custom entities /// and objects without raw DWG data of that version. acadrust 0.6.4 refuses the whole file /// for one of them (0.6.3 left them out); CADCraft already tells the user which entity types @@ -593,4 +654,138 @@ mod tests { assert_eq!(flags.len(), 8, "{flags:?}"); assert!(flags.iter().all(|(l, f)| (*f == 128) == (*l == "MOVED")), "{flags:?}"); } + + /// The handles a DXF text refers to or gives its records (groups 5, 105 and 330–369). + #[cfg(not(target_arch = "wasm32"))] + fn dxf_handles(dxf: &str) -> std::collections::HashSet { + let lines: Vec<&str> = dxf.lines().collect(); + lines + .chunks(2) + .filter_map(|p| match p { + [c, v] if matches!(c.trim().parse::(), Ok(5 | 105 | 330..=369)) => u64::from_str_radix(v.trim(), 16).ok(), + _ => None, + }) + .collect() + } + + /// Issue #1482: a DWG that once came from MicroStation can carry hundreds of thousands of DGN + /// line style components no definition uses (a real 83 MB site plan: 888,483 of 889,053, a + /// 1.2 GB DXF rendition over the limit, so it didn't open). They're left out with a note, and + /// their dictionary entries with them; the definition and every component it reaches — + /// through compound and point components, around a cycle — are kept. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn orphaned_dgn_line_style_components_are_left_out() { + use acadrust::objects::*; + let mut doc = acadrust::CadDocument::new(); + let container = doc.allocate_handle(); + let nod = doc.header.named_objects_dict_handle; + let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&nod) else { panic!("no named object dictionary") }; + root.entries.push(("ACAD_DGNLINESTYLECOMP".into(), container)); + let pattern = || DgnLsStrokePattern { + has_iteration_limit: false, + is_single_segment: false, + iteration_limit: 0, + auto_phase: 0.0, + phase: 0.0, + phase_mode: DgnLsPhaseMode::Distance, + strokes: vec![DgnLsStroke { + is_dash: true, + bypass_corner: false, + can_be_scaled: true, + invert_at_origin: false, + invert_at_end: false, + length: 2.5, + start_width: 0.0, + end_width: 0.0, + width_mode: 0, + cap_mode: 0, + }], + }; + let kind = |c: &DgnLsComponentData| match c { + DgnLsComponentData::Symbol(_) => DgnLsComponentType::Symbol, + DgnLsComponentData::Compound(_) => DgnLsComponentType::Compound, + DgnLsComponentData::Stroke(_) => DgnLsComponentType::Stroke, + DgnLsComponentData::Point(_) => DgnLsComponentType::Point, + DgnLsComponentData::Internal(_) => DgnLsComponentType::Internal, + }; + let compound = |of: &[acadrust::Handle]| { + DgnLsComponentData::Compound(DgnLsCompoundComponent { + entries: of.iter().map(|&component| DgnLsCompoundEntry { component, offset: 0.0 }).collect(), + }) + }; + let mut entries = Vec::new(); + let mut put = |doc: &mut acadrust::CadDocument, h: acadrust::Handle, data: DgnLineStyleData| { + entries.push((format!("{:X}", h.value()), h)); + let reactors = vec![container]; + doc.objects + .insert(h, ObjectType::DgnLineStyle(DgnLineStyleObject { handle: h, owner: container, reactors, xdictionary_handle: None, data })); + }; + let component = |c: DgnLsComponentData| DgnLineStyleData::Component { + kind: kind(&c), + description: "dash".into(), + version: 1, + component_uid: [7; 16], + scale: 1.0, + property_flags: 0, + component: c, + properties: vec![], + }; + let h: Vec = (0..9).map(|_| doc.allocate_handle()).collect(); + let [def, root, stroke, point, back, orphan, orphan_cycle, internal, shared] = h[..] else { panic!("nine handles") }; + put( + &mut doc, + def, + DgnLineStyleData::Definition { + description: "{ dash }".into(), + version: 1, + style_number: 1, + component_uid: [1; 16], + is_continuous: false, + unit_definition: 1.0, + unit_scale: 1.0, + units_type: 0, + is_element: false, + is_physical: false, + is_scale_independent: false, + is_snappable: false, + root_component: root, + properties: vec![], + }, + ); + // Used: def → root → {stroke, point → shared, back → root (a cycle)}. + put(&mut doc, root, component(compound(&[stroke, point, back]))); + put(&mut doc, stroke, component(DgnLsComponentData::Stroke(pattern()))); + put(&mut doc, point, component(DgnLsComponentData::Point(DgnLsPointComponent { stroke_component: shared, symbols: vec![] }))); + put(&mut doc, back, component(compound(&[root]))); + put(&mut doc, shared, component(DgnLsComponentData::Stroke(pattern()))); + // Orphans: an internal component, and two compounds that refer to each other and to a + // used component (which stays). + let pattern_internal = + DgnLsInternalComponent { pattern: pattern(), internal_version: 0, hardware_style: 0, is_hardware_style: false, line_code: 0 }; + put(&mut doc, internal, component(DgnLsComponentData::Internal(pattern_internal))); + put(&mut doc, orphan, component(compound(&[orphan_cycle, shared]))); + put(&mut doc, orphan_cycle, component(compound(&[orphan]))); + let mut dict = Dictionary::new(); + dict.handle = container; + dict.owner = nod; + dict.entries = entries; + doc.objects.insert(container, ObjectType::Dictionary(dict)); + let dwg = acadrust::DwgWriter::write_to_vec(&doc).unwrap(); + + let (dxf, notes) = dwg_to_dxf_noted(&dwg, Limits::DEFAULT).unwrap(); + let dxf = String::from_utf8(dxf).unwrap(); + let handles = dxf_handles(&dxf); + for kept in [def, root, stroke, point, back, shared] { + assert!(handles.contains(&kept.value()), "{:X} was left out", kept.value()); + } + for gone in [internal, orphan, orphan_cycle] { + assert!(!handles.contains(&gone.value()), "{:X} was kept or is still referred to", gone.value()); + } + let lines: Vec<&str> = dxf.lines().collect(); + let ls: Vec<&str> = lines.chunks(2).filter_map(|p| p.get(1).filter(|_| p[0].trim() == "0" && p[1].starts_with("LS")).copied()).collect(); + assert_eq!(ls.iter().filter(|t| t.ends_with("COMPONENT")).count(), 5, "{ls:?}"); + assert!(ls.contains(&"LSDEFINITION") && !ls.contains(&"LSINTERNALCOMPONENT"), "{ls:?}"); + assert_eq!(notes, ["DWG: 3 DGN line style components that no line style definition uses were left out"]); + } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index bb9b7dc09..c598193d7 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (multiline attributes read, drawn and written) · **Target:** Autodesk AutoCAD 2027 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (DWG read leaves out orphaned DGN line style components) · **Target:** Autodesk AutoCAD 2027 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work items are in [gaps.md](gaps.md#file-formats); the weighted score feeds @@ -53,7 +53,10 @@ title/header suppression. Everything lost in the DXF read/write paths below is also lost through DWG, plus whatever `acadrust`'s own DXF output omits (proxy objects, ACIS data, custom objects: not measured). User report #57: a 10 MB DWG failed after about two minutes; limits now stop runaway conversions -early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% write.** +early (#175), but the throughput problem remains. DGN line style components that no line style +definition uses (leftovers of DGN imports, hundreds of thousands in some drawings) are left out +on read with a note, so such files no longer exceed the DXF limit (#1482). **DWG ≈ 30% read, +≈ 15% write.** ## DXF, version by version @@ -135,6 +138,7 @@ our output in AutoCAD black-box. | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | DWG read: DGN line style components no line style definition reaches are left out before the DXF bridge, with a read note; an 83 MB site plan with 888,483 of them (1.2 GB of DXF, over the limit) now opens (#1482) | | 2026-10-12 | minor | PDF output: object transparency plotted as PDF alpha (`/ExtGState` `/ca` `/CA`) when the page setup's or the plot's "Plot transparency" is on; PNG plots blend it, SVG uses `opacity`; plots without it are unchanged byte for byte | | 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space | | 2026-10-12 | minor | VIEWPORT frozen layers also written and read as AutoCAD's group 341 layer handles, so third-party viewers honour per-viewport freezing in DXF (DWG converter gap noted) |