From 46e636b955d991fe4b676153c7cd1004e573dcee Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 20 Jul 2026 22:20:58 +0300 Subject: [PATCH] feat(dwg): render section marks on model-documentation layouts Draw decoded section symbols (AcDbSectionSymbol) as their "A-A" cut mark: the cut line through both endpoints extended past each end by its signed tick, a short perpendicular barb at each tip, and the identifier glyph. The mark now appears on the layout alongside the drawing views instead of being silently dropped. Bumps acadrust to pick up the section-symbol decode. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 4 +- src/scene/convert/tess.rs | 104 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96d9d9628..e9cd5a2ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#885a22bacf8e30e5218e7b5856a2175241593271" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#4469ccff930cad39c4edee822772c85021979349" dependencies = [ "ahash 0.8.12", "anyhow", @@ -3179,7 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 2.8.3", + "memchr 1.0.2", "nom 8.0.0", ] diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 12d33385e..132126446 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -17,6 +17,97 @@ fn fade_if_locked( } } +/// Build the display wires for a decoded `AcDbSectionSymbol` (the "A-A" cut +/// mark on a Model-Documentation view): the cut line through both endpoints — +/// extended past each end by its signed tick — plus a short perpendicular arrow +/// barb and the identifier glyph at each end. Coordinates are paper-space (the +/// symbol is owned by a layout's paper space). +fn section_symbol_wires( + s: &acadrust::entities::SectionSymbol, + h: Handle, + color: [f32; 4], + sel: bool, + lw_px: f32, +) -> Vec { + let nan = [f64::NAN; 3]; + let a = [s.end_a[0], s.end_a[1], 0.0]; + let b = [s.end_b[0], s.end_b[1], 0.0]; + // Unit direction B → A along the cut, and its left-hand perpendicular. + let (dx, dy) = (a[0] - b[0], a[1] - b[1]); + let len = (dx * dx + dy * dy).sqrt(); + let (ux, uy) = if len > 1e-9 { (dx / len, dy / len) } else { (0.0, 1.0) }; + let (px, py) = (-uy, ux); + let ta = s.tick_a.abs(); + let tb = s.tick_b.abs(); + // Cut line, extended outward (away from the opposite end) past each end. + let a_out = [a[0] + ux * ta, a[1] + uy * ta, 0.0]; + let b_out = [b[0] - ux * tb, b[1] - uy * tb, 0.0]; + + let mut pts: Vec<[f64; 3]> = vec![b_out, a_out]; + // Perpendicular arrow barbs at each outer tip (a short V), pointing along + // the cut so the mark reads as a section line rather than a plain segment. + let barb = ta.max(tb).max(1.0) * 0.6; + for (tip, along) in [(a_out, (ux, uy)), (b_out, (-ux, -uy))] { + let back = [tip[0] - along.0 * barb, tip[1] - along.1 * barb, 0.0]; + pts.push(nan); + pts.push([back[0] + px * barb * 0.5, back[1] + py * barb * 0.5, 0.0]); + pts.push(tip); + pts.push(nan); + pts.push([back[0] - px * barb * 0.5, back[1] - py * barb * 0.5, 0.0]); + pts.push(tip); + } + + let mut wires = Vec::new(); + let (dpts, dpts_low) = convert::tessellate::points_to_ds(pts); + let mut w = WireModel::solid(h.value().to_string(), dpts, color, sel); + w.points_low = dpts_low; + w.line_weight_px = lw_px; + wires.push(w); + + // Identifier glyph beyond each tip, centred on the cut line. + let txt_h = ta.max(tb).max(2.0); + if !s.label.trim().is_empty() { + let approx_w = txt_h * 0.7 * s.label.chars().count().max(1) as f64; + for (tip, along) in [(a_out, (ux, uy)), (b_out, (-ux, -uy))] { + // Baseline origin: past the tip by ~half a text height, shifted so + // the glyph run is centred across the cut line. + let base = [ + tip[0] + along.0 * txt_h * 0.6 - ux * approx_w * 0.5 - px * txt_h * 0.5, + tip[1] + along.1 * txt_h * 0.6 - uy * approx_w * 0.5 - py * txt_h * 0.5, + ]; + let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( + [0.0, 0.0], + txt_h as f32, + 0.0, + 1.0, + 0.0, + "standard", + &s.label, + ); + let mut tp: Vec<[f64; 3]> = Vec::new(); + for stroke in &strokes { + if stroke.len() < 2 { + continue; + } + if !tp.is_empty() { + tp.push(nan); + } + for &[gx, gy] in stroke { + tp.push([base[0] + gx as f64, base[1] + gy as f64, 0.0]); + } + } + if tp.len() >= 2 { + let (lp, lp_low) = convert::tessellate::points_to_ds(tp); + let mut lw = WireModel::solid(h.value().to_string(), lp, color, sel); + lw.points_low = lp_low; + lw.line_weight_px = lw_px; + wires.push(lw); + } + } + } + wires +} + // ── Parallel tessellation free function ────────────────────────────────────── // // Takes only the `Send + Sync` data needed for tessellation so that @@ -341,6 +432,19 @@ pub(crate) fn tessellate_entity( } } + // ── Section symbol (AcDbSectionSymbol): draw the "A-A" cut mark ────────── + // + // Model-Documentation section marks arrive as `Unknown` (class 825); the + // DWG reader decodes the two cut-line endpoints, the signed end ticks and + // the identifier. Synthesize the cut line + end ticks + label glyphs so the + // mark is visible on the layout, the way AutoCAD draws it. The raw record is + // still preserved for lossless write-back. + if let EntityType::Unknown(u) = e { + if let Some(s) = u.section_symbol.as_ref() { + return section_symbol_wires(s, h, entity_color, sel, line_weight_px); + } + } + // ── Dimension baked-block fast path ───────────────────────────────────── // // AutoCAD bakes each dimension's final geometry (extension lines, dim