From 952cc0326db33cff250d37181f2cb84903fac747 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:20:47 +0300 Subject: [PATCH] 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 --- src/modules/insert/dgn_import.rs | 21 +++ src/scene/model/underlay_vector/dgn7.rs | 206 ++++++++++++++++++++--- src/scene/model/underlay_vector/dgn8.rs | 1 + src/scene/model/underlay_vector/model.rs | 2 + 4 files changed, 203 insertions(+), 27 deletions(-) diff --git a/src/modules/insert/dgn_import.rs b/src/modules/insert/dgn_import.rs index f93da2733..2135d82dd 100644 --- a/src/modules/insert/dgn_import.rs +++ b/src/modules/insert/dgn_import.rs @@ -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 = 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, Vec) { @@ -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); } diff --git a/src/scene/model/underlay_vector/dgn7.rs b/src/scene/model/underlay_vector/dgn7.rs index dcbb0345b..a72343bf9 100644 --- a/src/scene/model/underlay_vector/dgn7.rs +++ b/src/scene/model/underlay_vector/dgn7.rs @@ -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 = 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 { 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 { 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 { 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()); diff --git a/src/scene/model/underlay_vector/dgn8.rs b/src/scene/model/underlay_vector/dgn8.rs index 413edb60b..730fe07ba 100644 --- a/src/scene/model/underlay_vector/dgn8.rs +++ b/src/scene/model/underlay_vector/dgn8.rs @@ -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); diff --git a/src/scene/model/underlay_vector/model.rs b/src/scene/model/underlay_vector/model.rs index 355d2759d..c7b526179 100644 --- a/src/scene/model/underlay_vector/model.rs +++ b/src/scene/model/underlay_vector/model.rs @@ -572,6 +572,8 @@ pub struct DgnElement { /// Line style. pub style: By, 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).