forked from 1b-agent/opencadstudio-upstream
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:
1 parent
97d6993768
commit
6a74f12a92
6 files changed
+237
-151
No files matched your search
Vendored
+39
-14
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user