From bfcc71678a371f863c80eb7f8360e68b5f45c83d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Jul 2026 07:00:35 +0300 Subject: [PATCH] fix(mline): drive geometry from the stored vertex parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Element lines were re-derived from the style's flat offsets along the vertex miters, but the file already stores the real numbers: each vertex's per-element parameter list starts with the offset measured ALONG THE MITER — corner lengthening (1/cos of the half-angle), justification and scale baked in — followed by draw/gap toggle distances along the segment, which is how crossing multilines cut each other. Re-deriving pinched the channel at every corner (the ends looked flared) and drew full uncut lines through crossings, stacking intersecting multilines on top of each other. mline_lines now consumes the stored parameters directly (runs chain across vertices when continuous; an odd toggle count leaves the last run open to the segment end) and computes end caps from the actual per-vertex offsets. Files without parameters keep the style-offset fallback. Co-Authored-By: Claude Fable 5 --- src/entities/mline.rs | 111 +++++++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/src/entities/mline.rs b/src/entities/mline.rs index dca069980..d62d7b710 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -60,7 +60,8 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec Vec [f64; 3] { + // The element's miter-space offset at vertex `vi`: the stored per-vertex + // parameter[0] when present — it is measured ALONG THE MITER, so corner + // vertices already carry the 1/cos(θ/2) miter lengthening and the + // justification shift (using the flat style offset instead pinched the + // channel at every corner and made the ends look flared). The style + // offset × scale is only the fallback for files without parameters. + let elem_off = |vi: usize, ei: usize| -> f64 { + m.vertices[vi] + .segments + .get(ei) + .and_then(|sg| sg.parameters.first().copied()) + .unwrap_or_else(|| (elems[ei].0 + shift) * scale) + }; + let off_pt = |vi: usize, d: f64| -> [f64; 3] { let v = &m.vertices[vi]; [ v.position.x + v.miter.x * d, @@ -87,11 +101,67 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec = Vec::with_capacity(elems.len() + 2); - for (offset, color, linetype) in &elems { - let d = (*offset + shift) * scale; - let mut pts: Vec<[f64; 3]> = (0..m.vertices.len()).map(|i| off(i, d)).collect(); - if closed && m.vertices.len() >= 2 { - pts.push(off(0, d)); + for (ei, (_, color, linetype)) in elems.iter().enumerate() { + let mut pts: Vec<[f64; 3]> = Vec::new(); + // Walk each segment (vi → vi+1, wrapping when closed); the vertex's + // parameters[1..] are draw-toggle distances along the element line — + // this is how crossing/merged multilines store their gaps. An odd + // toggle count leaves the final run open to the segment end. + let seg_count = if closed { n } else { n.saturating_sub(1) }; + let mut pen_at_end = false; + for k in 0..seg_count { + let vi = k; + let wi = (k + 1) % n; + let a = off_pt(vi, elem_off(vi, ei)); + let b = off_pt(wi, elem_off(wi, ei)); + let seg = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + let len = (seg[0] * seg[0] + seg[1] * seg[1] + seg[2] * seg[2]).sqrt(); + if len < 1e-12 { + continue; + } + let dirn = [seg[0] / len, seg[1] / len, seg[2] / len]; + let at = |t: f64| -> [f64; 3] { + [a[0] + dirn[0] * t, a[1] + dirn[1] * t, a[2] + dirn[2] * t] + }; + let toggles: &[f64] = m + .vertices[vi] + .segments + .get(ei) + .map(|sg| sg.parameters.get(1..).unwrap_or(&[])) + .unwrap_or(&[]); + // Build (start, end) runs from the toggle list. + let mut runs: Vec<(f64, f64)> = Vec::new(); + if toggles.is_empty() { + runs.push((0.0, len)); + } else { + let mut i = 0; + while i < toggles.len() { + let start = toggles[i].clamp(0.0, len); + let end = toggles + .get(i + 1) + .copied() + .unwrap_or(len) + .clamp(0.0, len); + if end - start > 1e-9 { + runs.push((start, end)); + } + i += 2; + } + } + for (ri, (t0, t1)) in runs.iter().enumerate() { + let continuous = pen_at_end && ri == 0 && *t0 <= 1e-9 && !pts.is_empty(); + if !continuous { + if !pts.is_empty() { + pts.push([f64::NAN; 3]); + } + pts.push(at(*t0)); + } + pts.push(at(*t1)); + } + pen_at_end = runs.last().is_some_and(|(_, t1)| (len - t1).abs() <= 1e-9); + } + if pts.len() < 2 { + continue; } out.push(MLineLine { points: pts, @@ -104,21 +174,28 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec 1e-9 && !closed { + if !closed && n >= 2 { let mut cap = |vi: usize| { - out.push(MLineLine { - points: vec![off(vi, d_lo), off(vi, d_hi)], - color: Color::ByLayer, - linetype: "ByLayer".to_string(), - }); + let mut dlo = f64::INFINITY; + let mut dhi = f64::NEG_INFINITY; + for ei in 0..elems.len() { + let d = elem_off(vi, ei); + dlo = dlo.min(d); + dhi = dhi.max(d); + } + if (dhi - dlo).abs() > 1e-9 { + out.push(MLineLine { + points: vec![off_pt(vi, dlo), off_pt(vi, dhi)], + color: Color::ByLayer, + linetype: "ByLayer".to_string(), + }); + } }; if s.flags.start_square_cap { cap(0); } if s.flags.end_square_cap { - cap(m.vertices.len() - 1); + cap(n - 1); } } }