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 from e7675be
and the scissor plumbing from fbbac42 still work — those touched
different fields of the same struct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-16 08:41:23 +03:00
commit 172c538159
8 changed files with 238 additions and 63 deletions

View file

@ -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,