SPHERICAL/HEMISPHERICAL gradients painted colour 1 at the centre;
AutoCAD's convention is colour 1 at the rim and colour 2 at the centre
(the INV variants swap them back). Both the native and the WebGL2
shader now mix the radial branch outside-in; linear/cylinder/curved
are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hatch/wipeout boundary sub-loops were separated by NaN vertices that the
fill shaders detected with `p.x == p.x`. Intel Mesa folds that NaN
self-compare to `true` under fast math, so separators read as real
vertices, the ray-cast crossing count broke, and fills bled outside
their boundary — only on Intel iGPUs. GPU uploads now rewrite the
separators to a finite 1e30 sentinel (GPU_BOUNDARY_SEP) and the shaders
test `abs(x) < 1e29`, which every driver evaluates identically. The
CPU-side NaN representation is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tint (Chrome/Edge WebGPU) rejects derivative builtins reached after the
family loop's non-uniform early return, invalidating hatch_web/wipeout
shaders on Windows. Take dpdx/dpdy(xz) once at fs_main top and rotate
into the family frame in check_family — same math, uniform flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GRADIENT silently degraded to a plain hatch: add_hatch rebuilt the
render model from the DXF entity but never encoded the gradient on it,
so the committed fill lost its gradient entirely. The entity now
carries the full gradient record (name, angle, colour stops,
solid-fill flag), and the standard shape set became selectable:
Linear / Cylinder / Spherical / Hemispherical / Curved plus an Invert
toggle, offered as prompt buttons on the GRADIENT command and as a
Gradient type dropdown + Invert box in the properties panel. All nine
standard DXF names round-trip (inverted linear persists by swapping
the colour stops), and the shaders shape each kind distinctly —
mirrored cylinder, eased curve, dome-profiled hemisphere — on both the
native and web pipelines without changing the GPU struct sizes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A gradient hatch was drawn linear from the entity colour to its second
stop, so a red->yellow spherical fill came out as a pale linear wash. Two
fixes:
- Colour: start from the gradient's first stop, not the entity colour.
hatch_model_from_dxf already baked stop-0 in; stop synced_hatch_models
from clobbering it with the entity's resolved colour for gradient fills.
- Shape: spherical / hemispherical / curved gradient names now render as a
radial fill (new shader mode 3, centred on the boundary with radius to the
farthest vertex) instead of linear. Native and web shaders both updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The batched path is the sole hatch renderer and has no family cap, yet it
was named hatch_batched_gpu while the old capped per-primitive renderer
kept the hatch_gpu / MAX_FAMILIES name — which reads as if visible hatch
fills are truncated at 16 families (they are not). Swap the names so the
active renderer is hatch_gpu.rs / hatch.wgsl and the legacy one, now used
only for wipeout masks, is wipeout_gpu.rs / wipeout.wgsl. MAX_FAMILIES now
lives only in the wipeout module. Pure rename; no behaviour change.
Refs #323.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GPU draws exclusively via the rotation-only `view_rot` + eye double-
single; the full view-projection survived only to project AABBs in the
CPU frustum-cull / scissor / LOD passes. Migrate those to the same
relative-to-eye math (project `point - eye` through `view_rot`) — pixel-
equivalent but precise at UTM scale — then drop the dead uniform:
- pipeline cull/scissor/LOD helpers take (view_rot, eye) instead of the
full view_proj; render.rs reconstructs eye from the uniform's
eye_high/eye_low and passes the cropped view_rot.
- Remove the `view_proj` field from the shared Uniforms struct and all six
shader structs that only declared it for layout (none read it), and
delete Camera::view_proj(). The struct shrinks by one mat4 on both
sides, keeping the Rust / WGSL layouts in lock-step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No GPU pipeline reads camera_pos any more — lighting and all transforms
moved to the relative-to-eye path (view_rot + eye_high/eye_low). Remove
the field from the shared Uniforms and every shader struct, plus the now
unused Camera::position_vec4. view_proj stays: the CPU frustum-cull /
scissor / LOD passes still project offset-relative AABBs with it (it is
no longer part of any GPU draw).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The hatch anchor (world_origin / pattern origin) was cast to a single
f32, quantizing UTM-scale fills to ~0.5 m and reintroducing jitter the
wire path no longer had. Both the batched and per-hatch pipelines now
carry the anchor as a high/low f32 pair (HatchInstance.world_origin_low,
HatchUniformData.origin_low) and reconstruct the eye-relative position in
the shader as (origin_high - eye_high) + (local + origin_low - eye_low).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch the remaining world-space shaders (mesh, image, face3d, hatch,
hatch_batched) to the same relative-to-eye transform the wire pipeline
uses: subtract the eye (high+low f32) per vertex and multiply by the
rotation-only view-projection. Each shader's Uniforms struct is widened
to the full shared layout so the appended RTE fields land at the correct
offsets. Math is unchanged (proj·R·(pos−eye)); large coordinates no
longer jitter on camera movement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The point-in-polygon test in both hatch shaders walked edges
(v0,v1)…(vn-2,vn-1) but never the closing edge vn-1→v0, so an unclosed
boundary miscounted crossings and the fill bled outside the shape. SOLID
entities hit this — their four corners are not repeated — making color-wheel
fills spike past their bounds. Close each sub-loop (at NaN separators and at
the end); already-closed loops get a degenerate closing edge that crosses
nothing, so they're unaffected. (#140)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A zero-length dash entry is a dot (e.g. the DOTS pattern), but every
render path treated only positive entries as marks and skipped it, so
dot patterns drew nothing.
- GPU shaders: light exactly the one pixel containing each dot's lattice
centre. The dot grid rotates with the pattern angle while the lit pixel
stays a steady single pixel, so dots no longer flicker at oblique
angles or zoom (an earlier coverage/round test still aliased).
- PDF export (pattern_segments): walk the dash sequence with absolute
phase and emit a short mark for each dot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both hatch fragment shaders ended their `fs_main` with a bare
`discard;` after the pattern-match for-loop falls through without
matching. WGSL accepts this — discard kills the fragment, so there is
nothing left to return — and naga (the Linux/Vulkan path wgpu defaults
to) lowers it without complaint. DX12's FXC (the default Windows
compiler) is stricter and rejects it with `X3507: 'fs_main': Not all
control paths return a value`, taking the entire pipeline create-down
on every drawing open.
Append an unreachable `return vec4<f32>(0.0);` after each terminal
`discard;` so every control path syntactically ends in a return. The
return never executes at runtime; it only exists to make FXC's
all-paths-return analysis succeed. Naga still accepts the shader
unchanged, so the Vulkan / Metal / WebGPU paths are unaffected.
Reported by @Ward-Vandepitte (Windows 11, DX12, wgpu 27.0.1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Hatch fills drew the full per-family pattern loop every frame regardless
of zoom. Two LOD tiers, neither of which existed:
* **Shader-side solid substitute.** When the densest line family's
spacing × scale projects to less than 2 px, individual lines blur
into a solid color and the per-fragment family loop is wasted ALU
(and produces moiré). Pass `world_per_pixel` through Uniforms; the
hatch fragment shader now returns `h.color` directly in that case.
* **CPU-side sub-pixel skip.** When the entire hatch AABB projects
to less than 2 px, drop the draw call. Adds `world_aabb` to
HatchGpu plus a per-frame `compute_hatch_lod` pass (mirrors the
existing scissor pass) and `hatch_skip_flags`. The render pass
skips flagged hatches.
Factored a shared `aabb_below_pixel` helper for future LOD callers.
The Uniforms layout is unchanged in size (96 B) — the trailing
`_pad: vec2<f32>` becomes `world_per_pixel: f32, _pad: f32`, so wgsl
modules that don't read the new field keep working with their old
`_pad: vec2<f32>` declarations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>