DGNIMPORT: V8 text styles; 3D text ends at its byte count

A V8 file's text style table comes in as text styles (a style the drawing
has keeps its own; Standard stays Standard), and each text or text node
takes the style its text style linkage names. A 3D text's characters end
at the byte count the element gives, so a byte after them is no longer
read as a character.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 21:52:33 +03:00
1 parent 952cc0326d
commit b686d5cbe8
4 files changed
+74 -12

No files matched your search

+29 -10
View File
@@ -362,10 +362,20 @@ fn color_code(color: By<crate::scene::model::underlay_vector::DgnColor>) -> u16
}
}
/// The text style of a line of text (V7: one per font number, "dgnNNN"),
/// added to `styles` with its font file the first time.
fn text_style(geom: &DgnGeom, styles: &mut Vec<(String, String)>) -> String {
/// The text style of a line of text (V7: one per font number, "dgnNNN";
/// V8: the style the text names), added to `styles` with its font file
/// the first time.
fn text_style(geom: &DgnGeom, import: &DgnImport, styles: &mut Vec<(String, String)>) -> String {
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) {
styles.push((name.clone(), "txt.shx".to_string()));
}
name.clone()
}
_ => "Standard".to_string(),
},
DgnGeom::Text {
font: Some(font), ..
} => {
@@ -435,7 +445,7 @@ fn text_line(g: &DgnGeom) -> Option<(&str, P3, f64, f64, f64)> {
/// 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).
fn mtext_of_text(geom: &DgnGeom, color: u16) -> Option<EntityType> {
fn mtext_of_text(geom: &DgnGeom, color: u16, style: &str) -> Option<EntityType> {
let DgnGeom::Text {
text,
origin,
@@ -477,7 +487,7 @@ fn mtext_of_text(geom: &DgnGeom, color: u16) -> Option<EntityType> {
m.rotation = rotation.rem_euclid(std::f64::consts::TAU);
m.rectangle_width = 0.0;
m.drawing_direction = DrawingDirection::ByStyle;
m.style = "Standard".to_string();
m.style = style.to_string();
Some(EntityType::MText(Box::new(m)))
}
@@ -768,12 +778,15 @@ impl<'a> Converter<'a> {
}
// V7 text is TEXT, V8 text an MTEXT.
DgnGeom::Text { font: Some(_), .. } => {
let style = text_style(geom, &mut self.out.styles);
let style = text_style(geom, self.import, &mut self.out.styles);
text_entity(geom, &style).into_iter().collect()
}
DgnGeom::Text { .. } => mtext_of_text(geom, color_code(element.color))
.into_iter()
.collect(),
DgnGeom::Text { .. } => {
let style = text_style(geom, self.import, &mut self.out.styles);
mtext_of_text(geom, color_code(element.color), &style)
.into_iter()
.collect()
}
// Exploded, each line converts as a text element of its own.
DgnGeom::TextNode { lines, .. } if self.explode_text_nodes => lines
.iter()
@@ -788,7 +801,7 @@ impl<'a> Converter<'a> {
let Some(first) = lines.first() else {
return Vec::new();
};
let style = text_style(first, &mut self.out.styles);
let style = text_style(first, self.import, &mut self.out.styles);
let color = (!self.import.v7).then(|| color_code(element.color));
mtext_of_node(lines, *origin, *just, *line_spacing, style, color)
.into_iter()
@@ -905,6 +918,12 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
c.layer(&element, false);
}
}
// 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()));
}
}
c.in_definition = true;
for (name, elements) in &cells {
let entities = c.elements(elements, true, CellCopy::default(), 0);
+1
View File
@@ -533,6 +533,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw {
rotation: text.rotation,
just: e[37],
font: Some(e[36] as u32),
style: None,
}));
raw.texts.push(text);
}
+38 -2
View File
@@ -279,6 +279,34 @@ fn f64_at(e: &[u8], at: usize) -> Option<f64> {
}
/// A string stored after the "ff fe 01 00" marker, when the element has one.
/// The text style a text element uses: the id in its text style linkage
/// (the four bytes after `04 20 e6 04 00 00`).
fn text_style_id(e: &[u8]) -> Option<u32> {
let at = e.windows(6).position(|w| w == [0x04, 0x20, 0xe6, 0x04, 0x00, 0x00])? + 6;
u32_at(e, at)
}
/// The text style table (the table entries after a header of table 3):
/// each style's id and name.
fn text_style_table(nm: &[&[u8]]) -> Vec<(u32, String)> {
let mut out = Vec::new();
let mut table = 0;
for e in nm {
match kind(e) {
96 => table = u32_at(e, 12).unwrap_or(0),
95 if table == 3 => {
if let (Some(id), Some(name)) = (u32_at(e, 32), marked_string(e, 32)) {
if !out.iter().any(|(i, _)| *i == id) {
out.push((id, name));
}
}
}
_ => {}
}
}
out
}
fn marked_string(e: &[u8], from: usize) -> Option<String> {
let i = e
.get(from..)?
@@ -861,7 +889,8 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
}
17 => {
// 3D: width 112, height 120, quaternion 144, origin 176, the
// characters after the string marker. 2D: angle 144, origin
// characters after the string marker, as many as the byte
// count at 110 holds. 2D: angle 144, origin
// 152, a byte count (with its 4-byte marker) at 110 and the
// characters from 174.
let placed = if d3 {
@@ -871,7 +900,12 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
f64_at(e, 120)?,
quat_axes(e, 144)?,
p3(176)?,
marked_string(e, 200)?,
// The byte count at 110 (with the 4-byte marker)
// ends the characters.
{
let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
marked_string(e, 200)?.chars().take(count.saturating_sub(4)).collect()
},
))
})()
} else {
@@ -921,6 +955,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
rotation: pu[1].atan2(pu[0]),
just,
font: None,
style: text_style_id(e),
}));
raw.texts.push(Text {
text,
@@ -1524,6 +1559,7 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
unit_names: (metres(4180), metres(4196)),
v7: false,
references,
text_styles: text_style_table(&nm),
})
}
+6
View File
@@ -615,6 +615,8 @@ pub enum DgnGeom {
just: u8,
/// Font number (V7); `None` when the file's font is not read.
font: Option<u32>,
/// Text style id (V8), an entry of [`DgnImport::text_styles`].
style: Option<u32>,
},
/// A text node: its lines, its origin (the justification point, when
/// read), its justification and (V7) the space between its lines.
@@ -698,6 +700,7 @@ impl DgnGeom {
rotation,
just,
font,
style,
} => DgnGeom::Text {
text: text.clone(),
origin: p(*origin),
@@ -707,6 +710,7 @@ impl DgnGeom {
rotation: *rotation,
just: *just,
font: *font,
style: *style,
},
DgnGeom::Cell {
name,
@@ -786,6 +790,8 @@ pub struct DgnImport {
pub v7: bool,
/// The model's reference attachments.
pub references: Vec<DgnReference>,
/// The text style table (V8): each style's id and name.
pub text_styles: Vec<(u32, String)>,
}
/// A reference attachment of a DGN model: the file and model it shows and