feat(render): draw DGN line-style symbols on linetypes

Linetypes converted from MicroStation DGN keep their pattern in DGN line-style
objects (empty standard LTYPE), so entities using them drew as a plain line and
lost the symbols (e.g. a pipe's end circles). Walk the line-style tree
(resolved by name to its LSDefinition), collect the symbol components' anonymous
blocks, and draw each at the host polyline's endpoints, scaled by the symbol's
decoded scale (geometry / scale) so it matches the drawing (verified: a 0.75
ellipse renders at the file's intended 0.375 radius).

Bumps acadrust to 8e18c6b (DGN line-style symbol-scale decode). First pass: the
stroke dash pattern and exact per-vertex placement need more of the line-style
leaf decoded; the base line still draws solid.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-16 21:48:06 +03:00
1 parent 7647fb427c
commit 1d6a04183e
4 files changed
+486 -288

No files matched your search

Generated
+299 -288
View File
File diff suppressed because it is too large. Load diff
+156
View File
@@ -0,0 +1,156 @@
//! DGN line-style rendering (first pass).
//!
//! Linetypes converted from MicroStation DGN store their real pattern as DGN
//! line-style objects (`AcDbLS*`), not standard `LTYPE` dashes — the standard
//! table entry is empty, so acadrust exposes the structure in
//! [`CadDocument::dgn_ls_definitions`] / `dgn_ls_components` instead. See
//! `objects/dgn_linestyle.rs` in acadrust and `~/Documents/OCS/DGN_LINESTYLE_PLAN.md`.
//!
//! The visible content is the **symbol components**, each of which references an
//! anonymous block (e.g. a pipe's end circle). This renders those blocks at the
//! host polyline's endpoints. The exact placement / scale / dash pattern live in
//! the components' leaf data-stream fields, which are not decoded yet, so this is
//! an approximation: symbols at native scale on the first and last vertices.
use acadrust::objects::DgnLsComponentType;
use acadrust::types::{Handle, Transform, Vector3};
use acadrust::{CadDocument, EntityType};
use std::collections::HashSet;
use crate::scene::model::wire_model::WireModel;
/// A symbol placement in a linetype's DGN line-style tree: the anonymous block
/// to draw and the scale divisor to draw it at (`geometry / scale`).
pub struct DgnSymbol {
pub block: Handle,
pub scale: f64,
}
/// Symbol placements referenced by a linetype's DGN line-style tree, in tree
/// order. Empty when the linetype is not a DGN line style.
pub fn symbol_blocks(doc: &CadDocument, lt_name: &str) -> Vec<DgnSymbol> {
let Some(def) = doc
.dgn_ls_definitions
.values()
.find(|d| d.name.eq_ignore_ascii_case(lt_name))
else {
return Vec::new();
};
let mut out = Vec::new();
let mut seen = HashSet::new();
walk(doc, def.root_component, &mut out, &mut seen);
out
}
fn walk(doc: &CadDocument, h: Handle, out: &mut Vec<DgnSymbol>, seen: &mut HashSet<Handle>) {
if !seen.insert(h) {
return;
}
let Some(c) = doc.dgn_ls_components.get(&h) else {
return;
};
match c.component_type {
DgnLsComponentType::Compound | DgnLsComponentType::Point => {
for r in &c.refs {
let Some(sub) = doc.dgn_ls_components.get(r) else {
continue;
};
if sub.component_type == DgnLsComponentType::Symbol {
if let Some(block) = sub.symbol_block() {
if !out.iter().any(|s| s.block == block) {
out.push(DgnSymbol {
block,
scale: sub.scale,
});
}
}
} else {
walk(doc, *r, out, seen);
}
}
}
_ => {}
}
}
/// Host entity's polyline vertices in WCS f64 (consecutive duplicates dropped).
pub fn polyline_points(e: &EntityType) -> Vec<[f64; 3]> {
let mut v: Vec<[f64; 3]> = match e {
EntityType::LwPolyline(p) => p
.vertices
.iter()
.map(|w| [w.location.x, w.location.y, 0.0])
.collect(),
EntityType::Polyline2D(p) => p
.vertices
.iter()
.map(|w| [w.location.x, w.location.y, 0.0])
.collect(),
EntityType::Line(l) => vec![
[l.start.x, l.start.y, l.start.z],
[l.end.x, l.end.y, l.end.z],
],
_ => Vec::new(),
};
v.dedup();
v
}
/// Tessellate a symbol block's entities, translated so the block origin lands at
/// `at`, in the host entity's colour. Reuses the normal entity tessellator on
/// translated clones — the symbol geometry (ellipses, lines, …) renders exactly
/// as it would anywhere else.
#[allow(clippy::too_many_arguments)]
pub fn place_block_wires(
doc: &CadDocument,
block: Handle,
scale_divisor: f64,
at: [f64; 3],
color: [f32; 4],
line_weight_px: f32,
anno_scale: f32,
world_per_pixel: Option<f32>,
bg_color: [f32; 4],
) -> Vec<WireModel> {
let Some(br) = doc.block_records.iter().find(|b| b.handle == block) else {
return Vec::new();
};
// The symbol block's native geometry is drawn at 1 / scale_divisor (the
// divisor is read from the symbol component's leaf data). Scale about the
// origin, then translate the (scaled) base point to the placement point.
let s = if scale_divisor.abs() > 1e-9 {
1.0 / scale_divisor
} else {
1.0
};
let scale = Transform::from_scale(s);
let offset = Vector3::new(
at[0] - br.base_point.x * s,
at[1] - br.base_point.y * s,
at[2] - br.base_point.z * s,
);
let mut out = Vec::new();
for eh in &br.entity_handles {
let Some(ent) = doc.get_entity(*eh) else {
continue;
};
let mut clone = ent.clone();
clone.as_entity_mut().apply_transform(&scale);
clone.as_entity_mut().translate(offset);
out.extend(super::tessellate::tessellate(
doc,
*eh,
&clone,
false,
color,
0.0,
[0.0; 8],
line_weight_px,
anno_scale,
world_per_pixel,
bg_color,
false,
));
}
out
}
+1
View File
@@ -18,6 +18,7 @@ pub mod acis_to_truck {
None
}
}
pub mod dgn_linestyle;
pub mod truck_tess;
pub mod tessellate;
pub(crate) mod tess;
+30
View File
@@ -702,6 +702,36 @@ pub(crate) fn tessellate_entity(
}
}
// DGN line-style: the linetype's real pattern lives in DGN line-style objects
// (empty standard LTYPE), so `resolve_complex_lt` sees nothing. Render its
// symbol blocks (e.g. a pipe's end circles) at the polyline endpoints. First
// pass — exact dash pattern / placement need the undecoded leaf data.
let dgn_syms = convert::dgn_linestyle::symbol_blocks(document, lt_name);
if !dgn_syms.is_empty() {
let verts = convert::dgn_linestyle::polyline_points(e);
if verts.len() >= 2 {
let last = *verts.last().unwrap();
for (i, sym) in dgn_syms.iter().enumerate() {
let at = if i == 0 { verts[0] } else { last };
let mut wires = convert::dgn_linestyle::place_block_wires(
document,
sym.block,
sym.scale,
at,
entity_color,
line_weight_px,
anno_scale,
world_per_pixel,
bg_color,
);
for w in &mut wires {
w.aabb = aabb;
}
bases.extend(wires);
}
}
}
bases
}