feat(render): double-single vertex positions for the wire pipeline

Vertices now carry the low residual alongside the high half all the way
to the GPU: tessellation splits the offset-relative f64 coordinate into
(high, low) f32 (TruckTessResult::Lines/Point, the Text path, Lines /
SegmentedLines, and the block-cache instance transform), WireModel and
the block batch carry a paired points_low buffer, WireInstance gains
pos_a_low/pos_b_low (locations 10/11), and wire.wgsl reconstructs
rel = (pos_high - eye_high) + (pos_low - eye_low).

This is the piece the camera-only RTE was missing: at UTM-scale
coordinates the single-f32 vertex was quantized to ~0.5 m, so it snapped
each time the eye crossed an f32 boundary on pan. With the low half the
position is exact and the cross-drawing-paste jitter is gone.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-06-23 18:28:52 +03:00
1 parent 97d6993768
commit 6a74f12a92
6 files changed
+237 -151

No files matched your search

+39 -14
View File
@@ -35,6 +35,9 @@ const MIN_PIXEL_SIZE: f32 = 2.0;
#[derive(Clone, Debug)]
pub struct LocalWire {
pub points: Vec<[f32; 3]>,
/// Low-bit residual paired with `points` so block-instance wires keep
/// sub-f32 precision once the renderer translates them to world space.
pub points_low: Vec<[f32; 3]>,
pub key_vertices: Vec<[f32; 3]>,
pub snap_pts: Vec<(glam::Vec3, SnapHint)>,
pub tangent_geoms: Vec<TangentGeom>,
@@ -521,6 +524,7 @@ fn tessellate_sub_local(
Some(LocalWire {
points: wire.points,
points_low: wire.points_low,
key_vertices: wire.key_vertices,
snap_pts: wire.snap_pts,
tangent_geoms: wire.tangent_geoms,
@@ -947,7 +951,11 @@ fn emit_text_baseline(
&& !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false);
if needs_sep {
entry.points.push([f32::NAN; 3]);
entry.points_low.push([0.0; 3]);
}
// Greek-substitute lines come from the LOD hatch path; their
// positions already sit at the same magnitude as other batched
// points, so a zero low component matches the f32 cast `xf` did.
for q in [p0, p1] {
if q[0] < entry.min_x {
entry.min_x = q[0];
@@ -962,6 +970,7 @@ fn emit_text_baseline(
entry.max_y = q[1];
}
entry.points.push(q);
entry.points_low.push([0.0; 3]);
}
}
}
@@ -1031,6 +1040,7 @@ struct BatchEntry {
aci: u8,
plinegen: bool,
points: Vec<[f32; 3]>,
points_low: Vec<[f32; 3]>,
snap_pts: Vec<(glam::Vec3, SnapHint)>,
key_vertices: Vec<[f32; 3]>,
tangent_geoms: Vec<TangentGeom>,
@@ -1106,7 +1116,7 @@ impl Batches {
WireModel {
name: name.to_string(),
points: b.points,
points_low: Vec::new(),
points_low: b.points_low,
color,
selected,
pattern_length: b.pattern_length,
@@ -1394,31 +1404,46 @@ fn emit_wire(
if !lw.points.is_empty() {
if needs_sep {
entry.points.push([f32::NAN; 3]);
entry.points_low.push([0.0; 3]);
}
for p in &lw.points {
// Iterate paired with the matching low residual so the GPU keeps
// sub-f32 precision once the INSERT transform lands the wire in
// world space at UTM-scale coordinates.
for (idx, p) in lw.points.iter().enumerate() {
if p[0].is_nan() {
entry.points.push([f32::NAN; 3]);
entry.points_low.push([0.0; 3]);
continue;
}
let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]);
// Reconstruct the f64 source from (high, low) before applying the
// insert transform — otherwise the low half is silently dropped.
let v = accum_xform.apply(Vector3::new(
p[0] as f64 + lo_x,
p[1] as f64 + lo_y,
p[2] as f64 + lo_z,
p[0] as f64 + pl[0] as f64 + lo_x,
p[1] as f64 + pl[1] as f64 + lo_y,
p[2] as f64 + pl[2] as f64 + lo_z,
));
let q = [(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32];
if q[0] < entry.min_x {
entry.min_x = q[0];
let qx = (v.x - ox) as f32;
let qy = (v.y - oy) as f32;
let qz = (v.z - oz) as f32;
let qx_l = ((v.x - ox) - qx as f64) as f32;
let qy_l = ((v.y - oy) - qy as f64) as f32;
let qz_l = ((v.z - oz) - qz as f64) as f32;
let q = [qx, qy, qz];
if qx < entry.min_x {
entry.min_x = qx;
}
if q[1] < entry.min_y {
entry.min_y = q[1];
if qy < entry.min_y {
entry.min_y = qy;
}
if q[0] > entry.max_x {
entry.max_x = q[0];
if qx > entry.max_x {
entry.max_x = qx;
}
if q[1] > entry.max_y {
entry.max_y = q[1];
if qy > entry.max_y {
entry.max_y = qy;
}
entry.points.push(q);
entry.points_low.push([qx_l, qy_l, qz_l]);
}
}
+96 -48
View File
@@ -24,6 +24,51 @@ use crate::scene::convert::truck_tess::{
};
use crate::scene::model::wire_model::{SnapHint, WireModel};
/// Split an f64 offset-relative coordinate into the double-single (high, low)
/// f32 pair the renderer consumes. `high + low ≈ value` to ~f64 precision; the
/// RTE shader subtracts the eye's own high/low so vertices stay smooth even at
/// coordinates where a plain f32 cast would quantize to half a metre.
#[inline]
fn split_ds(v: f64) -> (f32, f32) {
let h = v as f32;
let l = (v - h as f64) as f32;
(h, l)
}
#[inline]
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
let (xh, xl) = split_ds(x);
let (yh, yl) = split_ds(y);
let (zh, zl) = split_ds(z);
([xh, yh, zh], [xl, yl, zl])
}
/// Subtract `world_offset` from each f64 source point and split the residual
/// into double-single (high, low) f32 buffers in one pass.
fn offset_to_ds(
src: impl IntoIterator<Item = [f64; 3]>,
world_offset: [f64; 3],
) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
let [ox, oy, oz] = world_offset;
let it = src.into_iter();
let (lo, hi) = it.size_hint();
let cap = hi.unwrap_or(lo);
let mut high = Vec::with_capacity(cap);
let mut low = Vec::with_capacity(cap);
for [x, y, z] in it {
if x.is_nan() {
// Wire-model NaN-separator: keep both buffers index-paired.
high.push([f32::NAN; 3]);
low.push([0.0; 3]);
continue;
}
let (h, l) = split_ds_xyz(x - ox, y - oy, z - oz);
high.push(h);
low.push(l);
}
(high, low)
}
// ── Public entry points ────────────────────────────────────────────────────
/// Tessellate one entity into a WireModel.
@@ -104,7 +149,8 @@ pub fn tessellate(
// value mixes inline colours.
TruckObject::Text(stroke_groups) => {
let [ox, oy, oz] = world_offset;
let elev = entity_z(entity) - oz as f32;
let entity_zf = entity_z(entity) as f64;
let elev_v = entity_zf - oz;
// anno_scale anchors at the first group's origin so multi-line
// MText lines spread apart correctly as they grow.
@@ -112,46 +158,56 @@ pub fn tessellate(
.first()
.map(|g| g.origin)
.unwrap_or([0.0, 0.0]);
let ref_lx = (ref_origin[0] - ox) as f32;
let ref_ly = (ref_origin[1] - oy) as f32;
let ref_lx_v = ref_origin[0] - ox;
let ref_ly_v = ref_origin[1] - oy;
// Selection forces a single uniform colour — never split.
let split_by_color = !selected;
// Bins: key = Some(rgb) or None (= inherit entity colour).
// Preserve insertion order so wire-emit ordering tracks the
// original glyph stream (useful for stable hit-test indexing).
let mut bins: Vec<(Option<[f32; 3]>, Vec<[f32; 3]>)> = Vec::new();
// Bins: key = Some(rgb), parallel high/low f32 buffers — the
// low buffer is index-for-index with high so the renderer's
// double-single RTE shader survives at UTM-scale anchors.
let mut bins: Vec<(Option<[f32; 3]>, Vec<[f32; 3]>, Vec<[f32; 3]>)> = Vec::new();
let mut bin_first: Vec<bool> = Vec::new();
let find_or_make =
|key: Option<[f32; 3]>, bins: &mut Vec<(Option<[f32; 3]>, Vec<[f32; 3]>)>, firsts: &mut Vec<bool>| -> usize {
if let Some(i) = bins.iter().position(|(k, _)| *k == key) {
|key: Option<[f32; 3]>, bins: &mut Vec<(Option<[f32; 3]>, Vec<[f32; 3]>, Vec<[f32; 3]>)>, firsts: &mut Vec<bool>| -> usize {
if let Some(i) = bins.iter().position(|(k, _, _)| *k == key) {
i
} else {
bins.push((key, Vec::new()));
bins.push((key, Vec::new(), Vec::new()));
firsts.push(true);
bins.len() - 1
}
};
let anno = anno_scale as f64;
for group in &stroke_groups {
let lx = (group.origin[0] - ox) as f32;
let ly = (group.origin[1] - oy) as f32;
let slx = (lx - ref_lx) * anno_scale + ref_lx;
let sly = (ly - ref_ly) * anno_scale + ref_ly;
let lx_v = group.origin[0] - ox;
let ly_v = group.origin[1] - oy;
let slx_v = (lx_v - ref_lx_v) * anno + ref_lx_v;
let sly_v = (ly_v - ref_ly_v) * anno + ref_ly_v;
let bin_key = if split_by_color { group.color } else { None };
let bi = find_or_make(bin_key, &mut bins, &mut bin_first);
let pts = &mut bins[bi].1;
let (_k, pts, pts_low) = {
let b = &mut bins[bi];
(&b.0, &mut b.1, &mut b.2)
};
let _ = _k;
for stroke in &group.strokes {
if stroke.len() < 2 {
continue;
}
if !bin_first[bi] && !pts.is_empty() {
pts.push([f32::NAN, f32::NAN, f32::NAN]);
pts_low.push([0.0; 3]);
}
bin_first[bi] = false;
for &[x, y] in stroke {
pts.push([x * anno_scale + slx, y * anno_scale + sly, elev]);
let xv = x as f64 * anno + slx_v;
let yv = y as f64 * anno + sly_v;
let (h, l) = split_ds_xyz(xv, yv, elev_v);
pts.push(h);
pts_low.push(l);
}
}
}
@@ -188,7 +244,7 @@ pub fn tessellate(
let bin_count = bins.len();
let mut out: Vec<WireModel> = Vec::with_capacity(bin_count);
for (idx, (override_rgb, pts)) in bins.into_iter().enumerate() {
for (idx, (override_rgb, pts, pts_low)) in bins.into_iter().enumerate() {
let wire_color = match override_rgb {
Some([r, g, b]) => [r, g, b, color[3]],
None => color,
@@ -208,7 +264,7 @@ pub fn tessellate(
out.push(WireModel {
name: name.clone(),
points: pts,
points_low: Vec::new(),
points_low: pts_low,
color: wire_color,
selected,
pattern_length: 0.0,
@@ -231,7 +287,7 @@ pub fn tessellate(
TruckObject::Point(v) => {
let result = tessellate_vertex(&v, world_offset);
match result {
TruckTessResult::Point([x, y, z]) => {
TruckTessResult::Point([x, y, z], [xl, yl, zl]) => {
let s = 0.1_f32;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let [ox, oy, oz] = world_offset;
@@ -250,7 +306,11 @@ pub fn tessellate(
[x, y - s, z],
[x, y + s, z],
],
points_low: Vec::new(),
// All four cross points share the Point's residual
// (the cross arms are tiny, < 0.1 m, so the low
// component of the centre is also the right one
// for the arm tips at f32 precision).
points_low: vec![[xl, yl, zl]; 4],
color,
selected,
pattern_length: 0.0,
@@ -271,7 +331,9 @@ pub fn tessellate(
}
TruckObject::Curve(e) => {
if let TruckTessResult::Lines(points) = tessellate_edge(&e, world_offset) {
if let TruckTessResult::Lines(points, points_low) =
tessellate_edge(&e, world_offset)
{
let [ox, oy, oz] = world_offset;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te
@@ -282,7 +344,7 @@ pub fn tessellate(
return vec![WireModel {
name,
points,
points_low: Vec::new(),
points_low,
color,
selected,
pattern_length,
@@ -301,7 +363,9 @@ pub fn tessellate(
}
TruckObject::Contour(w) => {
if let TruckTessResult::Lines(points) = tessellate_wire(&w, world_offset) {
if let TruckTessResult::Lines(points, points_low) =
tessellate_wire(&w, world_offset)
{
let [ox, oy, oz] = world_offset;
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te
@@ -312,7 +376,7 @@ pub fn tessellate(
return vec![WireModel {
name,
points,
points_low: Vec::new(),
points_low,
color,
selected,
pattern_length,
@@ -333,19 +397,12 @@ pub fn tessellate(
TruckObject::Lines(points) => {
// Points are world-space f64 from entity converters (polyline,
// leader, mesh, solid2d, etc.). Subtract world_offset in f64
// before casting to f32 so drawings at large UTM-style
// coordinates keep sub-unit precision in the wire model.
// and split into double-single (high, low) f32 buffers — the
// GPU shader pairs them so drawings at large UTM-style
// coordinates keep sub-unit precision in the wire model and
// don't jitter on camera movement.
let [ox, oy, oz] = world_offset;
let local_pts: Vec<[f32; 3]> = points
.into_iter()
.map(|[x, y, z]| {
if x.is_nan() {
[f32::NAN, f32::NAN, f32::NAN]
} else {
[(x - ox) as f32, (y - oy) as f32, (z - oz) as f32]
}
})
.collect();
let (local_pts, local_pts_low) = offset_to_ds(points, world_offset);
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te
.key_vertices
@@ -360,7 +417,7 @@ pub fn tessellate(
return vec![WireModel {
name,
points: local_pts,
points_low: Vec::new(),
points_low: local_pts_low,
color,
selected,
pattern_length: 0.0,
@@ -379,16 +436,7 @@ pub fn tessellate(
TruckObject::SegmentedLines(points) => {
let [ox, oy, oz] = world_offset;
let local_pts: Vec<[f32; 3]> = points
.into_iter()
.map(|[x, y, z]| {
if x.is_nan() {
[f32::NAN, f32::NAN, f32::NAN]
} else {
[(x - ox) as f32, (y - oy) as f32, (z - oz) as f32]
}
})
.collect();
let (local_pts, local_pts_low) = offset_to_ds(points, world_offset);
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te
.key_vertices
@@ -398,7 +446,7 @@ pub fn tessellate(
return vec![WireModel {
name,
points: local_pts,
points_low: Vec::new(),
points_low: local_pts_low,
color,
selected,
pattern_length,
+39 -16
View File
@@ -72,10 +72,12 @@ pub(crate) fn active_curve_tol() -> Option<f64> {
/// Output of any truck topology tessellation.
#[allow(dead_code)]
pub enum TruckTessResult {
/// A single world-space point (from Vertex).
Point([f32; 3]),
/// An ordered sequence of points forming a polyline (from Edge or Wire).
Lines(Vec<[f32; 3]>),
/// A single world-space point (from Vertex). Double-single (high, low).
Point([f32; 3], [f32; 3]),
/// An ordered sequence of points forming a polyline (from Edge or Wire),
/// returned as parallel double-single (high, low) f32 buffers so the GPU
/// keeps sub-unit precision at UTM-scale coordinates.
Lines(Vec<[f32; 3]>, Vec<[f32; 3]>),
/// A triangle mesh (from Face, Shell, or Solid).
Mesh {
verts: Vec<[f32; 3]>,
@@ -98,11 +100,25 @@ fn to_local(x: f64, y: f64, z: f64, off: [f64; 3]) -> [f32; 3] {
]
}
/// Sub-f32 residual of `to_local`, computed in f64 — the renderer pairs this
/// with the high half so the double-single shader path reconstructs the f64
/// source even when the high half is quantized to half a metre at UTM scale.
#[inline]
fn to_local_low(x: f64, y: f64, z: f64, off: [f64; 3], hi: [f32; 3]) -> [f32; 3] {
[
((x - off[0]) - hi[0] as f64) as f32,
((y - off[1]) - hi[1] as f64) as f32,
((z - off[2]) - hi[2] as f64) as f32,
]
}
// ── Vertex ────────────────────────────────────────────────────────────────
pub fn tessellate_vertex(v: &Vertex, offset: [f64; 3]) -> TruckTessResult {
let p = v.point();
TruckTessResult::Point(to_local(p.x, p.y, p.z, offset))
let hi = to_local(p.x, p.y, p.z, offset);
let lo = to_local_low(p.x, p.y, p.z, offset, hi);
TruckTessResult::Point(hi, lo)
}
// ── Edge ──────────────────────────────────────────────────────────────────
@@ -115,29 +131,36 @@ pub fn tessellate_edge(e: &Edge, offset: [f64; 3]) -> TruckTessResult {
// more on tight curves, all within the active chord-height tolerance.
// The Scene scales this with zoom so far-out arcs aren't oversampled.
let (_, pts) = curve.parameter_division((t0, t1), current_curve_tol());
let lines = pts
.iter()
.map(|p| to_local(p.x, p.y, p.z, offset))
.collect();
TruckTessResult::Lines(lines)
let mut high: Vec<[f32; 3]> = Vec::with_capacity(pts.len());
let mut low: Vec<[f32; 3]> = Vec::with_capacity(pts.len());
for p in &pts {
let hi = to_local(p.x, p.y, p.z, offset);
let lo = to_local_low(p.x, p.y, p.z, offset, hi);
high.push(hi);
low.push(lo);
}
TruckTessResult::Lines(high, low)
}
// ── Wire ──────────────────────────────────────────────────────────────────
pub fn tessellate_wire(w: &Wire, offset: [f64; 3]) -> TruckTessResult {
let mut pts: Vec<[f32; 3]> = Vec::new();
let mut high: Vec<[f32; 3]> = Vec::new();
let mut low: Vec<[f32; 3]> = Vec::new();
for edge in w.edge_iter() {
if let TruckTessResult::Lines(ep) = tessellate_edge(edge, offset) {
if pts.is_empty() {
pts = ep;
if let TruckTessResult::Lines(eh, el) = tessellate_edge(edge, offset) {
if high.is_empty() {
high = eh;
low = el;
} else {
// Skip the first point of each continuation edge to avoid
// duplicating the shared junction vertex.
pts.extend_from_slice(&ep[1..]);
high.extend_from_slice(&eh[1..]);
low.extend_from_slice(&el[1..]);
}
}
}
TruckTessResult::Lines(pts)
TruckTessResult::Lines(high, low)
}
// ── Shell ─────────────────────────────────────────────────────────────────
+11 -2
View File
@@ -104,13 +104,22 @@ pub fn edge_wires(solid: &Solid) -> Vec<acadrust::entities::Wire> {
let mut wires = Vec::new();
for shell in solid.boundaries() {
for edge in shell.edge_iter() {
if let TruckTessResult::Lines(pts) = tessellate_edge(&edge, [0.0; 3]) {
if let TruckTessResult::Lines(pts, pts_low) =
tessellate_edge(&edge, [0.0; 3])
{
if pts.len() < 2 {
continue;
}
let pts3: Vec<Vector3> = pts
.iter()
.map(|p| Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64))
.zip(pts_low.iter())
.map(|(p, l)| {
Vector3::new(
p[0] as f64 + l[0] as f64,
p[1] as f64 + l[1] as f64,
p[2] as f64 + l[2] as f64,
)
})
.collect();
wires.push(acadrust::entities::Wire::from_points(pts3));
}
+41 -64
View File
@@ -9,18 +9,21 @@
// WireModel, separated by [NaN, NaN, NaN] points. Segments where either
// endpoint contains NaN are silently skipped during emission.
//
// Instance layout (76 bytes, stride = 76, step_mode = Instance):
// pos_a [f32; 3] offset 0 12 B — segment start (world)
// pos_b [f32; 3] offset 12 12 B — segment end (world)
// color [u8; 4] offset 24 4 B — RGBA, Unorm8x4 → vec4<f32> in shader
// distance_a f32 offset 28 4 B — arc-length at endpoint A
// distance_b f32 offset 32 4 B — arc-length at endpoint B
// half_width f32 offset 36 4 B — half line width in pixels
// pattern_length f32 offset 40 4 B — dash pattern total length
// pat0 [f32; 4] offset 44 16 B — pattern elements 0-3
// pat1 [f32; 4] offset 60 16 B — pattern elements 4-7
// ------
// 76 B / instance
// Instance layout (step_mode = Instance):
// pos_a [f32; 3] — segment start (high half, world / offset-relative)
// pos_a_low [f32; 3] — segment start low residual (double-single pair)
// pos_b [f32; 3] — segment end (high)
// pos_b_low [f32; 3] — segment end low residual
// color [u8; 4] — RGBA, Unorm8x4 → vec4<f32> in shader
// distance_a f32 — arc-length at endpoint A
// distance_b f32 — arc-length at endpoint B
// half_width f32 — half line width in pixels
// pattern_length f32 — dash pattern total length
// pat0 [f32; 4] — pattern elements 0-3
// pat1 [f32; 4] — pattern elements 4-7
// draw_depth f32 — normalized draw-order depth bias
// The high+low pair encodes the f64 source so the relative-to-eye shader
// stays precise at UTM-scale coordinates and after a cross-drawing paste.
use crate::scene::model::wire_model::WireModel;
use iced::wgpu;
@@ -61,7 +64,9 @@ fn instance_buffer_mapped(
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WireInstance {
pub pos_a: [f32; 3],
pub pos_a_low: [f32; 3],
pub pos_b: [f32; 3],
pub pos_b_low: [f32; 3],
/// RGBA packed as `Unorm8x4` — the vertex shader receives a `vec4<f32>`
/// in [0, 1] after the GPU does the conversion. 8 bits per channel is
/// indistinguishable from f32 at 8-bit display output.
@@ -79,61 +84,27 @@ pub struct WireInstance {
impl WireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
// Offsets come from the struct layout (must match the shader location
// indices in wire.wgsl). The low residuals are appended at locations
// 10/11 so the existing 0-9 stay stable.
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, color) as u64, shader_location: 2, format: wgpu::VertexFormat::Unorm8x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, half_width) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pattern_length) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pat0) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pat1) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, draw_depth) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_a_low) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, pos_b_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x3 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<WireInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x3,
}, // pos_a
wgpu::VertexAttribute {
offset: 12,
shader_location: 1,
format: wgpu::VertexFormat::Float32x3,
}, // pos_b
wgpu::VertexAttribute {
offset: 24,
shader_location: 2,
format: wgpu::VertexFormat::Unorm8x4,
}, // color
wgpu::VertexAttribute {
offset: 28,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
}, // distance_a
wgpu::VertexAttribute {
offset: 32,
shader_location: 4,
format: wgpu::VertexFormat::Float32,
}, // distance_b
wgpu::VertexAttribute {
offset: 36,
shader_location: 5,
format: wgpu::VertexFormat::Float32,
}, // half_width
wgpu::VertexAttribute {
offset: 40,
shader_location: 6,
format: wgpu::VertexFormat::Float32,
}, // pattern_length
wgpu::VertexAttribute {
offset: 44,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
}, // pat0
wgpu::VertexAttribute {
offset: 60,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
}, // pat1
wgpu::VertexAttribute {
offset: 76,
shader_location: 9,
format: wgpu::VertexFormat::Float32,
}, // draw_depth
],
attributes: ATTRS,
}
}
}
@@ -236,6 +207,10 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
}
}
// Low residual paired index-for-index with points; empty → all-zero
// (interactive / preview wires that don't need sub-f32 precision).
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
for i in 0..seg_count {
let a = wire.points[i];
@@ -251,7 +226,9 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
}
instances.push(WireInstance {
pos_a: a,
pos_a_low: low(i),
pos_b: b,
pos_b_low: low(i + 1),
color: color_u8,
distance_a: dists[i],
distance_b: dists[i + 1],
+11 -7
View File
@@ -53,6 +53,9 @@ struct InstanceIn {
@location(7) pat0: vec4<f32>,
@location(8) pat1: vec4<f32>,
@location(9) draw_depth: f32,
// Double-single low residuals of the endpoints.
@location(10) pos_a_low: vec3<f32>,
@location(11) pos_b_low: vec3<f32>,
}
// Draw-order depth bias: shifts clip-space z so 2D entities of different
@@ -85,13 +88,14 @@ struct VertexOut {
let which_end = which_end_arr[vid];
let side = side_arr[vid];
// Relative-to-eye: subtract the eye (high+low) from each endpoint, then
// transform by the rotation-only view-projection. (pos − eye_high) is exact
// in f32 when both are the same magnitude (Sterbenz), and eye_low corrects
// the eye's sub-f32 residual — so the position tracks the camera smoothly
// at large coordinates instead of snapping onto the f32 grid each frame.
let rel_a = (in.pos_a - u.eye_high) - u.eye_low;
let rel_b = (in.pos_b - u.eye_high) - u.eye_low;
// Double-single relative-to-eye: subtract the eye from each endpoint with
// both halves of the f64-emulating pair, then transform by the rotation-only
// view-projection. (pos_high − eye_high) is exact in f32 for same-magnitude
// operands (Sterbenz); adding (pos_low − eye_low) restores the residual both
// the vertex and the eye would otherwise lose — so geometry stays put at
// UTM-scale coordinates and after a cross-drawing paste, with no jitter.
let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low);
let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_low);
let clip_a = u.view_rot * vec4<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(rel_b, 1.0);