forked from 1b-agent/opencadstudio-upstream
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:
1 parent
8f20f0aef6
commit
952cc0326d
4 files changed
+203
-27
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user