DGNIMPORT: V7 3D files, V7 cells, filled shapes as solid hatches

A 3D V7 design file's points keep their z, ellipses, arcs and text take
their plane from the element's quaternion, and shared cell instances
their 3x3 transformation and z. A V7 cell (type 2) brings its components
in as a block of its own placed at the origin, as a V8 one does. A
filled shape (V7 or V8) comes in as its outline followed by a solid
hatch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 21:20:47 +03:00
1 parent 8f20f0aef6
commit 952cc0326d
4 files changed
+203 -27

No files matched your search

+21
View File
@@ -213,6 +213,22 @@ fn polyline(points: &[P3], bulges: &[f64]) -> EntityType {
EntityType::LwPolyline(pl)
}
/// A solid hatch filling a closed outline, in its plane.
fn solid_hatch(points: &[P3]) -> EntityType {
use codec::entities::{BoundaryEdge, BoundaryPath, Hatch, PolylineEdge};
let mut ring: Vec<Vector2> = points.iter().map(|p| Vector2::new(p[0], p[1])).collect();
if ring.len() > 1 && ring.first() == ring.last() {
ring.pop();
}
let mut hatch = Hatch::solid();
let mut boundary = BoundaryPath::external();
boundary.flags.set_polyline(true);
boundary.edges.push(BoundaryEdge::Polyline(PolylineEdge::new(ring, true)));
hatch.add_path(boundary);
hatch.elevation = points.first().map_or(0.0, |p| p[2]);
EntityType::Hatch(Box::new(hatch))
}
/// A complex chain's vertices and bulges: lines and line strings add their
/// points, circular arcs a bulge, other arcs their points.
fn chain(parts: &[DgnGeom]) -> (Vec<P3>, Vec<f64>) {
@@ -712,6 +728,10 @@ impl<'a> Converter<'a> {
vec![EntityType::Point(Point::from_coords(a[0], a[1], a[2]))]
}
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(v3(*a), v3(*b)))],
// A filled shape is its outline and a solid hatch inside it.
DgnGeom::Points(pts) if pts.len() >= 3 && element.filled => {
vec![polyline(pts, &[]), solid_hatch(pts)]
}
DgnGeom::Points(pts) if pts.len() >= 2 => vec![polyline(pts, &[])],
DgnGeom::Points(_) => Vec::new(),
DgnGeom::Arc {
@@ -880,6 +900,7 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
weight: By::Level,
style: By::Level,
geom: DgnGeom::Points(Vec::new()),
filled: false,
};
c.layer(&element, false);
}
+179 -27
View File
@@ -49,6 +49,42 @@ pub fn is_v7(bytes: &[u8]) -> bool {
bytes.len() > 1256 && (bytes[1] & 0x7f) == 9 && (bytes[0] & 0x3f) == 8
}
/// A 3D design file: the header element's 3D flag. Its points have a z,
/// and planar elements carry their plane as a quaternion.
fn is_3d(bytes: &[u8]) -> bool {
bytes[0] & 0x40 != 0
}
/// The rows of the rotation a 3D element's quaternion (w, x, y, z as
/// signed 1.31 fractions) stands for: the element's own x, y and z axes.
fn quaternion_axes(e: &[u8], at: usize) -> Option<[[f64; 3]; 3]> {
let q = |k: usize| int32(e, at + 4 * k).map(|v| v as f64 / 2_147_483_648.0);
let (w, x, y, z) = (q(0)?, q(1)?, q(2)?, q(3)?);
// The rotation matrix's columns are the axes.
let m = [
[x * x - y * y - z * z + w * w, 2.0 * (z * w + x * y), 2.0 * (x * z - y * w)],
[2.0 * (x * y - z * w), -x * x + y * y - z * z + w * w, 2.0 * (x * w + y * z)],
[2.0 * (x * z + y * w), 2.0 * (y * z - x * w), -x * x - y * y + z * z + w * w],
];
Some([0, 1, 2].map(|c| [m[0][c], m[1][c], m[2][c]]))
}
/// A cell name in Radix-50: two words of three characters each.
fn radix50(e: &[u8], at: usize) -> String {
const SET: &[u8] = b" ABCDEFGHIJKLMNOPQRSTUVWXYZ$.?0123456789";
let mut name = String::new();
for k in 0..2 {
let Some(mut word) = uint16(e, at + 2 * k).map(usize::from) else { break };
let mut chars = [0u8; 3];
for c in chars.iter_mut().rev() {
*c = SET.get(word % 40).copied().unwrap_or(b' ');
word /= 40;
}
name.extend(chars.iter().map(|c| char::from(*c)));
}
name.trim().to_string()
}
struct Units {
uor_per_sub: f64,
sub_per_master: f64,
@@ -137,8 +173,10 @@ struct Instance {
}
/// Reads a run of elements into raw geometry: plain elements, complex
/// chains and shapes (joined), B-splines, text and shared cell instances.
fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
/// chains and shapes (joined), B-splines, text, cells and shared cell
/// instances. In a 3D file (`d3`) points have a z and planar elements their
/// plane; the drawn geometry is their plan.
fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw {
let mut raw = Raw::default();
// An open B-spline curve: order, pole count, colour, closed, and the
// header as the element it imports as.
@@ -205,7 +243,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
}
};
let mut collect = DgnCollector::default();
for e in run {
let mut index = 0;
while index < run.len() {
let e = run[index];
index += 1;
let kind = e[1] & 0x7f;
if e[1] & 0x80 != 0 || e.len() < 36 {
continue;
@@ -222,22 +263,46 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
weight: By::Own(((symbology >> 3) & 31) as u32),
style: By::Own((symbology & 7) as u32),
geom,
filled: fill_index(e).is_some(),
};
let flat = |p: [f64; 2]| [p[0], p[1], 0.0];
// 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));
let fill = fill_index(e).map(|i| colors[i as usize]);
let int_pt =
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
// A point as the file holds it: two whole UORs, or three in 3D.
let pt3 = |i: usize| -> Option<[f64; 3]> {
Some([
int32(e, i)? as f64,
int32(e, i + 4)? as f64,
if d3 { int32(e, i + 8)? as f64 } else { 0.0 },
])
};
let plan = |p: [f64; 3]| [p[0], p[1]];
let step = if d3 { 12 } else { 8 };
match kind {
// A cell: its components follow as complex elements; they are
// already placed, so the cell keeps them as they are.
2 => {
let start = index;
while index < run.len() && run[index][0] & 0x80 != 0 {
index += 1;
}
let inner = read_run(&run[start..index], colors, d3);
raw.paths.extend(inner.paths);
raw.texts.extend(inner.texts);
raw.instances.extend(inner.instances);
collect.push(element(DgnGeom::CellGroup {
name: radix50(e, 38),
parts: inner.elements,
}));
}
// A text node: its count of text components, justification
// (45), line spacing (46) and origin (62).
// (45), line spacing (46) and origin (62, or 74 in 3D).
7 => collect.open(
uint16(e, 38).unwrap_or(0) as usize,
element(DgnGeom::TextNode {
lines: Vec::new(),
origin: int_pt(62).map(flat),
origin: pt3(if d3 { 74 } else { 62 }),
just: e.get(45).copied().unwrap_or(0),
line_spacing: int32(e, 46).map(|v| v as f64),
}),
@@ -256,8 +321,9 @@ 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(flat(a), flat(b))));
if let (Some(a3), Some(b3)) = (pt3(36), pt3(36 + step)) {
let (a, b) = (plan(a3), plan(b3));
collect.push(element(DgnGeom::Line(a3, b3)));
push(
&mut raw,
&mut complex,
@@ -274,9 +340,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
}
4 | 6 | 11 => {
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();
let pts3: Vec<[f64; 3]> = (0..n).filter_map(|k| pt3(38 + k * step)).collect();
let pts: Vec<[f64; 2]> = pts3.iter().map(|p| plan(*p)).collect();
if pts.len() >= 2 {
collect.push(element(DgnGeom::Points(pts.iter().map(|p| flat(*p)).collect())));
collect.push(element(DgnGeom::Points(pts3)));
push(
&mut raw,
&mut complex,
@@ -284,6 +351,69 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
);
}
}
// 3D ellipses and arcs: the axes from the quaternion (52 or
// 60), the centre as three doubles after it.
15 | 16 if d3 => {
let at = if kind == 15 { 36 } else { 44 };
let q = at + 16;
let (Some(a), Some(b), Some(axes), Some(cx), Some(cy), Some(cz)) = (
vax_double(e, at),
vax_double(e, at + 8),
quaternion_axes(e, q),
vax_double(e, q + 16),
vax_double(e, q + 24),
vax_double(e, q + 32),
) else {
continue;
};
let (start, sweep) = if kind == 15 {
(0.0, 360.0)
} else {
let (Some(st), Some(sw)) = (int32(e, 36), int32(e, 40)) else { continue };
let sw = sw as u32;
let mut sweep = if sw & 0x8000_0000 != 0 {
-((sw & 0x7fff_ffff) as f64)
} else {
sw as f64
} / 360000.0;
if sweep == 0.0 {
sweep = 360.0;
}
(st as f64 / 360000.0, sweep)
};
let [x, y, _] = axes;
collect.push(element(DgnGeom::Arc {
center: [cx, cy, cz],
u: x.map(|c| c * a),
v: y.map(|c| c * b),
start: start.to_radians(),
sweep: sweep.to_radians(),
}));
let pieces = arc_cubics(
[cx, cy],
a,
b,
x[1].atan2(x[0]),
start.to_radians(),
sweep.to_radians(),
);
let closed = sweep.abs() >= 360.0;
push(
&mut raw,
&mut complex,
Path {
subpaths: vec![SubPath {
segments: pieces
.into_iter()
.map(|[a, b, c, d]| Segment::Cubic(a, b, c, d))
.collect(),
closed,
}],
stroke,
fill: if closed { fill } else { None },
},
);
}
15 | 16 => {
let arc = if kind == 15 {
(|| {
@@ -357,13 +487,23 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
);
}
17 => {
let (Some(width), Some(height), Some(rotation), Some(origin)) =
(int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50))
// 2D: rotation (46), origin (50), count (58), characters
// (60); 3D: quaternion (46), origin (62), count (74),
// characters (76).
let rotation = if d3 {
quaternion_axes(e, 46).map(|[x, _, _]| (x[1].atan2(x[0]).to_degrees() * 360000.0).round() as i32)
} else {
int32(e, 46)
};
let (origin_at, count_at) = if d3 { (62, 74) } else { (50, 58) };
let (Some(width), Some(height), Some(rotation), Some(origin3)) =
(int32(e, 38), int32(e, 42), rotation, pt3(origin_at))
else {
continue;
};
let n = e.get(58).copied().unwrap_or(0) as usize;
let Some(chars) = e.get(60..60 + n) else {
let origin = plan(origin3);
let n = e.get(count_at).copied().unwrap_or(0) as usize;
let Some(chars) = e.get(count_at + 2..count_at + 2 + n) else {
continue;
};
// Sizes in whole UORs (the multipliers are 1000/6 of one).
@@ -386,7 +526,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
// characters times their width.
collect.push(element(DgnGeom::Text {
text: text.text.clone(),
origin: flat(origin),
origin: origin3,
height,
width_factor: text.width_factor,
length: text.text.chars().count() as f64 * width,
@@ -418,7 +558,8 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
}
21 => {
let n = uint16(e, 36).unwrap_or(0) as usize;
let poles: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
let poles3: Vec<[f64; 3]> = (0..n).filter_map(|k| pt3(38 + k * step)).collect();
let poles: Vec<[f64; 2]> = poles3.iter().map(|p| plan(*p)).collect();
if let Some((order, count, color, closed, header)) = spline.take() {
if poles.len() == count && count >= order {
let stored: Vec<f64> = knots
@@ -441,7 +582,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
geom: DgnGeom::Spline {
degree: order - 1,
knots: full,
poles: poles.iter().map(|p| flat(*p)).collect(),
poles: poles3.clone(),
closed,
},
..header
@@ -466,21 +607,32 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
.to_string()
})
.unwrap_or_default();
// A 3D instance holds a 3×3 matrix (its plan part and the z
// scale are used) and a z.
let (r1, zz) = if d3 { (100, 140) } else { (92, 0) };
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) = (
vax_double(e, 76),
vax_double(e, 84),
vax_double(e, 92),
vax_double(e, 100),
vax_double(e, r1),
vax_double(e, r1 + 8),
le_i32(e, 148),
le_i32(e, 152),
) else {
continue;
};
let (oz, z_scale) = if d3 {
(
le_i32(e, 156).unwrap_or(0) as f64,
vax_double(e, zz).filter(|z| *z != 0.0).unwrap_or(1.0),
)
} else {
(0.0, 1.0)
};
collect.push(element(DgnGeom::Cell {
name: name.clone(),
m: [m0, m1, m2, m3],
origin: [ox as f64, oy as f64, 0.0],
z_scale: 1.0,
origin: [ox as f64, oy as f64, oz],
z_scale,
}));
raw.instances.push(Instance {
name,
@@ -683,14 +835,14 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1;
}
defs.insert(name, read_run(&all[i + 1..j], &colors));
defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes)));
i = j;
continue;
}
top.push(e);
i += 1;
}
let raw = read_run(&top, &colors);
let raw = read_run(&top, &colors, is_3d(bytes));
let per_unit = u.uor_per_sub * u.sub_per_master;
let m: Affine = [
1.0 / per_unit,
@@ -757,7 +909,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1;
}
let def = read_run(&all[i + 1..j], &colors).elements;
let def = read_run(&all[i + 1..j], &colors, is_3d(bytes)).elements;
cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
i = j;
continue;
@@ -765,7 +917,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
top.push(e);
i += 1;
}
let raw = read_run(&top, &colors);
let raw = read_run(&top, &colors, is_3d(bytes));
let levels = (0..64u32)
.map(|l| {
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
+1
View File
@@ -673,6 +673,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
Some(s) => By::Own(s),
},
geom,
filled: matches!(kind(e), 6 | 14) && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]),
};
let stroke = Some((color, -1.0));
let d3 = is_3d(e);
+2
View File
@@ -572,6 +572,8 @@ pub struct DgnElement {
/// Line style.
pub style: By<u32>,
pub geom: DgnGeom,
/// The element is a filled shape (it has a fill linkage).
pub filled: bool,
}
/// A point of a DGN element (z 0 in a 2D file).