forked from 1b-agent/opencadstudio-upstream
fix(scene): hatch boundary + pattern precision for large-WCS drawings
Two precision problems were producing visibly wrong hatches on UTM-scale (and similarly-offset) drawings even after world_offset was correct: 1. **Boundary tessellation** — `hatch_model_from_dxf`'s polyline-bulge arc tessellation ran the whole arc-center computation in f32. With vertices at WCS magnitude ~1e5 the f32 ULP is ~1 cm, which propagated into the sampled arc points and showed up as visibly wavy / jagged hatch boundaries on edges that crossed the long axis of the drawing. Re-run the full bulge → arc math in f64; only cast at the to_xy call that feeds the (also f64) boundary buffer. 2. **Pattern phase** — the per-fragment `perp / perp_step` modulo blew precision when `xz` was at offset-rel WCS magnitude and perp_step was sub-cm. Move the precision-critical bits out of f32 by keeping each hatch's anchor in f64 on the model side: `HatchModel` now carries a `world_origin: [f64; 2]` and the `boundary: [f32; 2]` vertices are stored as small offsets from it. The GPU vertex shader adds `origin` back inside the view_proj multiply for clip position (sub-pixel-lossy at large hatches but invisible); the `xz` varying that the fragment shader uses for in_polygon + pattern math stays in hatch-local space at full f32 precision. For pattern alignment across adjacent hatches with the same family, the GPU pipeline snaps the anchor to the family's perp/along grid in f64 before sending it down. Snap uses the same QCAD-PAT convention (`fam.dy = perp_step`, `fam.dx = along_step`) the shader's check_family uses — an earlier attempt used the world-frame `(-dx*sin + dy*cos)` formula, which produced a meaningless grid and no alignment. Trade-off: pattern phase is "global modulo per-family-grid" rather than tied to WCS (0,0). Same hatches still align with each other; the absolute phase relative to the WCS origin may differ from the pre-fix behaviour on small drawings. Consumers that read `model.boundary` directly (hit_test, paper_canvas, add_hatch round-trip) reconstruct WCS via `boundary[i] + world_origin`. The hatch wgsl bind-group entry for binding 0 was bumped to `VERTEX | FRAGMENT` because the vertex shader now reads `h.origin`. The fragment-shader hatch LOD substitute + sub-pixel skip frome7675beand the scissor plumbing fromfbbac42still work — those touched different fields of the same struct. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
dc80968023
commit
172c538159
8 files changed
+238
-63
No files matched your search
@@ -119,6 +119,7 @@ impl HatchCommand {
|
||||
color: [0.75, 0.75, 0.75, 0.85],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
@@ -255,6 +256,7 @@ impl GradientCommand {
|
||||
color: [0.30, 0.60, 0.95, 0.80],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
@@ -404,6 +406,7 @@ impl CadCommand for BoundaryCommand {
|
||||
color: [0.45, 0.45, 0.45, 0.60],
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
};
|
||||
return CmdResult::CommitHatch(model);
|
||||
|
||||
@@ -43,8 +43,22 @@ pub enum HatchPattern {
|
||||
/// A hatched region defined by a closed polygon boundary.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HatchModel {
|
||||
/// World XY coordinates of the boundary polygon vertices.
|
||||
/// Arc so HatchModel::clone() is a pointer bump rather than a full Vec copy.
|
||||
/// World XY anchor (in the same offset-relative coordinate space as
|
||||
/// the rest of the scene — `world_offset` already subtracted, but
|
||||
/// kept at f64 precision). Boundary vertices are stored as f32
|
||||
/// offsets from this anchor so that:
|
||||
/// 1) hit-test / paper_canvas can still read small-magnitude f32
|
||||
/// coords without precision loss from the f64 → f32 cast that
|
||||
/// would otherwise happen at large drawing extents (UTM, etc.).
|
||||
/// 2) the GPU pipeline can pre-shift the quad in hatch-local
|
||||
/// space (so the fragment shader's `xz` varying stays small)
|
||||
/// and add `world_origin` back inside the view_proj multiply.
|
||||
/// Reconstruct WCS-relative coords as `(world_origin.x + v.x as f64,
|
||||
/// world_origin.y + v.y as f64)`.
|
||||
pub world_origin: [f64; 2],
|
||||
/// World-XY coordinates of the boundary polygon vertices, stored as
|
||||
/// f32 offsets from `world_origin`. NaN-NaN sentinels separate
|
||||
/// disconnected paths and must be preserved un-shifted by consumers.
|
||||
pub boundary: Arc<Vec<[f32; 2]>>,
|
||||
/// Fill pattern.
|
||||
pub pattern: HatchPattern,
|
||||
|
||||
+12
-3
@@ -277,10 +277,15 @@ pub fn click_hit_hatch(
|
||||
bounds: Rectangle,
|
||||
) -> Option<Handle> {
|
||||
for (&handle, hatch) in hatches {
|
||||
// boundary verts are stored as small f32 offsets from
|
||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||
// projecting to screen.
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x, y, 0.0), view_proj, bounds))
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.collect();
|
||||
if screen.len() >= 3 && point_in_polygon(cursor, &screen) {
|
||||
return Some(handle);
|
||||
@@ -315,10 +320,12 @@ pub fn box_hit_hatch(
|
||||
if hatch.boundary.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x, y, 0.0), view_proj, bounds))
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.collect();
|
||||
let hit = if crossing {
|
||||
screen.iter().any(|&sp| inside(sp))
|
||||
@@ -352,10 +359,12 @@ pub fn poly_hit_hatch(
|
||||
if hatch.boundary.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x, y, 0.0), view_proj, bounds))
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.collect();
|
||||
let hit = if crossing {
|
||||
screen.iter().any(|&sp| point_in_polygon(sp, poly))
|
||||
|
||||
+74
-34
@@ -2307,6 +2307,7 @@ impl Scene {
|
||||
color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
});
|
||||
}
|
||||
@@ -2360,6 +2361,7 @@ impl Scene {
|
||||
color: fill_color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
});
|
||||
}
|
||||
@@ -2418,21 +2420,26 @@ impl Scene {
|
||||
) -> Option<HatchModel> {
|
||||
let [ox, oy, _oz] = world_offset;
|
||||
let normal = (dxf.normal.x, dxf.normal.y, dxf.normal.z);
|
||||
let to_xy = |x: f64, y: f64| -> [f32; 2] {
|
||||
// Build the boundary in f64 first so the precision-preserving
|
||||
// origin computation below sees full WCS precision. We only cast
|
||||
// to f32 once at the end, after subtracting the AABB centre, so
|
||||
// the stored offsets are small-magnitude with high f32 precision
|
||||
// even on large UTM-scale drawings.
|
||||
let to_xy = |x: f64, y: f64| -> [f64; 2] {
|
||||
let (wx, wy, _) =
|
||||
crate::scene::transform::ocs_point_to_wcs((x, y, dxf.elevation), normal);
|
||||
[(wx - ox) as f32, (wy - oy) as f32]
|
||||
[wx - ox, wy - oy]
|
||||
};
|
||||
if dxf.paths.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut boundary: Vec<[f32; 2]> = Vec::new();
|
||||
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
let before_path = boundary.len();
|
||||
if !boundary.is_empty() {
|
||||
boundary.push([f32::NAN, f32::NAN]);
|
||||
boundary.push([f64::NAN, f64::NAN]);
|
||||
}
|
||||
let path_start = boundary.len();
|
||||
|
||||
@@ -2456,54 +2463,50 @@ impl Scene {
|
||||
if bulge.abs() < 1e-9 {
|
||||
boundary.push(to_xy(v0.x, v0.y));
|
||||
} else {
|
||||
let p0 = [v0.x as f32, v0.y as f32];
|
||||
let p1 = [v1.x as f32, v1.y as f32];
|
||||
let theta = 4.0 * (bulge as f32).atan();
|
||||
let dx = p1[0] - p0[0];
|
||||
let dy = p1[1] - p0[1];
|
||||
// Run the whole bulge → arc tessellation in
|
||||
// f64. The old f32 path lost ~1 cm of
|
||||
// precision at UTM-scale WCS coordinates
|
||||
// (1e5 magnitude × 1 ULP), which showed up
|
||||
// as visibly wavy hatch-boundary arcs.
|
||||
let theta = 4.0 * bulge.atan();
|
||||
let dx = v1.x - v0.x;
|
||||
let dy = v1.y - v0.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < 1e-9 {
|
||||
if d < 1e-12 {
|
||||
boundary.push(to_xy(v0.x, v0.y));
|
||||
continue;
|
||||
}
|
||||
let r = (d * 0.5) / (theta * 0.5).sin().abs();
|
||||
let mx = (p0[0] + p1[0]) * 0.5;
|
||||
let my = (p0[1] + p1[1]) * 0.5;
|
||||
let mx = (v0.x + v1.x) * 0.5;
|
||||
let my = (v0.y + v1.y) * 0.5;
|
||||
let px = -dy / d;
|
||||
let py = dx / d;
|
||||
let sign = if bulge > 0.0 { 1.0_f32 } else { -1.0_f32 };
|
||||
let sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let center_offset = r * (theta * 0.5).cos();
|
||||
let cx = mx + sign * px * center_offset;
|
||||
let cy = my + sign * py * center_offset;
|
||||
let a0 = (p0[1] - cy).atan2(p0[0] - cx);
|
||||
let a1 = (p1[1] - cy).atan2(p1[0] - cx);
|
||||
let a0 = (v0.y - cy).atan2(v0.x - cx);
|
||||
let a1 = (v1.y - cy).atan2(v1.x - cx);
|
||||
let mut sweep = a1 - a0;
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
if bulge > 0.0 {
|
||||
if sweep <= 0.0 {
|
||||
sweep += std::f32::consts::TAU;
|
||||
sweep += TAU;
|
||||
}
|
||||
} else if sweep >= 0.0 {
|
||||
sweep -= std::f32::consts::TAU;
|
||||
sweep -= TAU;
|
||||
}
|
||||
if sweep.abs() < 1e-7 {
|
||||
sweep = if bulge > 0.0 {
|
||||
std::f32::consts::TAU
|
||||
} else {
|
||||
-std::f32::consts::TAU
|
||||
};
|
||||
if sweep.abs() < 1e-9 {
|
||||
sweep = if bulge > 0.0 { TAU } else { -TAU };
|
||||
}
|
||||
let r64 = r as f64;
|
||||
let segs = tessellate::arc_segments(
|
||||
r64,
|
||||
sweep.abs() as f64,
|
||||
tessellate::fill_chord_tol(r64),
|
||||
r,
|
||||
sweep.abs(),
|
||||
tessellate::fill_chord_tol(r),
|
||||
);
|
||||
for j in 0..segs {
|
||||
let a = a0 + sweep * (j as f32 / segs as f32);
|
||||
boundary.push(to_xy(
|
||||
(cx + r * a.cos()) as f64,
|
||||
(cy + r * a.sin()) as f64,
|
||||
));
|
||||
let a = a0 + sweep * (j as f64 / segs as f64);
|
||||
boundary.push(to_xy(cx + r * a.cos(), cy + r * a.sin()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2656,13 +2659,47 @@ impl Scene {
|
||||
} else {
|
||||
dxf.pattern.name.clone()
|
||||
};
|
||||
|
||||
// Precision-preserving cast f64 → f32: pick an `world_origin`
|
||||
// anchor (boundary AABB centre in f64) and store every vertex
|
||||
// as a small f32 offset from it. NaN separators are preserved
|
||||
// so the in_polygon ray-cast still sees the path breaks.
|
||||
let mut min_x = f64::INFINITY;
|
||||
let mut min_y = f64::INFINITY;
|
||||
let mut max_x = f64::NEG_INFINITY;
|
||||
let mut max_y = f64::NEG_INFINITY;
|
||||
for &[x, y] in &boundary {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
if x < min_x { min_x = x; }
|
||||
if y < min_y { min_y = y; }
|
||||
if x > max_x { max_x = x; }
|
||||
if y > max_y { max_y = y; }
|
||||
}
|
||||
}
|
||||
let world_origin = if min_x.is_finite() && min_y.is_finite() {
|
||||
[(min_x + max_x) * 0.5, (min_y + max_y) * 0.5]
|
||||
} else {
|
||||
[0.0, 0.0]
|
||||
};
|
||||
let boundary_f32: Vec<[f32; 2]> = boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
[(x - world_origin[0]) as f32, (y - world_origin[1]) as f32]
|
||||
} else {
|
||||
[f32::NAN, f32::NAN]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(HatchModel {
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
pattern,
|
||||
name,
|
||||
color,
|
||||
angle_offset: dxf.pattern_angle as f32,
|
||||
scale: dxf.pattern_scale as f32,
|
||||
world_origin,
|
||||
vp_scissor: None,
|
||||
})
|
||||
}
|
||||
@@ -2812,6 +2849,7 @@ impl Scene {
|
||||
color,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
world_origin: [0.0; 2],
|
||||
vp_scissor: None,
|
||||
}
|
||||
}
|
||||
@@ -2822,10 +2860,12 @@ impl Scene {
|
||||
model.pattern,
|
||||
crate::scene::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
let [wx, wy] = model.world_origin;
|
||||
let verts: Vec<Vector2> = model
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| Vector2::new(x as f64, y as f64))
|
||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
|
||||
.collect();
|
||||
let edge = PolylineEdge::new(verts, true);
|
||||
let mut path = BoundaryPath::external();
|
||||
|
||||
@@ -171,11 +171,14 @@ fn draw_hatch(frame: &mut canvas::Frame, hatch: &HatchModel, to_px: &impl Fn(f32
|
||||
let [r, g, b, a] = hatch.color;
|
||||
let color = Color { r, g, b, a };
|
||||
|
||||
// Reconstruct offset-rel WCS from stored small offsets + f64 anchor.
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let path = canvas::Path::new(|builder| {
|
||||
let first = to_px(hatch.boundary[0][0], hatch.boundary[0][1]);
|
||||
let first = to_px(hatch.boundary[0][0] + ox, hatch.boundary[0][1] + oy);
|
||||
builder.move_to(first);
|
||||
for &[x, y] in &hatch.boundary[1..] {
|
||||
builder.line_to(to_px(x, y));
|
||||
builder.line_to(to_px(x + ox, y + oy));
|
||||
}
|
||||
builder.close();
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ impl HatchVertex {
|
||||
|
||||
// ── Uniform structs ───────────────────────────────────────────────────────
|
||||
|
||||
/// Per-hatch parameters (binding 0). Must be 64 bytes.
|
||||
/// Per-hatch parameters (binding 0). Must be 80 bytes (16-byte aligned).
|
||||
///
|
||||
/// `mode` encoding:
|
||||
/// 0 → Pattern (families in FamilyBatchData)
|
||||
@@ -48,17 +48,24 @@ impl HatchVertex {
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct HatchUniformData {
|
||||
pub color: [f32; 4], // 0: primary RGBA / gradient start
|
||||
pub color2: [f32; 4], // 16: gradient end color
|
||||
pub mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
|
||||
pub vertex_count: u32, // 36: boundary vertex count
|
||||
pub angle_offset: f32, // 40: pattern rotation (radians)
|
||||
pub scale: f32, // 44: pattern scale multiplier
|
||||
pub grad_cos: f32, // 48: gradient direction cos
|
||||
pub grad_sin: f32, // 52: gradient direction sin
|
||||
pub grad_min: f32, // 56: gradient proj_min
|
||||
pub grad_range: f32, // 60: gradient proj_range
|
||||
} // total 64 bytes
|
||||
pub color: [f32; 4], // 0: primary RGBA / gradient start
|
||||
pub color2: [f32; 4], // 16: gradient end color
|
||||
pub mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
|
||||
pub vertex_count: u32, // 36: boundary vertex count
|
||||
pub angle_offset: f32, // 40: pattern rotation (radians)
|
||||
pub scale: f32, // 44: pattern scale multiplier
|
||||
pub grad_cos: f32, // 48: gradient direction cos
|
||||
pub grad_sin: f32, // 52: gradient direction sin
|
||||
pub grad_min: f32, // 56: gradient proj_min
|
||||
pub grad_range: f32, // 60: gradient proj_range
|
||||
pub origin: [f32; 2], // 64: hatch-local origin (boundary AABB centre).
|
||||
// // The pattern fragment shader subtracts this
|
||||
// // from xz before the perp/perp_step quotient
|
||||
// // so f32 doesn't catastrophically cancel when
|
||||
// // world coords are large and pattern spacing
|
||||
// // is small (e.g. UTM drawing + sub-mm hatch).
|
||||
pub _pad: [f32; 2], // 72: 16-byte alignment
|
||||
} // total 80 bytes
|
||||
|
||||
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
|
||||
#[repr(C)]
|
||||
@@ -153,7 +160,68 @@ impl HatchGpu {
|
||||
};
|
||||
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
|
||||
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
|
||||
let (x0, x1, y0, y1) = (min_x - pad, max_x + pad, min_y - pad, max_y + pad);
|
||||
// ── Pattern phase anchor.
|
||||
//
|
||||
// Boundary verts are already stored as small f32 offsets from
|
||||
// `model.world_origin` (an f64 anchor near the AABB centre), so
|
||||
// the in_polygon ray-cast and the pattern math both run at full
|
||||
// f32 precision in the fragment shader. The remaining question
|
||||
// is *which* offset-rel point we hand to the vertex shader as
|
||||
// `h.origin` (so the quad lands at the correct WCS position
|
||||
// after `view_proj * (v.pos + h.origin)`).
|
||||
//
|
||||
// For pattern alignment across adjacent hatches with the same
|
||||
// family, snap `h.origin` to the family's perp/along grid in
|
||||
// f64. Without the snap, each hatch's pattern phase is per-hatch
|
||||
// and stripes don't continue across hatch boundaries (you see
|
||||
// pattern misalignment along a road etc).
|
||||
let mut origin = model.world_origin;
|
||||
if let HatchPattern::Pattern(families) = &model.pattern {
|
||||
if let Some(fam) = families.first() {
|
||||
// Must mirror the shader's pattern math exactly. QCAD PAT
|
||||
// convention (matching build_family_batch): `dy` is the
|
||||
// perpendicular spacing and `dx` is the along-line phase
|
||||
// shift, both in family-local space. Multiplied by
|
||||
// `model.scale` in the fragment shader. Combined angle
|
||||
// (family + pattern_angle) rotates the perp/along axes.
|
||||
let angle = (fam.angle_deg.to_radians() + model.angle_offset) as f64;
|
||||
let cos_a = angle.cos();
|
||||
let sin_a = angle.sin();
|
||||
let scale = model.scale as f64;
|
||||
let perp_step = fam.dy as f64 * scale;
|
||||
let along_step = fam.dx as f64 * scale;
|
||||
let o_perp = -origin[0] * sin_a + origin[1] * cos_a;
|
||||
let o_along = origin[0] * cos_a + origin[1] * sin_a;
|
||||
let snapped_perp = if perp_step.abs() > 1e-12 {
|
||||
(o_perp / perp_step).round() * perp_step
|
||||
} else {
|
||||
o_perp
|
||||
};
|
||||
let snapped_along = if along_step.abs() > 1e-12 {
|
||||
(o_along / along_step).round() * along_step
|
||||
} else {
|
||||
o_along
|
||||
};
|
||||
origin = [
|
||||
-snapped_perp * sin_a + snapped_along * cos_a,
|
||||
snapped_perp * cos_a + snapped_along * sin_a,
|
||||
];
|
||||
}
|
||||
}
|
||||
// Boundary buffer + quad now live in "snap-shifted" hatch-local
|
||||
// coords: subtract any drift between model.world_origin and the
|
||||
// snapped origin so the in_polygon ray-cast in fragment shader
|
||||
// (which reads the boundary verts straight) lines up with v.xz.
|
||||
let drift = [
|
||||
(model.world_origin[0] - origin[0]) as f32,
|
||||
(model.world_origin[1] - origin[1]) as f32,
|
||||
];
|
||||
let (x0, x1, y0, y1) = (
|
||||
min_x + drift[0] - pad,
|
||||
max_x + drift[0] + pad,
|
||||
min_y + drift[1] - pad,
|
||||
max_y + drift[1] + pad,
|
||||
);
|
||||
|
||||
let quad = [
|
||||
HatchVertex {
|
||||
@@ -187,13 +255,13 @@ impl HatchGpu {
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
|
||||
// ── Gradient: projection range ───────────────────────────────────
|
||||
// ── Gradient: projection range (in snapped-local space) ──────────
|
||||
let (grad_min, grad_range) = if mode == 2 {
|
||||
let projs: Vec<f32> = model
|
||||
.boundary
|
||||
.iter()
|
||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||
.map(|&[x, y]| x * grad_cos + y * grad_sin)
|
||||
.map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin)
|
||||
.collect();
|
||||
if projs.is_empty() {
|
||||
(0.0, 1.0)
|
||||
@@ -219,9 +287,11 @@ impl HatchGpu {
|
||||
grad_sin,
|
||||
grad_min,
|
||||
grad_range,
|
||||
origin: [origin[0] as f32, origin[1] as f32],
|
||||
_pad: [0.0; 2],
|
||||
};
|
||||
|
||||
// ── BoundaryData ─────────────────────────────────────────────────
|
||||
// ── BoundaryData (in snapped-local space) ────────────────────────
|
||||
let mut boundary_data = BoundaryData {
|
||||
verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS],
|
||||
};
|
||||
@@ -231,7 +301,13 @@ impl HatchGpu {
|
||||
.take(MAX_HATCH_BOUNDARY_VERTS)
|
||||
.enumerate()
|
||||
{
|
||||
boundary_data.verts[i] = [x, y, 0.0, 0.0];
|
||||
// Preserve NaN separators — they signal path breaks to the
|
||||
// in_polygon ray-cast, must not be shifted into finite numbers.
|
||||
if x.is_finite() && y.is_finite() {
|
||||
boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0];
|
||||
} else {
|
||||
boundary_data.verts[i] = [f32::NAN, f32::NAN, 0.0, 0.0];
|
||||
}
|
||||
}
|
||||
|
||||
// ── FamilyBatchData ───────────────────────────────────────────────
|
||||
|
||||
@@ -220,9 +220,13 @@ impl Pipeline {
|
||||
});
|
||||
|
||||
// ── Hatch pipeline ─────────────────────────────────────────────────
|
||||
let hatch_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||||
// binding 0 (HatchUniforms) is read by the vertex shader too — it
|
||||
// pulls `origin` to undo the CPU-side hatch-local pre-shift when
|
||||
// computing clip position. bindings 1 (Boundary) and 2
|
||||
// (FamilyBatch) stay fragment-only.
|
||||
let hatch_entry = |binding: u32, vis: wgpu::ShaderStages| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
visibility: vis,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
@@ -230,9 +234,15 @@ impl Pipeline {
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
let frag = wgpu::ShaderStages::FRAGMENT;
|
||||
let vert_frag = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
|
||||
let hatch_bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("hatch.bgl1"),
|
||||
entries: &[hatch_entry(0), hatch_entry(1), hatch_entry(2)],
|
||||
entries: &[
|
||||
hatch_entry(0, vert_frag),
|
||||
hatch_entry(1, frag),
|
||||
hatch_entry(2, frag),
|
||||
],
|
||||
});
|
||||
|
||||
let hatch_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
|
||||
+22
-2
@@ -39,6 +39,12 @@ struct HatchUniforms {
|
||||
grad_sin: f32, // 52: gradient direction sin
|
||||
grad_min: f32, // 56: gradient proj_min
|
||||
grad_range: f32, // 60: gradient proj_range
|
||||
origin: vec2<f32>, // 64: hatch-local pattern origin (boundary
|
||||
// // centre). Pattern math runs against
|
||||
// // `xz - origin` so the f32 modulo doesn't
|
||||
// // catastrophically cancel when world coords
|
||||
// // are large and pattern spacing is small.
|
||||
_pad: vec2<f32>, // 72: 16-byte alignment
|
||||
}
|
||||
@group(1) @binding(0) var<uniform> h: HatchUniforms;
|
||||
|
||||
@@ -81,7 +87,16 @@ struct VOut {
|
||||
|
||||
@vertex fn vs_main(v: VIn) -> VOut {
|
||||
var o: VOut;
|
||||
o.clip = u.view_proj * vec4<f32>(v.pos, 1.0);
|
||||
// v.pos comes in pre-shifted to hatch-local space (CPU subtracted
|
||||
// h.origin from the quad). Add origin back inside the view_proj
|
||||
// multiply to land at the correct world-space clip position. The
|
||||
// f32+f32 addition can lose sub-mm precision at very far hatches
|
||||
// but the result feeds view_proj for screen NDC where sub-pixel
|
||||
// jitter is invisible. The local-space `xz` we export to the
|
||||
// fragment shader stays at full f32 precision so the in_polygon /
|
||||
// pattern math doesn't catastrophically cancel.
|
||||
let world = vec3<f32>(v.pos.x + h.origin.x, v.pos.y + h.origin.y, v.pos.z);
|
||||
o.clip = u.view_proj * vec4<f32>(world, 1.0);
|
||||
o.xz = vec2<f32>(v.pos.x, v.pos.y);
|
||||
return o;
|
||||
}
|
||||
@@ -213,7 +228,12 @@ fn check_family(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Pattern: evaluate each line family.
|
||||
// 4. Pattern: v.xz is already in hatch-local space (CPU pre-shifted
|
||||
// quad + BoundaryData by h.origin). The PAT family origin
|
||||
// (typically (0,0)) is treated as relative to h.origin, so
|
||||
// pattern phase is per-hatch rather than world-aligned — that
|
||||
// trade-off is the price of f32 precision at large drawing
|
||||
// extents.
|
||||
let cos_off = cos(h.angle_offset);
|
||||
let sin_off = sin(h.angle_offset);
|
||||
for (var i = 0u; i < f.n_families; i++) {
|
||||
|
||||
Reference in new issue
Block a user