fix(hatch): anchor prebaked pattern origin near the geometry

Prebaked hatch fills anchored each pattern family at its authored
base point minus world_origin. On UTM-scale drawings that offset is
~1e6, which broke the fill two ways:

  - the shader consumes x0/y0 as f32, so ~0.5 m quantization shredded
    the pattern phase; and
  - it evaluated the pattern that far from its origin, where the
    multi-family aggregate patterns (AR-CONC, AR-SAND, GRAVEL, ...) —
    effectively quasi-periodic — dissolve from their interlocking
    stones into scattered dashes.

Boundaries were unaffected throughout; they ride the double-single
relative-to-eye path.

Fold the origin's offset from world_origin down to a small,
coherence-safe remainder on a `spacing * 64` grid, applied as one
common fold so every family keeps the relative phase that forms the
stones. Because the origin grip / Origin X/Y edit shifts every base
point by the same delta, the sub-grid remainder tracks it 1:1, so the
grip still moves the fill; a grid crossing needs a full `spacing * 64`
drag and is never hit by realistic alignment drags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-10 20:05:15 +03:00
commit 0cd861cc76

View file

@ -1372,17 +1372,48 @@ impl Scene {
[0.0, 0.0]
};
// Anchor each prebaked family at its stored pattern origin (per-line
// `base_point`), expressed relative to `world_origin` so it shares the
// boundary's coordinate frame. Left at 0 the pattern tiles from the
// boundary centre instead: invisible for a solid infinite-line family
// but the wrong phase for dashed / offset patterns and for a user-moved
// origin (Origin X/Y property + origin grip).
// Anchor the prebaked families near the geometry, tracking the pattern
// origin. The DWG base points sit at the pattern's *authored* origin,
// which on a UTM drawing can be ~1e6 units from the hatch. Handing the
// shader `base - world_origin` (~1e6) is wrong two ways: it consumes
// `x0/y0` as f32 (so ~0.5 m quantization shreds the phase — boundaries
// are unaffected, they ride the double-single relative-to-eye path), AND
// it evaluates the pattern that far from its origin. Multi-family
// aggregate patterns (AR-CONC, AR-SAND, GRAVEL, …) are effectively
// quasi-periodic: their families only cohere into the intended
// stones/grains near their shared origin and dissolve into scattered
// dashes at large offsets.
//
// So fold the origin's offset from `world_origin` down to a small,
// coherence-safe remainder on a grid of `spacing * 64` (many multiples
// of the pattern spacing — well inside the coherence range yet far
// larger than any realistic grip drag). Applying ONE common fold (from
// the reference line) preserves each family's relative phase, which is
// what forms the stones. Because the origin grip / Origin X/Y edit
// shifts every base point by the same delta, the sub-grid remainder
// tracks that delta 1:1, so the grip still moves the fill; a grid
// crossing (a whole `spacing * 64` drag) is never hit in practice.
if prebaked {
if let model::hatch_model::HatchPattern::Pattern(fams) = &mut pattern {
for (fam, ln) in fams.iter_mut().zip(dxf.pattern.lines.iter()) {
fam.x0 = (ln.base_point.x - world_origin[0]) as f32;
fam.y0 = (ln.base_point.y - world_origin[1]) as f32;
if let Some(rb) = dxf.pattern.lines.first().map(|l| l.base_point) {
let spacing = dxf
.pattern
.lines
.iter()
.map(|l| l.offset.length())
.fold(0.0_f64, f64::max)
.max(1e-6);
let cell = spacing * 64.0;
let fold = |v: f64, o: f64| -> f64 {
let d = v - o;
d - (d / cell).round() * cell
};
let ox = fold(rb.x, world_origin[0]);
let oy = fold(rb.y, world_origin[1]);
for (fam, ln) in fams.iter_mut().zip(dxf.pattern.lines.iter()) {
fam.x0 = (ln.base_point.x - rb.x + ox) as f32;
fam.y0 = (ln.base_point.y - rb.y + oy) as f32;
}
}
}
}