feat(mline): GPU-dash multiline elements with shared A-type alignment

MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per
dash — to keep the parallel lines in phase. Move pure-dash elements onto the
GPU dash shader and end-align every element to the multiline's centre-line so
they share one interior phase.

- wire_distances now accumulates arc-length from the double-single (high+low)
  points, so the dash phase stays precise at UTM coordinates. f32-high-only
  deltas quantised ~0.1 there and drifted the parallel lines apart — the
  original reason MLINE dashes were CPU-side.
- New WireModel.dash_align_end carries a shared begin/end dash length. The
  shader uses it as align_end for every parallel element (the interior phase
  depends on align_end, not the wire's own length) while align_total stays the
  element's own length, so each still ends on a dash at its own endpoint.
- MLINE tessellation derives that length once from the centre-line, routes
  pure dash/space/dot elements to the GPU pattern and text-bearing elements to
  apply_along.
- apply_along gained an "A"-type mode (shared reference length): a solid
  begin/end dash and a phased interior, so its dashes — and embedded text —
  line up with the GPU-dashed sibling elements instead of tiling from start.

Pure-dash MLINEs now cost one WireModel + pattern per element instead of N
segments; single (non-MLINE) entities keep the from-start tiling unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-17 02:59:11 +03:00
commit 5bd683cf4e
41 changed files with 263 additions and 14 deletions

View file

@ -258,22 +258,30 @@ fn wire_distances(wire: &WireModel) -> (Vec<f32>, f32, f32) {
let n = wire.points.len();
let mut dists = vec![0.0_f32; n];
let mut has_break = false;
// Accumulate arc-length in f64 from double-single deltas (high + low). An
// f32-high-only delta `q[0] - p[0]` quantises ~0.1 at UTM coordinates
// (−1.2M), which shifts the dash phase and drifts parallel lines out of sync
// — the reason MLINE elements were CPU-dashed. `points_low` may be empty for
// non-RTE wires → treat the low half as zero (same as the old behaviour).
let mut acc = 0.0_f64;
for i in 1..n {
let p = wire.points[i - 1];
let q = wire.points[i];
if !p[0].is_finite() || !q[0].is_finite() {
has_break = true;
// plinegen=false: reset to 0 at the first real point after a NaN separator.
dists[i] = if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
0.0
} else {
dists[i - 1]
};
if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
acc = 0.0;
}
dists[i] = acc as f32;
} else {
let dx = q[0] - p[0];
let dy = q[1] - p[1];
let dz = q[2] - p[2];
dists[i] = dists[i - 1] + (dx * dx + dy * dy + dz * dz).sqrt();
let pl = wire.points_low.get(i - 1).copied().unwrap_or([0.0; 3]);
let ql = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
let dx = (q[0] as f64 - p[0] as f64) + (ql[0] as f64 - pl[0] as f64);
let dy = (q[1] as f64 - p[1] as f64) + (ql[1] as f64 - pl[1] as f64);
let dz = (q[2] as f64 - p[2] as f64) + (ql[2] as f64 - pl[2] as f64);
acc += (dx * dx + dy * dy + dz * dz).sqrt();
dists[i] = acc as f32;
}
}
@ -295,6 +303,20 @@ fn wire_distances(wire: &WireModel) -> (Vec<f32>, f32, f32) {
return (dists, 0.0, 0.0);
}
// Shared "A"-type for MLINE elements: the caller fixes the begin/end
// solid-dash length (`dash_align_end`, derived once from the multiline's
// centre-line length) so every parallel element runs the SAME interior phase
// — the shader's interior walk depends on `align_end`, not on the wire's own
// length — while `align_total` stays this wire's own length, so each element
// still ends on a dash at its own endpoint.
if let Some(ae) = wire.dash_align_end {
if total <= pat_len {
// Shorter than one full period → solid (matches the per-wire path).
return (dists, total, total);
}
return (dists, ae.clamp(1e-4, total * 0.5), total);
}
// Align only a proper alternating pattern that BEGINS with a dash followed
// by a gap — every standard linetype does (DASHED/DASHDOT/CENTER/HIDDEN/…).
// Gap-first, dot-first, single-element, or consecutive-dash patterns keep