forked from 1b-agent/opencadstudio-upstream
feat(dgn-ls): render DET pipe as dashed double walls
DGN line-style linetypes (empty standard LTYPE) drew only end symbols on a single centre line. Render the host polyline as two parallel walls offset by ±(symbol radius = block extent / scale) so each wall sits tangent to the end circles, and dash them with the line style's stroke pattern. The stroke dash lengths live in the component leaf (byte-aligned big-endian f64 after the shared class GUID). `wall_dashes` reads the native dash of the root compound's dash stroke ([5.25, 7.125] for DET); `stroke_dashes` decodes the leaf. The native lengths scale to drawing units by f = radius / symbol-scale (0.1875) — the same factor that maps the compound's native offset (2.0) onto the measured wall offset (0.375), which also lands the dash at ~1.0 drawing unit. Walls dash through the normal WireModel pattern (shader rebuilds each segment relative-to-eye, so it stays precise at UTM coordinates). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
1d6a04183e
commit
765e6606e2
2 files changed
+234
No files matched your search
@@ -73,6 +73,174 @@ fn walk(doc: &CadDocument, h: Handle, out: &mut Vec<DgnSymbol>, seen: &mut HashS
|
||||
}
|
||||
}
|
||||
|
||||
/// Native dash lengths in a stroke component's leaf. The DGN line-style leaf is
|
||||
/// byte-aligned big-endian f64 (MicroStation origin, not DWG bit-codes); the
|
||||
/// stroke's dash/gap values sit in an 8-byte-aligned run after the shared
|
||||
/// 16-byte class GUID (whose fixed tail is `ae da 14`). Values outside a sane
|
||||
/// size band are skipped (denormals / stream-boundary bytes). Empty when the
|
||||
/// object carries no raw snapshot or no plausible lengths.
|
||||
fn stroke_dashes(doc: &CadDocument, h: Handle) -> Vec<f64> {
|
||||
use acadrust::objects::ObjectType;
|
||||
let Some(ObjectType::Unknown {
|
||||
raw_dwg_data: Some(d),
|
||||
..
|
||||
}) = doc.objects.get(&h)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(gi) = d.windows(3).position(|w| w == [0xae, 0xda, 0x14]) else {
|
||||
return Vec::new();
|
||||
};
|
||||
// GUID tail (3) + version/type bytes (3) → the first length field.
|
||||
let start = gi + 6;
|
||||
let mut out = Vec::new();
|
||||
let mut i = start;
|
||||
while i + 8 <= d.len() {
|
||||
let v = f64::from_be_bytes(d[i..i + 8].try_into().unwrap());
|
||||
if v.is_finite() && v.abs() >= 0.01 && v.abs() < 1.0e4 {
|
||||
out.push(v);
|
||||
}
|
||||
i += 8;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Native dash pattern of a DGN line style's pipe walls: the dash lengths of the
|
||||
/// first stroke that is a **direct** child of the root compound and carries at
|
||||
/// least two values (a dash + a gap). The base stroke a point component sits on
|
||||
/// (a single long length) is intentionally skipped — it is the solid placement
|
||||
/// guide, not the visible dash. Empty for a solid style.
|
||||
pub fn wall_dashes(doc: &CadDocument, lt_name: &str) -> Vec<f64> {
|
||||
let Some(def) = doc
|
||||
.dgn_ls_definitions
|
||||
.values()
|
||||
.find(|d| d.name.eq_ignore_ascii_case(lt_name))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(root) = doc.dgn_ls_components.get(&def.root_component) else {
|
||||
return Vec::new();
|
||||
};
|
||||
for r in &root.refs {
|
||||
if doc.dgn_ls_components.get(r).map(|c| c.component_type)
|
||||
== Some(DgnLsComponentType::Stroke)
|
||||
{
|
||||
let dashes = stroke_dashes(doc, *r);
|
||||
if dashes.len() >= 2 {
|
||||
return dashes;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Rendered half-width of a symbol block: its geometry's largest extent from
|
||||
/// the block base point, divided by the symbol scale (the same divisor
|
||||
/// [`place_block_wires`] draws it at). For a pipe end-circle this is the circle
|
||||
/// radius, which is also the offset of the two pipe walls (they sit tangent to
|
||||
/// the end circles) — so it doubles as the rail offset for the double line.
|
||||
pub fn symbol_radius(doc: &CadDocument, block: Handle, scale: f64) -> f64 {
|
||||
let Some(br) = doc.block_records.iter().find(|b| b.handle == block) else {
|
||||
return 0.0;
|
||||
};
|
||||
let bx = br.base_point.x;
|
||||
let by = br.base_point.y;
|
||||
let d = |x: f64, y: f64| ((x - bx).powi(2) + (y - by).powi(2)).sqrt();
|
||||
let mut r = 0.0_f64;
|
||||
for eh in &br.entity_handles {
|
||||
let Some(e) = doc.get_entity(*eh) else {
|
||||
continue;
|
||||
};
|
||||
let ext = match e {
|
||||
EntityType::Ellipse(el) => {
|
||||
let a = (el.major_axis.x.powi(2) + el.major_axis.y.powi(2)).sqrt();
|
||||
d(el.center.x, el.center.y) + a
|
||||
}
|
||||
EntityType::Circle(c) => d(c.center.x, c.center.y) + c.radius,
|
||||
EntityType::Arc(a) => d(a.center.x, a.center.y) + a.radius,
|
||||
EntityType::Line(l) => d(l.start.x, l.start.y).max(d(l.end.x, l.end.y)),
|
||||
_ => 0.0,
|
||||
};
|
||||
r = r.max(ext);
|
||||
}
|
||||
let s = if scale.abs() > 1e-9 { scale } else { 1.0 };
|
||||
r / s
|
||||
}
|
||||
|
||||
/// Per-vertex left-normal offset of an XY polyline by `d` (signed). Uses the
|
||||
/// averaged adjacent-segment normal at interior vertices — good for the gentle
|
||||
/// bends of a pipe run; sharp corners are not mitred.
|
||||
fn offset_xy(pts: &[[f64; 3]], d: f64) -> Vec<[f64; 2]> {
|
||||
let n = pts.len();
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let (ax, ay) = if i == 0 {
|
||||
(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1])
|
||||
} else {
|
||||
(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1])
|
||||
};
|
||||
let (bx, by) = if i + 1 < n {
|
||||
(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1])
|
||||
} else {
|
||||
(ax, ay)
|
||||
};
|
||||
let na = (ax * ax + ay * ay).sqrt().max(1e-12);
|
||||
let nb = (bx * bx + by * by).sqrt().max(1e-12);
|
||||
let tx = ax / na + bx / nb;
|
||||
let ty = ay / na + by / nb;
|
||||
let tn = (tx * tx + ty * ty).sqrt().max(1e-12);
|
||||
// Left normal of the averaged tangent.
|
||||
out.push([pts[i][0] - d * (ty / tn), pts[i][1] + d * (tx / tn)]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Clone the host polyline with every vertex offset perpendicular by `d` — one
|
||||
/// wall of the pipe. Returns `None` for entity kinds without an XY vertex list.
|
||||
pub fn offset_host_entity(e: &EntityType, d: f64) -> Option<EntityType> {
|
||||
let mut clone = e.clone();
|
||||
match &mut clone {
|
||||
EntityType::LwPolyline(p) => {
|
||||
if p.vertices.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let pts: Vec<[f64; 3]> = p
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| [v.location.x, v.location.y, 0.0])
|
||||
.collect();
|
||||
for (v, o) in p.vertices.iter_mut().zip(offset_xy(&pts, d)) {
|
||||
v.location.x = o[0];
|
||||
v.location.y = o[1];
|
||||
}
|
||||
}
|
||||
EntityType::Polyline2D(p) => {
|
||||
if p.vertices.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let pts: Vec<[f64; 3]> = p
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| [v.location.x, v.location.y, 0.0])
|
||||
.collect();
|
||||
for (v, o) in p.vertices.iter_mut().zip(offset_xy(&pts, d)) {
|
||||
v.location.x = o[0];
|
||||
v.location.y = o[1];
|
||||
}
|
||||
}
|
||||
EntityType::Line(l) => {
|
||||
let pts = [[l.start.x, l.start.y, 0.0], [l.end.x, l.end.y, 0.0]];
|
||||
let o = offset_xy(&pts, d);
|
||||
l.start.x = o[0][0];
|
||||
l.start.y = o[0][1];
|
||||
l.end.x = o[1][0];
|
||||
l.end.y = o[1][1];
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(clone)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -710,6 +710,72 @@ pub(crate) fn tessellate_entity(
|
||||
if !dgn_syms.is_empty() {
|
||||
let verts = convert::dgn_linestyle::polyline_points(e);
|
||||
if verts.len() >= 2 {
|
||||
// The pipe body is drawn as two parallel walls, not a single centre
|
||||
// line: offset the host polyline by ±(symbol radius) so each wall
|
||||
// sits tangent to the end circles, and replace the centre line with
|
||||
// them. The radius is the rendered symbol extent (block / scale).
|
||||
let radius = dgn_syms
|
||||
.iter()
|
||||
.map(|s| convert::dgn_linestyle::symbol_radius(document, s.block, s.scale))
|
||||
.fold(0.0_f64, f64::max);
|
||||
if radius > 1e-6 {
|
||||
// The walls carry the line style's dash pattern. Its native
|
||||
// lengths scale to drawing units by f = radius / symbol-scale
|
||||
// (the same factor that turns the compound's native offset into
|
||||
// the measured wall offset). Sign-alternate: dash, gap, dash…
|
||||
let scale = dgn_syms
|
||||
.iter()
|
||||
.map(|s| s.scale)
|
||||
.find(|s| *s > 1e-9)
|
||||
.unwrap_or(1.0);
|
||||
let f = radius / scale;
|
||||
let native = convert::dgn_linestyle::wall_dashes(document, lt_name);
|
||||
let (wall_pat, wall_pat_len) = if f > 1e-9 && native.len() >= 2 {
|
||||
let mut pat = [0.0_f32; 8];
|
||||
let mut plen = 0.0_f32;
|
||||
for (i, &d) in native.iter().take(8).enumerate() {
|
||||
let v = (d * f) as f32;
|
||||
pat[i] = if i % 2 == 0 { v } else { -v };
|
||||
plen += v;
|
||||
}
|
||||
(pat, plen)
|
||||
} else {
|
||||
([0.0_f32; 8], 0.0)
|
||||
};
|
||||
let mut rails = Vec::new();
|
||||
for sgn in [1.0_f64, -1.0] {
|
||||
if let Some(off) =
|
||||
convert::dgn_linestyle::offset_host_entity(e, sgn * radius)
|
||||
{
|
||||
let mut w = convert::tessellate::tessellate(
|
||||
document,
|
||||
h,
|
||||
&off,
|
||||
sel,
|
||||
entity_color,
|
||||
pattern_length,
|
||||
pattern,
|
||||
line_weight_px,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
false,
|
||||
);
|
||||
for x in &mut w {
|
||||
x.aci = aci;
|
||||
x.aabb = aabb;
|
||||
if wall_pat_len > 1e-6 {
|
||||
x.pattern = wall_pat;
|
||||
x.pattern_length = wall_pat_len;
|
||||
}
|
||||
}
|
||||
rails.append(&mut w);
|
||||
}
|
||||
}
|
||||
if !rails.is_empty() {
|
||||
bases = rails;
|
||||
}
|
||||
}
|
||||
let last = *verts.last().unwrap();
|
||||
for (i, sym) in dgn_syms.iter().enumerate() {
|
||||
let at = if i == 0 { verts[0] } else { last };
|
||||
|
||||
Reference in new issue
Block a user