fix(render): miter the joints of wide polyline bands

Each band segment was a rectangle ending square at its vertex, so the
outer corner of every joint showed a notch. Every segment end now carries
the tangent of half its turn, and the shader slides that end's corners
onto the joint's bisector so neighbours meet in a miter. Turns sharper
than the limit keep butt ends. Constant-width runs inside a polyline split
around its arcs no longer count as tapered, which had kept them unmitred.

Fixes #1565

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-04 20:44:08 +03:00
1 parent 4cfcc9dc38
commit d6dfd0d46d
6 files changed
+161 -23

No files matched your search

+11 -4
View File
@@ -495,10 +495,17 @@ fn split_mixed_polyline(
.iter()
.copied()
.fold(0.0f32, f32::max);
let has_line_taper = straight_widths
.first()
.map_or(false, |&w0| straight_widths.iter().any(|&w| (w - w0).abs() > 1e-6));
let taper_widths = if has_line_taper || straight_widths.iter().any(|&w| w > 1e-6) {
// NaN breaks carry a 0 width; only real points decide whether the
// band tapers, so a constant-width run keeps its mitered joints.
let mut real_widths = line_pts
.iter()
.zip(&straight_widths)
.filter(|(p, _)| p[0].is_finite())
.map(|(_, &w)| w);
let has_line_taper = real_widths
.next()
.map_or(false, |w0| real_widths.any(|w| (w - w0).abs() > 1e-6));
let taper_widths = if has_line_taper {
straight_widths
} else {
Vec::new()
+117 -10
View File
@@ -71,8 +71,9 @@ pub struct WireInstance {
/// Endpoint width / the per-wire maximum width, normalized by the vertex
/// fetch unit. `[0, 0]` means use the constant width. Ratios retain the
/// full f32 world-width scale in `WireConst` while making every instance
/// exactly one 64-byte cache line.
pub taper_ratio: [u16; 2],
/// exactly one 64-byte cache line. A constant-width band carries its
/// joint miters (`band_miters`) here instead.
pub taper_ratio: [i16; 2],
}
impl WireInstance {
@@ -86,7 +87,7 @@ impl WireInstance {
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Unorm16x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Snorm16x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WireInstance>() as u64,
@@ -288,7 +289,7 @@ pub struct BlockWireVertex {
pub pos_b: [f32; 3],
pub pos_b_low: [f32; 3],
pub distances: [f32; 2],
pub taper_ratio: [u16; 2],
pub taper_ratio: [i16; 2],
}
impl BlockWireVertex {
@@ -299,7 +300,7 @@ impl BlockWireVertex {
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, distances) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64, shader_location: 5, format: wgpu::VertexFormat::Unorm16x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64, shader_location: 5, format: wgpu::VertexFormat::Snorm16x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
@@ -577,6 +578,96 @@ fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) {
)
}
/// Largest |tan(turn/2)| a band joint is mitered at; sharper turns stay butt
/// ends instead of growing a long spike. Must match `MITER_LIMIT` in the wire
/// shaders, which decode the normalized value back.
pub(crate) const MITER_LIMIT: f64 = 8.0;
/// Per-segment miter factors `[k_a, k_b]` of a constant-width world band,
/// normalized by `MITER_LIMIT`. The shader slides each quad corner by
/// `k·side·half_width` along the segment, so neighbouring segments meet on
/// their joint's bisector instead of leaving a notch at the outer corner.
/// Empty for every other wire.
fn band_miters(wire: &WireModel, normal: [f32; 4]) -> Vec<[f32; 2]> {
let n = wire.points.len();
if wire.world_width <= 0.0 || !wire.taper_widths.is_empty() || n < 3 {
return Vec::new();
}
let at = |i: usize| -> Option<glam::DVec3> {
let h = wire.points[i];
let l = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
finite3(h).then(|| {
glam::DVec3::new(
h[0] as f64 + l[0] as f64,
h[1] as f64 + l[1] as f64,
h[2] as f64 + l[2] as f64,
)
})
};
let same = |a: glam::DVec3, b: glam::DVec3| a.distance(b) <= 1e-9 * (1.0 + a.length());
let first = (0..n).find_map(|i| at(i).map(|p| (i, p)));
let last = (0..n).rev().find_map(|i| at(i).map(|p| (i, p)));
let closed = matches!((first, last), (Some((i, a)), Some((j, b))) if i < j && same(a, b));
// Direction leaving joint `from` (or arriving at it when `step` walks
// back), skipping duplicate points and NaN breaks whose next run starts
// at the same joint; a closed wire wraps around once.
let neighbour = |from: usize, step: isize| -> Option<glam::DVec3> {
let joint = at(from)?;
let mut i = from as isize;
let mut wrapped = false;
let mut broken = false;
loop {
i += step;
if i < 0 || i >= n as isize {
if !closed || wrapped {
return None;
}
wrapped = true;
i = if step > 0 { first?.0 as isize } else { last?.0 as isize };
continue;
}
let Some(q) = at(i as usize) else {
broken = true;
continue;
};
if same(q, joint) {
broken = false;
continue;
}
if broken {
return None;
}
let d = if step > 0 { q - joint } else { joint - q };
return Some(d.normalize());
}
};
let axis = glam::DVec3::new(normal[0] as f64, normal[1] as f64, normal[2] as f64);
let axis = if axis.length() > 1e-4 { axis.normalize() } else { glam::DVec3::Z };
// tan of half the signed turn from `d1` to `d2` about the band normal.
let half_turn = |d1: glam::DVec3, d2: glam::DVec3| -> f32 {
let cos = d1.dot(d2);
if cos <= -1.0 + 1e-9 {
return 0.0;
}
let t = axis.dot(d1.cross(d2)) / (1.0 + cos);
if t.abs() > MITER_LIMIT { 0.0 } else { (t / MITER_LIMIT) as f32 }
};
(0..n - 1)
.map(|i| {
let (Some(a), Some(b)) = (at(i), at(i + 1)) else {
return [0.0; 2];
};
if same(a, b) {
return [0.0; 2];
}
let d = (b - a).normalize();
let k_a = neighbour(i, -1).map_or(0.0, |prev| half_turn(prev, d));
let k_b = neighbour(i + 1, 1).map_or(0.0, |next| -half_turn(d, next));
[k_a, k_b]
})
.collect()
}
/// Emit packed per-segment instances (each carries the wire's constants).
pub(crate) fn emit_wire_packed(
wire: &WireModel,
@@ -599,6 +690,7 @@ pub(crate) fn emit_wire_packed(
(Vec::new(), 0.0, 0.0)
};
let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
let miters = band_miters(wire, marker_normal_scale);
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
let is_tapered = !wire.taper_widths.is_empty();
let tw = |i: usize| -> f32 {
@@ -638,8 +730,17 @@ pub(crate) fn emit_wire_packed(
world_half_width: wire.world_width * 0.5,
world_hw_a: tw(i),
world_hw_b: tw(i + 1),
marker_origin_high,
marker_origin_low,
// The origin `w` slots are free: they carry the band miters.
marker_origin_high: {
let mut m = marker_origin_high;
m[3] = miters.get(i).map_or(0.0, |k| k[0]);
m
},
marker_origin_low: {
let mut m = marker_origin_low;
m[3] = miters.get(i).map_or(0.0, |k| k[1]);
m
},
marker_normal_scale,
});
}
@@ -684,14 +785,17 @@ pub(crate) fn emit_wire_native(
return (Vec::new(), cst);
}
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
let taper_ratio = |i: usize| -> u16 {
let snorm = |v: f32| -> i16 { (v.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16 };
let taper_ratio = |i: usize| -> i16 {
if !is_tapered || wire.world_width <= 0.0 {
0
} else {
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32).round() as u16
snorm(width / wire.world_width)
}
};
// A constant-width band reuses the slot for its joint miters.
let miters = band_miters(wire, marker_normal_scale);
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
for i in 0..seg_count {
let a = wire.points[i];
@@ -712,7 +816,10 @@ pub(crate) fn emit_wire_native(
distance_a: dist_a,
distance_b: dist_b,
wire_id,
taper_ratio: [taper_ratio(i), taper_ratio(i + 1)],
taper_ratio: match miters.get(i) {
Some(k) => [snorm(k[0]), snorm(k[1])],
None => [taper_ratio(i), taper_ratio(i + 1)],
},
});
}
(instances, cst)
+8 -2
View File
@@ -46,6 +46,8 @@ struct VertexIn {
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
// Must match `MITER_LIMIT` in wire_gpu.rs.
const MITER_LIMIT: f32 = 8.0;
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@@ -161,13 +163,17 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
}
perp_world = normalize(perp_world);
// A constant-width band slides each corner along the segment onto
// its joint's bisector (miter), so neighbours meet without a notch.
let miter = select(in.taper_ratio, vec2<f32>(0.0), is_tapered) * MITER_LIMIT;
let along = mix(miter.x, miter.y, which_end) * eff_hw * side;
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
let world_pos = pos_rel + perp_world * (eff_hw * side) + world_dir * along;
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos = apply_draw_order(clip_pos, in.depth.x);
final_clip = clip_pos;
out_dist = mix(in.distances.x, in.distances.y, which_end);
out_dist = mix(in.distances.x, in.distances.y, which_end) + along * (in.distances.y - in.distances.x) / max(world_len, 1e-6);
out_cap = vec2<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(world_len, world_hw_a, world_hw_b);
} else {
+9 -3
View File
@@ -70,6 +70,8 @@ struct VertexIn {
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
// Must match `MITER_LIMIT` in wire_gpu.rs.
const MITER_LIMIT: f32 = 8.0;
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@@ -144,7 +146,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
let seg_pos_b = vec3<f32>(seg.pos_b_x, seg.pos_b_y, seg.pos_b_z);
let seg_pos_b_low = vec3<f32>(seg.pos_b_low_x, seg.pos_b_low_y, seg.pos_b_low_z);
let seg_distances = vec2<f32>(seg.distance_a, seg.distance_b);
let seg_taper_ratio = unpack2x16unorm(seg.taper_ratio);
let seg_taper_ratio = unpack2x16snorm(seg.taper_ratio);
let which_end_arr = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
let side_arr = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let which_end = which_end_arr[corner];
@@ -192,13 +194,17 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
}
perp_world = normalize(perp_world);
// A constant-width band slides each corner along the segment onto
// its joint's bisector (miter), so neighbours meet without a notch.
let miter = select(seg_taper_ratio, vec2<f32>(0.0), is_tapered) * MITER_LIMIT;
let along = mix(miter.x, miter.y, which_end) * eff_hw * side;
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
let world_pos = pos_rel + perp_world * (eff_hw * side) + world_dir * along;
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos = apply_draw_order(clip_pos, in.depth.x);
final_clip = clip_pos;
out_dist = mix(seg_distances.x, seg_distances.y, which_end);
out_dist = mix(seg_distances.x, seg_distances.y, which_end) + along * (seg_distances.y - seg_distances.x) / max(world_len, 1e-6);
out_cap = vec2<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(world_len, world_hw_a, world_hw_b);
} else {
+8 -2
View File
@@ -69,6 +69,8 @@ struct InstanceIn {
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
// Must match `MITER_LIMIT` in wire_gpu.rs.
const MITER_LIMIT: f32 = 8.0;
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@@ -191,13 +193,17 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
}
perp_world = normalize(perp_world);
// A constant-width band slides each corner along the segment onto
// its joint's bisector (miter), so neighbours meet without a notch.
let miter = vec2<f32>(in.marker_origin_high.w, in.marker_origin_low.w) * MITER_LIMIT;
let along = mix(miter.x, miter.y, which_end) * eff_hw * side;
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
let world_pos = pos_rel + perp_world * (eff_hw * side) + world_dir * along;
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos = apply_draw_order(clip_pos, in.misc.x);
final_clip = clip_pos;
out_dist = mix(in.dists.x, in.dists.y, which_end);
out_dist = mix(in.dists.x, in.dists.y, which_end) + along * (in.dists.y - in.dists.x) / max(world_len, 1e-6);
out_cap = vec2<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(world_len, world_hw_a, world_hw_b);
} else {
+8 -2
View File
@@ -55,6 +55,8 @@ struct InstanceIn {
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
// Must match `MITER_LIMIT` in wire_gpu.rs.
const MITER_LIMIT: f32 = 8.0;
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@@ -168,13 +170,17 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
}
perp_world = normalize(perp_world);
// A constant-width band slides each corner along the segment onto
// its joint's bisector (miter), so neighbours meet without a notch.
let miter = select(in.taper_ratio, vec2<f32>(0.0), is_tapered) * MITER_LIMIT;
let along = mix(miter.x, miter.y, which_end) * eff_hw * side;
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
let world_pos = pos_rel + perp_world * (eff_hw * side) + world_dir * along;
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos = apply_draw_order(clip_pos, c.draw_depth);
final_clip = clip_pos;
out_dist = mix(in.distance_a, in.distance_b, which_end);
out_dist = mix(in.distance_a, in.distance_b, which_end) + along * (in.distance_b - in.distance_a) / max(world_len, 1e-6);
out_cap = vec2<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(world_len, world_hw_a, world_hw_b);
} else {