Merge pull request #1812 from ramox81/codex/import-dgnimport

IMPORT window and DGNIMPORT / -DGNIMPORT

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/lo-LA/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
This commit is contained in:
Hakan Seven committed 2026-10-15 16:47:21 +03:00
commit 2b4c4bfdc6
39 files changed
+3210 -13

No files matched your search

+117 -4
View File
@@ -6,8 +6,9 @@
use std::collections::HashMap;
use super::model::{
arc_cubics, bspline_points, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet,
SubPath, Text, DGN_DEFAULT_COLORS,
arc_cubics, bspline_points, dgn_color_table, paths_bounds, scaled_elements, DgnCollector,
DgnElement, DgnGeom, DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text,
DGN_DEFAULT_COLORS,
};
/// A 32-bit integer stored as two little-endian words, high word first.
@@ -123,6 +124,8 @@ struct Raw {
paths: Vec<Path>,
texts: Vec<Text>,
instances: Vec<Instance>,
/// The same run element by element, for DGNIMPORT.
elements: Vec<DgnElement>,
}
#[derive(Clone)]
@@ -199,6 +202,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
raw.paths.push(path);
}
};
let mut collect = DgnCollector::default();
for e in run {
let kind = e[1] & 0x7f;
if e[1] & 0x80 != 0 || e.len() < 36 {
@@ -206,6 +210,13 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
}
let symbology = uint16(e, 34).unwrap_or(0);
let color = colors[(symbology >> 8) as usize];
let element = |geom: DgnGeom| DgnElement {
level: level(e) as u32,
color: Some(color),
weight: Some(((symbology >> 3) & 31) as u32),
style: Some((symbology & 7) as u32),
geom,
};
// One pixel wide, as V8 models draw: the element weight is a
// lineweight, not shown while lineweights are off.
let stroke = Some((color, -1.0));
@@ -213,8 +224,14 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let int_pt =
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
match kind {
// A text node: its count of text components.
7 => collect.open(
uint16(e, 38).unwrap_or(0) as usize,
element(DgnGeom::TextNode(Vec::new())),
),
12 | 14 => {
let n = uint16(e, 38).unwrap_or(0) as usize;
collect.open(n, element(DgnGeom::Chain(Vec::new())));
if n > 0 {
complex = Some((
n,
@@ -227,6 +244,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
}
3 => {
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
collect.push(element(DgnGeom::Line(a, b)));
push(
&mut raw,
&mut complex,
@@ -245,6 +263,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let n = uint16(e, 36).unwrap_or(0) as usize;
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
if pts.len() >= 2 {
collect.push(element(DgnGeom::Points(pts.clone())));
push(
&mut raw,
&mut complex,
@@ -290,6 +309,15 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let Some((a, b, r, ox, oy, start, sweep)) = arc else {
continue;
};
let turn = (r as f64 / 360000.0).to_radians();
let (c, s) = (turn.cos(), turn.sin());
collect.push(element(DgnGeom::Arc {
center: [ox, oy],
u: [a * c, a * s],
v: [-b * s, b * c],
start: start.to_radians(),
sweep: sweep.to_radians(),
}));
let pieces = arc_cubics(
[ox, oy],
a,
@@ -326,7 +354,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
continue;
};
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
raw.texts.push(Text {
let text = Text {
text: String::from_utf8_lossy(chars)
.trim_end_matches('\0')
.to_string(),
@@ -336,7 +364,16 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
rotation: (rotation as f64 / 360000.0).to_radians(),
color,
font: "txt".to_string(),
});
};
collect.push(element(DgnGeom::Text {
text: text.text.clone(),
origin,
height,
width_factor: text.width_factor,
rotation: text.rotation,
font: Some(e[36] as u32),
}));
raw.texts.push(text);
}
27 => {
// B-spline curve header: order, pole count.
@@ -369,6 +406,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
full.extend(interior);
full.extend(vec![1.0; order]);
let pts = bspline_points(order, &poles, &full, 24 * count);
collect.push(DgnElement {
color: Some(color),
..element(DgnGeom::Points(pts.clone()))
});
push(
&mut raw,
&mut complex,
@@ -399,6 +440,12 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
) else {
continue;
};
collect.push(element(DgnGeom::Cell {
name: name.clone(),
m: [m0, m1, m2, m3],
origin: [ox as f64, oy as f64],
z_scale: 1.0,
}));
raw.instances.push(Instance {
name,
m: [m0, m1, m2, m3],
@@ -408,6 +455,8 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
_ => {}
}
}
collect.close();
raw.elements = collect.elements;
raw
}
@@ -632,3 +681,67 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
})?;
Some(sheet)
}
/// A unit name of the design file header (two characters, zero padded).
fn unit_name(b: &[u8], at: usize) -> String {
b.get(at..at + 2)
.map(|n| String::from_utf8_lossy(n).trim_matches(char::from(0)).trim().to_string())
.unwrap_or_default()
}
/// The model element by element (DGNIMPORT): in master units with the
/// global origin at 0,0, shared cell definitions in their own units scaled
/// to master units, and every level by number with its name.
pub fn import(bytes: &[u8]) -> Option<DgnImport> {
if !is_v7(bytes) {
return None;
}
let u = units(bytes)?;
let all = elements(bytes);
let colors = color_table(&all).unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
let names = level_names(&all);
let per_unit = u.uor_per_sub * u.sub_per_master;
let s = 1.0 / per_unit;
let mut cells = Vec::new();
let mut top: Vec<&[u8]> = Vec::new();
let mut i = 0;
while i < all.len() {
let e = all[i];
if e[1] & 0x7f == 34 && e[0] & 0x80 == 0 {
let name = e
.get(164..180)
.map(|n| {
String::from_utf8_lossy(n)
.trim_end_matches('\0')
.trim()
.to_string()
})
.unwrap_or_default();
let mut j = i + 1;
while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1;
}
let def = read_run(&all[i + 1..j], &colors).elements;
cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
i = j;
continue;
}
top.push(e);
i += 1;
}
let raw = read_run(&top, &colors);
let levels = (0..64u32)
.map(|l| {
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
(l, (name, None))
})
.collect();
Some(DgnImport {
elements: scaled_elements(&raw.elements, s, [-u.origin[0] * s, -u.origin[1] * s]),
cells,
levels,
sub_per_master: u.sub_per_master,
unit_names: (unit_name(bytes, 1120), unit_name(bytes, 1122)),
v7: true,
})
}
+156 -8
View File
@@ -8,8 +8,8 @@ use std::collections::HashMap;
use std::io::Read;
use super::model::{
arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text,
DGN_DEFAULT_COLORS,
arc_cubics, dgn_color_table, paths_bounds, scaled_elements, DgnCollector, DgnElement, DgnGeom,
DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS,
};
// ── Compound file ───────────────────────────────────────────────────────────
@@ -392,6 +392,8 @@ struct Raw {
paths: Vec<Path>,
texts: Vec<Text>,
instances: Vec<Instance>,
/// The same run element by element, for DGNIMPORT.
elements: Vec<DgnElement>,
}
#[derive(Clone)]
@@ -551,11 +553,21 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
let q = xf_vector(xf, v);
[q[0], q[1]]
};
for e in run {
let mut collect = DgnCollector::default();
for (index, e) in run.iter().enumerate() {
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
continue;
}
let color = styles.color(e);
// Colour and weight words: 0xFFFFFFFF takes the level's; line style
// 0x7FFFFFFF (or 0xFFFFFFFF).
let element = |geom: DgnGeom| DgnElement {
level: u32_at(e, 12).unwrap_or(0),
color: (u32_at(e, 52) != Some(u32::MAX)).then_some(color),
weight: u32_at(e, 48).filter(|w| *w != u32::MAX).map(|w| w & 31),
style: u32_at(e, 44).filter(|s| *s != u32::MAX && *s != 0x7FFF_FFFF),
geom,
};
let stroke = Some((color, -1.0));
let d3 = is_3d(e);
// A point: x, y (and z in a 3D element).
@@ -567,8 +579,20 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
])
};
match kind(e) {
// A text node: grouped when its count is followed by that many
// text components.
7 => {
let n = u32_at(e, 104).unwrap_or(0) as usize;
let texts = run.get(index + 1..index + 1 + n).is_some_and(|parts| {
parts.iter().all(|p| p.len() >= 104 && kind(p) == 17 && is_component(p))
});
if texts {
collect.open(n, element(DgnGeom::TextNode(Vec::new())));
}
}
12 | 14 => {
let n = u32_at(e, 104).unwrap_or(0) as usize;
collect.open(n, element(DgnGeom::Chain(Vec::new())));
if n > 0 {
let fill = (kind(e) == 14
&& e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]))
@@ -578,6 +602,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
}
3 => {
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
collect.push(element(DgnGeom::Line(plan(a), plan(b))));
push(
&mut raw,
&mut complex,
@@ -600,6 +625,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
.map(plan)
.collect();
if pts.len() >= 2 {
collect.push(element(DgnGeom::Points(pts.clone())));
let mut pb = PathBuilder::default();
pb.move_to(pts[0]);
for p in &pts[1..] {
@@ -675,6 +701,16 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
sweep
};
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
{
let (pu, pv) = (axis(u), axis(v));
collect.push(element(DgnGeom::Arc {
center: plan(c),
u: [a * pu[0], a * pu[1]],
v: [b * pv[0], b * pv[1]],
start,
sweep,
}));
}
push(
&mut raw,
&mut complex,
@@ -730,11 +766,20 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
continue;
}
let (height, width) = (height * 6.0 / 1000.0 * sv, width * 6.0 / 1000.0 * su);
let width_factor = if height > 0.0 { width / height } else { 1.0 };
collect.push(element(DgnGeom::Text {
text: text.clone(),
origin: plan(origin),
height,
width_factor,
rotation: pu[1].atan2(pu[0]),
font: None,
}));
raw.texts.push(Text {
text,
origin: plan(origin),
height,
width_factor: if height > 0.0 { width / height } else { 1.0 },
width_factor,
rotation: pu[1].atan2(pu[0]),
color,
font: "txt".to_string(),
@@ -756,6 +801,12 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
[m[3], m[4], m[5], o[1]],
[m[6], m[7], m[8], o[2]],
];
collect.push(element(DgnGeom::Cell {
name: name.clone(),
m: [m[0], m[1], m[3], m[4]],
origin: [o[0], o[1]],
z_scale: m[8],
}));
raw.instances.push(Instance {
name: name.to_ascii_uppercase(),
xf: xf_compose(xf, &local),
@@ -775,6 +826,8 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
fill,
});
}
collect.close();
raw.elements = collect.elements;
raw
}
@@ -921,6 +974,17 @@ pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
/// name and colour index. The default level (64) is listed as "0", as the
/// reference names it.
fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
let mut out = level_table_raw(nm);
for (id, name, _) in &mut out {
if *id == 64 && name.eq_ignore_ascii_case("Default") {
*name = "0".to_string();
}
}
out
}
/// The level table with the file's own names.
fn level_table_raw(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
let mut out = Vec::new();
let mut in_levels = false;
for e in nm {
@@ -930,10 +994,7 @@ fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
continue;
};
let mut name = marked_string(e, 32).unwrap_or_default();
if id == 64 && name.eq_ignore_ascii_case("Default") {
name = "0".to_string();
}
let name = marked_string(e, 32).unwrap_or_default();
out.push((id, name, color));
}
_ => {}
@@ -1072,6 +1133,93 @@ fn stored_range(graphics: &[&[u8]]) -> Option<[f64; 4]> {
(r[0] <= r[2]).then_some(r)
}
/// The short name of a unit of `metres` per unit ("m", "mm", "ft" …), empty
/// for one without a common name.
fn unit_name(metres: f64) -> String {
const NAMES: [(f64, &str); 8] = [
(1.0, "m"),
(0.001, "mm"),
(0.01, "cm"),
(1000.0, "km"),
(0.0254, "in"),
(0.3048, "ft"),
(0.9144, "yd"),
(1609.344, "mi"),
];
NAMES
.iter()
.find(|(m, _)| (metres / m - 1.0).abs() < 1e-6)
.map(|(_, n)| n.to_string())
.unwrap_or_default()
}
/// The named model element by element (DGNIMPORT): in master units with
/// the global origin at 0,0, shared cell definitions in master units, and
/// every level by id with its own name and colour.
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
let cfb = Cfb::open(bytes)?;
let all = models(&cfb);
let (_, graphics, header) = all
.iter()
.find(|m| m.0.eq_ignore_ascii_case(name))
.or_else(|| all.first())?;
let non_model = non_model(&cfb);
let nm = elements(&non_model);
let colors = nm
.iter()
.find(|e| kind(e) == 5)
.and_then(|e| dgn_color_table(e, 37))
.unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
let table = level_table_raw(&nm);
let styles = Styles {
levels: table.iter().map(|l| (l.0, l.2)).collect(),
hidden: Default::default(),
extended: extended_colors(&cfb),
colors,
};
let levels = table
.iter()
.map(|(id, name, color)| {
let rgb = styles.colors.get(*color as usize).copied();
(*id, (name.clone(), rgb))
})
.collect();
let (per_unit, sub_uor) = units(header);
let s = 1.0 / per_unit;
let mut cells = Vec::new();
let mut i = 0;
while i < nm.len() {
if kind(nm[i]) == 34 && !is_component(nm[i]) {
let cell = marked_string(nm[i], 248).unwrap_or_default();
let mut j = i + 1;
while j < nm.len() && is_component(nm[j]) {
j += 1;
}
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
i = j;
} else {
i += 1;
}
}
let raw = read_run(&elements(graphics), &styles, &IDENTITY);
let origin = global_origin(header);
// Unit definitions: numerator / denominator per metre, master at 4180,
// sub at 4196.
let metres = |at: usize| match (f64_at(header, at), f64_at(header, at + 8)) {
(Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n),
_ => String::new(),
};
Some(DgnImport {
elements: scaled_elements(&raw.elements, s, [-origin[0] * s, -origin[1] * s]),
cells,
levels,
sub_per_master: per_unit / sub_uor,
unit_names: (metres(4180), metres(4196)),
v7: false,
})
}
#[cfg(test)]
mod cfb_memory_tests {
use super::*;
+12 -1
View File
@@ -20,10 +20,21 @@ use std::sync::{Arc, Mutex, OnceLock};
use codec::entities::UnderlayType;
pub use model::{Path, Segment, Sheet, SubPath};
pub use model::{scaled_elements, DgnElement, DgnGeom, DgnImport, Path, Segment, Sheet, SubPath};
use super::pdf_raster::{source_bytes, PdfPage};
/// A DGN model element by element, for DGNIMPORT; `None` when the file is
/// not a V7 or V8 design file.
pub fn dgn_import(path: &str, item: &str) -> Option<DgnImport> {
let bytes = source_bytes(path)?;
if dgn7::is_v7(&bytes) {
dgn7::import(&bytes)
} else {
dgn8::import(&bytes, item)
}
}
/// Sheet names (DWF) or model names (DGN) of a file.
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
let bytes = source_bytes(path)?;
+176
View File
@@ -532,6 +532,182 @@ pub fn dgn_color_table(e: &[u8], first: usize) -> Option<Vec<[u8; 3]>> {
Some(out)
}
/// A DGN element as drawing geometry (what DGNIMPORT converts): its level,
/// symbology and shape, in the units of the run that read it.
#[derive(Clone, Debug, PartialEq)]
pub struct DgnElement {
/// Level number (V7) or id (V8).
pub level: u32,
/// Colour; `None` takes the level's.
pub color: Option<[u8; 3]>,
/// Weight 0-31; `None` takes the level's.
pub weight: Option<u32>,
/// Line style; `None` takes the level's.
pub style: Option<u32>,
pub geom: DgnGeom,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DgnGeom {
Line([f64; 2], [f64; 2]),
/// A line string, curve or B-spline (sampled), or a shape (its points
/// end where they start).
Points(Vec<[f64; 2]>),
/// An elliptical arc: the point at parameter t is
/// `center + cos t · u + sin t · v`, from `start` over `sweep` (radians).
Arc {
center: [f64; 2],
u: [f64; 2],
v: [f64; 2],
start: f64,
sweep: f64,
},
/// A complex chain or shape: its components in order.
Chain(Vec<DgnGeom>),
Text {
text: String,
origin: [f64; 2],
height: f64,
width_factor: f64,
rotation: f64,
/// Font number (V7); `None` when the file's font is not read.
font: Option<u32>,
},
/// A text node: its lines.
TextNode(Vec<DgnGeom>),
/// A shared cell instance: x' = m[0]·x + m[1]·y + origin.x,
/// y' = m[2]·x + m[3]·y + origin.y.
Cell {
name: String,
m: [f64; 4],
origin: [f64; 2],
/// Scale along z (1 in a 2D file).
z_scale: f64,
},
}
impl DgnGeom {
/// The geometry through x' = s·x + t (lengths scaled by `s`).
pub fn scaled(&self, s: f64, t: [f64; 2]) -> DgnGeom {
let p = |q: [f64; 2]| [q[0] * s + t[0], q[1] * s + t[1]];
let v = |q: [f64; 2]| [q[0] * s, q[1] * s];
match self {
DgnGeom::Line(a, b) => DgnGeom::Line(p(*a), p(*b)),
DgnGeom::Points(pts) => DgnGeom::Points(pts.iter().map(|q| p(*q)).collect()),
DgnGeom::Arc {
center,
u,
v: w,
start,
sweep,
} => DgnGeom::Arc {
center: p(*center),
u: v(*u),
v: v(*w),
start: *start,
sweep: *sweep,
},
DgnGeom::Chain(parts) => DgnGeom::Chain(parts.iter().map(|g| g.scaled(s, t)).collect()),
DgnGeom::TextNode(parts) => {
DgnGeom::TextNode(parts.iter().map(|g| g.scaled(s, t)).collect())
}
DgnGeom::Text {
text,
origin,
height,
width_factor,
rotation,
font,
} => DgnGeom::Text {
text: text.clone(),
origin: p(*origin),
height: height * s,
width_factor: *width_factor,
rotation: *rotation,
font: *font,
},
DgnGeom::Cell {
name,
m,
origin,
z_scale,
} => DgnGeom::Cell {
name: name.clone(),
m: *m,
origin: p(*origin),
z_scale: *z_scale,
},
}
}
}
/// Collects elements in file order, gathering the components of a complex
/// chain, shape or text node into their header's element.
#[derive(Default)]
pub struct DgnCollector {
pub elements: Vec<DgnElement>,
/// The open complex element and how many components it still takes.
open: Option<(usize, DgnElement)>,
}
impl DgnCollector {
/// A complex chain or shape (or a text node) header with `count`
/// components.
pub fn open(&mut self, count: usize, header: DgnElement) {
self.close();
if count > 0 {
self.open = Some((count, header));
}
}
pub fn push(&mut self, element: DgnElement) {
if let Some((left, header)) = self.open.as_mut() {
if let DgnGeom::Chain(parts) | DgnGeom::TextNode(parts) = &mut header.geom {
parts.push(element.geom);
}
*left -= 1;
if *left == 0 {
self.close();
}
return;
}
self.elements.push(element);
}
/// Ends a complex element cut short with the components it has.
pub fn close(&mut self) {
if let Some((_, header)) = self.open.take() {
self.elements.push(header);
}
}
}
/// What DGNIMPORT reads from a model: its elements and the shared cells
/// they place (in master units, the global origin at 0,0), the levels by
/// number or id (name, colour) and the sub units per master unit.
#[derive(Clone, Debug, Default)]
pub struct DgnImport {
pub elements: Vec<DgnElement>,
pub cells: Vec<(String, Vec<DgnElement>)>,
pub levels: std::collections::HashMap<u32, (String, Option<[u8; 3]>)>,
pub sub_per_master: f64,
/// Master and sub unit names, when the file has them.
pub unit_names: (String, String),
/// A V7 file (its elements carry their own line style and weight).
pub v7: bool,
}
/// The elements through x' = s·x + t.
pub fn scaled_elements(elements: &[DgnElement], s: f64, t: [f64; 2]) -> Vec<DgnElement> {
elements
.iter()
.map(|e| DgnElement {
geom: e.geom.scaled(s, t),
..e.clone()
})
.collect()
}
/// A line of text the host draws with its own font.
#[derive(Clone, Debug, PartialEq)]
pub struct Text {