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 <noreply@anthropic.com>
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()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Naming::Prefix(format!("{stem} - {}$0$", request.model))
Naming::Prefix(dgn_import::symbol_name(&format!("{stem} - {}$0$", request.model)))
} else {
Naming::Keep
};
@@ -469,7 +469,7 @@ impl OpenCADStudio {
let layer = import
.levels
.get(&64)
.map(|l| l.0.clone())
.map(|l| dgn_import::symbol_name(&l.0))
.unwrap_or_else(|| "0".to_string());
let layer = layers.get(&layer).cloned().unwrap_or(layer);
for reference in &import.references {
@@ -502,9 +502,12 @@ impl OpenCADStudio {
.parent()
.map(|p| p.to_path_buf())
.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 file = if named.is_file() {
named
let joined = host_dir.join(&named);
let file = if joined.is_file() {
joined
} else {
host_dir.join(named.file_name().unwrap_or_default())
};
@@ -519,13 +522,15 @@ impl OpenCADStudio {
}) else {
return;
};
// Sub units scale the placement and the size alike.
let origin = glam::DVec3::from_array(reference.origin) * units;
let scale = reference.scale * units;
if request.references == ReferenceMode::Underlay {
let mut placed = crate::modules::insert::pdf_attach::underlays_for_pages(
codec::entities::UnderlayType::Dgn,
std::slice::from_ref(&model),
origin,
reference.scale,
scale,
reference.rotation,
);
let scene = &mut self.tabs[i].scene;
@@ -548,9 +553,12 @@ impl OpenCADStudio {
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let target = file.with_file_name(format!("{stem}-{model}.dwg"));
// ponytail: "Prompt to overwrite" keeps the existing drawing, as the
// reference does when it cannot ask; ask here if a window is wanted.
// The model name comes from the file: made a plain file name so the
// drawing is written in the reference's folder.
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;
if write {
let Some(source) = dgn_import(&path, &model) else {
@@ -569,7 +577,7 @@ impl OpenCADStudio {
overlay: false,
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);
self.command_line
.push_output(&format!("Attach Xref \"{name}\": {full}"));
@@ -589,7 +597,7 @@ impl OpenCADStudio {
block,
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);
insert.common.layer = layer.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.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ReferenceMode {
#[default]
Translate,
#[default]
Ignore,
Underlay,
}
@@ -109,11 +109,25 @@ pub fn unit_display(short: &str) -> 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 {
import
.levels
.get(&level)
.map(|l| l.0.clone())
.map(|l| symbol_name(&l.0))
.filter(|n| !n.is_empty())
.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
/// written out) is dropped.
pub fn block_name(cell: &str) -> String {
cell.trim_start_matches('*').to_string()
symbol_name(cell.trim_start_matches('*'))
}
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 along = length * h as f64 / 2.0;
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 => height,
1 => height / 2.0,
@@ -369,10 +383,11 @@ fn text_style(geom: &DgnGeom, import: &DgnImport, styles: &mut Vec<(String, Stri
match geom {
DgnGeom::Text { style: Some(id), .. } => match import.text_styles.iter().find(|(i, _)| i == id) {
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()));
}
name.clone()
name
}
_ => "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
/// (exact line spacing 0.8), any other at its justification point; its
/// 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
/// -z (x negated in its entity coordinates, z scale negated), as the
/// reference places it.
/// -z (x negated in its entity coordinates, z scale negated).
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 mirrored = det < 0.0;
@@ -704,8 +718,7 @@ impl<'a> Converter<'a> {
}
fn layer(&mut self, element: &DgnElement, in_cell: bool) -> String {
// A cell's objects are on layer 0, as the reference defines the
// block.
// A cell's objects are on layer 0 of its block.
let layer = if in_cell {
"0".to_string()
} 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.
for (_, name) in &import.text_styles {
if !name.eq_ignore_ascii_case("Standard") && !c.out.styles.iter().any(|s| s.0 == *name) {
c.out.styles.push((name.clone(), "txt.shx".to_string()));
let name = symbol_name(name);
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;
@@ -934,6 +948,18 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
c.in_definition = false;
let entities = c.elements(&top, false, CellCopy::default(), 0);
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
}