feat(gradient): selectable gradient types + working creation

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>
This commit is contained in:
Hakan Seven 2026-07-21 12:51:08 +03:00
commit 930d8c51ea
8 changed files with 290 additions and 32 deletions

View file

@ -85,7 +85,8 @@ pub struct HatchInstance {
/// an instance whose boundary failed to tessellate); 0 = the rasterized
/// triangles already bound the fill, skip the test.
pub poly_test: u32, // 116
pub _pad1: u32, // 120
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
pub grad_kind: u32, // 120
pub _pad2: u32, // 124
}
@ -223,10 +224,12 @@ impl HatchGpu {
let family_offset = families.len() as u32;
let mut family_count = 0u32;
let mut grad_kind = 0u32;
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
HatchPattern::Gradient { angle_deg, color2, radial } => {
if *radial {
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
if kind.radial() {
// Radial fill: the boundary is stored relative to its
// centre (`world_origin`), so the centre is the local
// origin; radius = the farthest boundary vertex. mode 3.
@ -319,7 +322,7 @@ impl HatchGpu {
family_count,
draw_depth: h.draw_depth,
poly_test: 1,
_pad1: 0,
grad_kind,
_pad2: 0,
});
continue;
@ -367,7 +370,7 @@ impl HatchGpu {
// boundary complexity, so a hatch-dense drawing can't overflow
// the device buffer limit. Each instance draws its AABB quad.
poly_test: 1,
_pad1: 0,
grad_kind,
_pad2: 0,
});
}

View file

@ -105,16 +105,19 @@ impl HatchWebGpu {
bgl1: &wgpu::BindGroupLayout,
) -> Self {
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
// The gradient shape (kind + invert bit) rides in the mode's high
// bits — the 96-byte uniform has no spare slot.
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
HatchPattern::Gradient { angle_deg, color2, radial } => {
if *radial {
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
let gk = (kind.shader_kind() | if *invert { 16 } else { 0 }) << 8;
if kind.radial() {
// Radial: centre is the local origin; grad_cos/sin unused.
(3u32, *color2, 0.0, 0.0)
(3u32 | gk, *color2, 0.0, 0.0)
} else {
let r = angle_deg.to_radians();
(2u32, *color2, r.cos(), r.sin())
(2u32 | gk, *color2, r.cos(), r.sin())
}
}
};