From 3b2b8ad21d42f84a3f95da083e0f7402af681c28 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 20 Jul 2026 23:50:58 +0300 Subject: [PATCH] feat(dwg): render section marks from their decoded style and view graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section mark's appearance now comes entirely from file data instead of invented conventions: - Arrow direction: resolved through the drawing-view graph — the symbol's parent AcDbViewRep, the section view (owner of the section definition), each view border's active viewport — by projecting the section camera's sight onto the parent camera's basis, using the same yaw/pitch/twist convention the viewport renderer does. Only when the chain is unavailable (DXF, older files) does a geometric fallback apply. - Broken cutting line: driven by the style's plane-line / end-segment flags rather than hard-coded. - Arrowhead: resolved through the shared dimension/leader arrow table from the style's arrow block handle; a null handle gives the standard ClosedFilled default. - Label gap: the style's identifier offset. Bumps acadrust for the style and view-graph decode. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- src/scene/convert/tess.rs | 294 ++++++++++++++++++++++++++++++-------- 2 files changed, 238 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9cd5a2ce..7dff35def 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#4469ccff930cad39c4edee822772c85021979349" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#8f2cf9543af65353c5fe3f7afbe5139de4ad711c" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 132126446..7b8f1c323 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -17,67 +17,227 @@ 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( +/// The section mark's viewing direction (paper-space, unit), derived entirely +/// from the decoded model-documentation view graph: +/// +/// 1. `symbol.view_rep_handle` → the parent view's `AcDbViewRep`. +/// 2. The parent's refs name the section view's `AcDbViewRep` (any ref that +/// owns an `AcDbViewRepSectionDefinition`), falling back to the document's +/// section views. +/// 3. Each view's refs include its `AcDbViewBorder`, whose first handle is the +/// view's *active* viewport — the entity carrying the real camera. +/// 4. The section camera's sight (−view_direction) is projected onto the parent +/// camera's right/up basis (same convention as `camera_from_view`), giving +/// the on-paper arrow direction. +/// +/// `None` when any link is missing (DXF files, older DWGs) — the caller then +/// falls back to a geometric heuristic. +fn section_arrow_dir_from_views( + document: &acadrust::CadDocument, s: &acadrust::entities::SectionSymbol, +) -> Option<[f64; 2]> { + use acadrust::types::Handle as AHandle; + if s.view_rep_handle == 0 { + return None; + } + let parent_vr = AHandle::from(s.view_rep_handle); + let parent_refs = document.view_rep_refs.get(&parent_vr)?; + let parent_vp_h = parent_refs + .iter() + .find_map(|h| document.view_border_viewport.get(h))?; + let sect_vr = parent_refs + .iter() + .find(|h| **h != parent_vr && document.section_view_reps.contains(h)) + .or_else(|| document.section_view_reps.iter().find(|&&h| h != parent_vr)) + .copied()?; + let sect_refs = document.view_rep_refs.get(§_vr)?; + let sect_vp_h = sect_refs + .iter() + .find_map(|h| document.view_border_viewport.get(h))?; + + let viewport = |h: &AHandle| { + document.entities().find_map(|e| match e { + EntityType::Viewport(v) if v.common.handle == *h => Some(v), + _ => None, + }) + }; + let pvp = viewport(parent_vp_h)?; + let svp = viewport(sect_vp_h)?; + + // Section sight: view_direction points target→camera, sight is its inverse. + let sight = -glam::DVec3::new( + svp.view_direction.x, + svp.view_direction.y, + svp.view_direction.z, + ) + .try_normalize()?; + + // Parent camera basis — the same yaw/pitch/roll convention the viewport + // renderer uses (`camera_from_view`), so the arrow lands exactly where the + // view content does. + let vd = glam::Vec3::new( + pvp.view_direction.x as f32, + pvp.view_direction.y as f32, + pvp.view_direction.z as f32, + ) + .normalize_or(glam::Vec3::Z); + let pitch = vd.z.clamp(-1.0, 1.0).asin(); + let yaw = if vd.x.abs() < 1e-6 && vd.y.abs() < 1e-6 { + 0.0_f32 + } else { + vd.x.atan2(-vd.y) + }; + let rotation = view::camera::yaw_pitch_to_quat(yaw, pitch, -(pvp.twist_angle as f32)); + let right = (rotation * glam::Vec3::X).as_dvec3(); + let up = (rotation * glam::Vec3::Y).as_dvec3(); + + let d = glam::DVec2::new(sight.dot(right), sight.dot(up)); + let len = d.length(); + (len > 1e-6).then(|| [d.x / len, d.y / len]) +} + +/// Build the display wires for a decoded `AcDbSectionSymbol` (the "A-A" cut +/// mark on a Model-Documentation view), styled from its `AcDbSectionViewStyle`. +/// +/// Everything drawn here is decoded from the file: +/// - Endpoints, end-tick lengths and the identifier come from the symbol. +/// - Whether the full cutting line is drawn (`show_plane_line` — off gives the +/// familiar broken line), the end segments (`show_end_lines`), the arrows +/// (`show_arrows`), and all sizes come from the section-view style's flags. +/// - The arrowhead shape resolves through the style's arrow block handle via +/// the shared dimension/leader arrow table (`arrow_from_block`); a null +/// handle is the standard ClosedFilled default. +/// - The arrow direction comes from the view graph +/// ([`section_arrow_dir_from_views`]); only when that chain is unavailable +/// does it fall back to the 90°-CCW rotation of END-A→END-B. +fn section_symbol_wires( + document: &acadrust::CadDocument, + s: &acadrust::entities::SectionSymbol, + style: Option<&acadrust::entities::SectionViewStyle>, 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]; + use crate::scene::convert::tessellate::{append_arrow, arrow_from_block, ArrowKind, DimGeom}; - 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 nan = [f64::NAN; 3]; + let (ax, ay) = (s.end_a[0], s.end_a[1]); + let (bx, by) = (s.end_b[0], s.end_b[1]); + // Unit along the cut, pointing from END-B toward END-A (each end's tick + // extends *outward*, away from the other end). + let (dx, dy) = (ax - bx, ay - by); + let clen = (dx * dx + dy * dy).sqrt().max(1e-9); + let (ux, uy) = (dx / clen, dy / clen); + // Viewing direction: from the decoded view graph when the chain resolves, + // else the 90°-CCW rotation of (END-A → END-B). + let [vx, vy] = section_arrow_dir_from_views(document, s).unwrap_or_else(|| { + let (vx0, vy0) = (ay - by, bx - ax); + let vlen = (vx0 * vx0 + vy0 * vy0).sqrt().max(1e-9); + [vx0 / vlen, vy0 / vlen] + }); + + let (show_arrows, show_plane_line, show_end_lines, arrow_size, arrow_ext, label_h) = + match style { + Some(st) => ( + st.show_arrows, + st.show_plane_line, + st.show_end_lines, + st.arrow_size, + st.arrow_extension, + st.label_height, + ), + None => { + let t = s.tick_a.abs().max(s.tick_b.abs()); + (true, false, true, (t * 0.66).max(2.5), t.max(5.0), t.max(2.5)) + } + }; + // Arrowhead via the shared dimension/leader arrow table: the style's arrow + // block handle (null → ClosedFilled), sized from the style. + let arrow_kind = match style { + Some(st) if st.arrow_start_handle != 0 => arrow_from_block( + document, + acadrust::types::Handle::from(st.arrow_start_handle), + arrow_size as f32, + ), + _ => ArrowKind::Triangle { + size: arrow_size as f32, + filled: true, + size_mul: 1.0, + }, + }; + + let mut lines: Vec<[f64; 3]> = Vec::new(); + let mut fill: Vec<[f64; 3]> = Vec::new(); + + // Full cutting-plane line through the view, only when the style asks. + if show_plane_line { + lines.push([bx, by, 0.0]); + lines.push([ax, ay, 0.0]); } - 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); + // Each end: (endpoint, tick length, outward sign along the cut). + for (ex, ey, tick, osign) in [(ax, ay, s.tick_a.abs(), 1.0), (bx, by, s.tick_b.abs(), -1.0)] { + let (ox, oy) = (ux * osign, uy * osign); // outward along the cut + let tip = [ex + ox * tick, ey + oy * tick, 0.0]; // outer tick tip + // End segment: the short drawn extension past the end (the "broken" + // section line when show_plane_line is off). + if show_end_lines && tick > 1e-9 { + lines.push(nan); + lines.push([ex, ey, 0.0]); + lines.push(tip); + } - // 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, - ]; + if show_arrows { + // Arrowhead at the end of a short shaft along the viewing + // direction, apex landing `arrow_ext` out from the tick tip. The + // shared `append_arrow` takes the apex and the tip→base direction. + let apex = [tip[0] + vx * arrow_ext, tip[1] + vy * arrow_ext, 0.0]; + let mut g = DimGeom::new(); + append_arrow( + &mut g, + glam::Vec3::new(apex[0] as f32, apex[1] as f32, 0.0), + glam::Vec3::new(-vx as f32, -vy as f32, 0.0), + &arrow_kind, + ); + // Shaft from the tick tip to the arrowhead base. + let a_len = match arrow_kind { + ArrowKind::Triangle { size, size_mul, .. } => (size * size_mul) as f64, + _ => arrow_size, + }; + let base = [apex[0] - vx * a_len, apex[1] - vy * a_len, 0.0]; + lines.push(nan); + lines.push(tip); + lines.push(base); + let mut it = g.dim_lines.chunks_exact(2); + while let Some([p0, p1]) = it.next() { + lines.push(nan); + lines.push([p0[0] as f64, p0[1] as f64, 0.0]); + lines.push([p1[0] as f64, p1[1] as f64, 0.0]); + } + for t in g.arrow_fill.chunks_exact(3) { + for p in t { + fill.push([p[0] as f64, p[1] as f64, 0.0]); + } + } + } + + // Identifier glyph outboard of the tick tip, centred on the cut line. + if !s.label.trim().is_empty() && label_h > 1e-6 { + let approx_w = label_h * 0.7 * s.label.chars().count().max(1) as f64; + // Gap past the tip along the outward direction — the style's + // `identifier_offset` when decoded; for the far (negative-outward) + // end drop a text height so the glyph clears the tick. Centre the + // run on the cut line's X. + let gap = style + .map(|st| st.label_offset) + .filter(|v| *v > 1e-9) + .unwrap_or(label_h * 0.35); + let drop = if osign < 0.0 { label_h } else { 0.0 }; + let base = [ex - approx_w * 0.5, tip[1] + oy * gap - drop]; let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( [0.0, 0.0], - txt_h as f32, + label_h as f32, 0.0, 1.0, 0.0, @@ -96,15 +256,27 @@ fn section_symbol_wires( 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); - } + lines.push(nan); + lines.extend(tp); } } + + let mut wires = Vec::new(); + // Lines (ticks + shafts + glyphs): a fill-free wire so the strokes render. + let (lp, lp_low) = convert::tessellate::points_to_ds(lines); + 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); + // Filled arrowheads: a separate wire carrying only triangles. + if !fill.is_empty() { + let (fp, fp_low) = convert::tessellate::points_to_ds(fill); + let mut fw = WireModel::solid(h.value().to_string(), Vec::new(), color, sel); + fw.fill_tris = fp; + fw.fill_tris_low = fp_low; + fw.line_weight_px = lw_px; + wires.push(fw); + } wires } @@ -441,7 +613,15 @@ pub(crate) fn tessellate_entity( // 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); + return section_symbol_wires( + document, + s, + document.section_view_style.as_ref(), + h, + entity_color, + sel, + line_weight_px, + ); } }