Keep DGN reference attachments and cell instances well-formed

Plain -DGNIMPORT no longer writes drawings next to referenced files on
its own: references are ignored unless the window (or the kept import
settings) asks to translate them or attach them as underlays.

A referenced file named relative to the host is looked up in the host's
folder rather than the working directory. Its translated drawing takes
a file name made from the model name with path separators and other
reserved characters replaced, so it is always written in the
reference's own folder. A reference placed in sub units is scaled by the
same factor as its origin.

Layer, block, text style and reference names read from the file are
made valid symbol names (reserved characters become underscores).

An instance of a cell that has no block (the point display cell, an
empty or missing definition) is dropped instead of inserting a block
that does not exist.

"Prompt to overwrite" still keeps an existing translated drawing; the
code now says so instead of implying a prompt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-16 23:11:18 +03:00
1 parent 6cbe62921b
commit ed4dc91670
2 files changed
+57 -23

No files matched your search

+18 -10
View File
@@ -458,7 +458,7 @@ impl OpenCADStudio {
.file_stem() .file_stem()
.map(|s| s.to_string_lossy().into_owned()) .map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(); .unwrap_or_default();
Naming::Prefix(format!("{stem} - {}$0$", request.model)) Naming::Prefix(dgn_import::symbol_name(&format!("{stem} - {}$0$", request.model)))
} else { } else {
Naming::Keep Naming::Keep
}; };
@@ -469,7 +469,7 @@ impl OpenCADStudio {
let layer = import let layer = import
.levels .levels
.get(&64) .get(&64)
.map(|l| l.0.clone()) .map(|l| dgn_import::symbol_name(&l.0))
.unwrap_or_else(|| "0".to_string()); .unwrap_or_else(|| "0".to_string());
let layer = layers.get(&layer).cloned().unwrap_or(layer); let layer = layers.get(&layer).cloned().unwrap_or(layer);
for reference in &import.references { for reference in &import.references {
@@ -502,9 +502,12 @@ impl OpenCADStudio {
.parent() .parent()
.map(|p| p.to_path_buf()) .map(|p| p.to_path_buf())
.unwrap_or_default(); .unwrap_or_default();
// A relative name is beside the host file; failing that, the file
// of that name in the host's folder.
let named = std::path::Path::new(&reference.file.replace('\\', "/")).to_path_buf(); let named = std::path::Path::new(&reference.file.replace('\\', "/")).to_path_buf();
let file = if named.is_file() { let joined = host_dir.join(&named);
named let file = if joined.is_file() {
joined
} else { } else {
host_dir.join(named.file_name().unwrap_or_default()) host_dir.join(named.file_name().unwrap_or_default())
}; };
@@ -519,13 +522,15 @@ impl OpenCADStudio {
}) else { }) else {
return; return;
}; };
// Sub units scale the placement and the size alike.
let origin = glam::DVec3::from_array(reference.origin) * units; let origin = glam::DVec3::from_array(reference.origin) * units;
let scale = reference.scale * units;
if request.references == ReferenceMode::Underlay { if request.references == ReferenceMode::Underlay {
let mut placed = crate::modules::insert::pdf_attach::underlays_for_pages( let mut placed = crate::modules::insert::pdf_attach::underlays_for_pages(
codec::entities::UnderlayType::Dgn, codec::entities::UnderlayType::Dgn,
std::slice::from_ref(&model), std::slice::from_ref(&model),
origin, origin,
reference.scale, scale,
reference.rotation, reference.rotation,
); );
let scene = &mut self.tabs[i].scene; let scene = &mut self.tabs[i].scene;
@@ -548,9 +553,12 @@ impl OpenCADStudio {
.file_stem() .file_stem()
.map(|s| s.to_string_lossy().into_owned()) .map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default(); .unwrap_or_default();
let target = file.with_file_name(format!("{stem}-{model}.dwg")); // The model name comes from the file: made a plain file name so the
// ponytail: "Prompt to overwrite" keeps the existing drawing, as the // drawing is written in the reference's folder.
// reference does when it cannot ask; ask here if a window is wanted. let model_name = dgn_import::symbol_name(&model);
let target = file.with_file_name(format!("{stem}-{model_name}.dwg"));
// ponytail: "Prompt to overwrite" asks nothing and keeps the existing
// drawing, like "Do not overwrite"; add a confirmation if wanted.
let write = !target.is_file() || request.existing_reference == ExistingReference::Overwrite; let write = !target.is_file() || request.existing_reference == ExistingReference::Overwrite;
if write { if write {
let Some(source) = dgn_import(&path, &model) else { let Some(source) = dgn_import(&path, &model) else {
@@ -569,7 +577,7 @@ impl OpenCADStudio {
overlay: false, overlay: false,
path_type: crate::io::xref_model::Pathtype::Relative, path_type: crate::io::xref_model::Pathtype::Relative,
}; };
let name = format!("{stem} - {model}"); let name = dgn_import::symbol_name(&format!("{stem} - {model}"));
let full = crate::modules::insert::xattach::resolve_xref_store_path(&xref.path, None); let full = crate::modules::insert::xattach::resolve_xref_store_path(&xref.path, None);
self.command_line self.command_line
.push_output(&format!("Attach Xref \"{name}\": {full}")); .push_output(&format!("Attach Xref \"{name}\": {full}"));
@@ -589,7 +597,7 @@ impl OpenCADStudio {
block, block,
codec::types::Vector3::new(origin.x, origin.y, origin.z), codec::types::Vector3::new(origin.x, origin.y, origin.z),
) )
.with_scale(reference.scale, reference.scale, reference.scale) .with_scale(scale, scale, scale)
.with_rotation(reference.rotation); .with_rotation(reference.rotation);
insert.common.layer = layer.to_string(); insert.common.layer = layer.to_string();
insert.common.linetype = "Continuous".to_string(); insert.common.linetype = "Continuous".to_string();
+39 -13
View File
@@ -50,8 +50,8 @@ pub struct DgnImportRequest {
/// as external references, left out, or attached as DGN underlays. /// as external references, left out, or attached as DGN underlays.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ReferenceMode { pub enum ReferenceMode {
#[default]
Translate, Translate,
#[default]
Ignore, Ignore,
Underlay, Underlay,
} }
@@ -109,11 +109,25 @@ pub fn unit_display(short: &str) -> String {
.to_string() .to_string()
} }
/// A name read from a DGN file as a symbol (layer, block, text style) or
/// file name: the characters those cannot hold become `_`.
pub fn symbol_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_control() || "<>/\\\":;?*|,=`".contains(c) {
'_'
} else {
c
}
})
.collect()
}
fn layer_name(import: &DgnImport, level: u32) -> String { fn layer_name(import: &DgnImport, level: u32) -> String {
import import
.levels .levels
.get(&level) .get(&level)
.map(|l| l.0.clone()) .map(|l| symbol_name(&l.0))
.filter(|n| !n.is_empty()) .filter(|n| !n.is_empty())
.unwrap_or_else(|| level.to_string()) .unwrap_or_else(|| level.to_string())
} }
@@ -121,7 +135,7 @@ fn layer_name(import: &DgnImport, level: u32) -> String {
/// A cell's name as a block name: a leading `*` (an anonymous block /// A cell's name as a block name: a leading `*` (an anonymous block
/// written out) is dropped. /// written out) is dropped.
pub fn block_name(cell: &str) -> String { pub fn block_name(cell: &str) -> String {
cell.trim_start_matches('*').to_string() symbol_name(cell.trim_start_matches('*'))
} }
fn v3(p: P3) -> Vector3 { fn v3(p: P3) -> Vector3 {
@@ -305,7 +319,7 @@ fn just_point(origin: P3, rotation: f64, length: f64, height: f64, just: u8) ->
let (h, v) = just_parts(just); let (h, v) = just_parts(just);
let along = length * h as f64 / 2.0; let along = length * h as f64 / 2.0;
let up = match v { let up = match v {
// Centre top is placed half a height up, as the reference places it. // Centre top is placed half a height up.
0 if just == 6 => height / 2.0, 0 if just == 6 => height / 2.0,
0 => height, 0 => height,
1 => height / 2.0, 1 => height / 2.0,
@@ -369,10 +383,11 @@ fn text_style(geom: &DgnGeom, import: &DgnImport, styles: &mut Vec<(String, Stri
match geom { match geom {
DgnGeom::Text { style: Some(id), .. } => match import.text_styles.iter().find(|(i, _)| i == id) { DgnGeom::Text { style: Some(id), .. } => match import.text_styles.iter().find(|(i, _)| i == id) {
Some((_, name)) if !name.eq_ignore_ascii_case("Standard") => { Some((_, name)) if !name.eq_ignore_ascii_case("Standard") => {
if !styles.iter().any(|s| s.0 == *name) { let name = symbol_name(name);
if !styles.iter().any(|s| s.0 == name) {
styles.push((name.clone(), "txt.shx".to_string())); styles.push((name.clone(), "txt.shx".to_string()));
} }
name.clone() name
} }
_ => "Standard".to_string(), _ => "Standard".to_string(),
}, },
@@ -441,7 +456,7 @@ fn text_line(g: &DgnGeom) -> Option<(&str, P3, f64, f64, f64)> {
} }
} }
/// A V8 text as MTEXT, as the reference converts it: a bottom left text /// A V8 text as MTEXT: a bottom left text
/// attached at its bottom left a third of the height below the baseline /// attached at its bottom left a third of the height below the baseline
/// (exact line spacing 0.8), any other at its justification point; its /// (exact line spacing 0.8), any other at its justification point; its
/// contents carry its colour (and width factor). /// contents carry its colour (and width factor).
@@ -602,8 +617,7 @@ fn arc_entity(center: P3, u: P3, v: P3, start: f64, sweep: f64) -> Vec<EntityTyp
} }
/// A shared cell instance as an INSERT: a mirrored placement looks along /// A shared cell instance as an INSERT: a mirrored placement looks along
/// -z (x negated in its entity coordinates, z scale negated), as the /// -z (x negated in its entity coordinates, z scale negated).
/// reference places it.
fn insert_entity(block: String, m: [f64; 4], origin: P3, z_scale: f64) -> EntityType { fn insert_entity(block: String, m: [f64; 4], origin: P3, z_scale: f64) -> EntityType {
let det = m[0] * m[3] - m[1] * m[2]; let det = m[0] * m[3] - m[1] * m[2];
let mirrored = det < 0.0; let mirrored = det < 0.0;
@@ -704,8 +718,7 @@ impl<'a> Converter<'a> {
} }
fn layer(&mut self, element: &DgnElement, in_cell: bool) -> String { fn layer(&mut self, element: &DgnElement, in_cell: bool) -> String {
// A cell's objects are on layer 0, as the reference defines the // A cell's objects are on layer 0 of its block.
// block.
let layer = if in_cell { let layer = if in_cell {
"0".to_string() "0".to_string()
} else { } else {
@@ -920,8 +933,9 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
} }
// Every text style of the file's table comes in, used or not. // Every text style of the file's table comes in, used or not.
for (_, name) in &import.text_styles { for (_, name) in &import.text_styles {
if !name.eq_ignore_ascii_case("Standard") && !c.out.styles.iter().any(|s| s.0 == *name) { let name = symbol_name(name);
c.out.styles.push((name.clone(), "txt.shx".to_string())); if !name.eq_ignore_ascii_case("Standard") && !c.out.styles.iter().any(|s| s.0 == name) {
c.out.styles.push((name, "txt.shx".to_string()));
} }
} }
c.in_definition = true; c.in_definition = true;
@@ -934,6 +948,18 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
c.in_definition = false; c.in_definition = false;
let entities = c.elements(&top, false, CellCopy::default(), 0); let entities = c.elements(&top, false, CellCopy::default(), 0);
c.out.entities = entities; c.out.entities = entities;
// A cell left without a block (the point display cell, an empty or
// missing definition) places nothing: its instances are dropped.
let defined: std::collections::HashSet<String> =
c.out.blocks.iter().map(|b| b.0.to_ascii_uppercase()).collect();
let placed = |e: &EntityType| match e {
EntityType::Insert(i) => defined.contains(&i.block_name.to_ascii_uppercase()),
_ => true,
};
c.out.entities.retain(placed);
for (_, entities) in &mut c.out.blocks {
entities.retain(placed);
}
c.out c.out
} }