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 SevenandClaude Fable 5 committed 2026-07-21 12:51:08 +03:00
1 parent 9bfcad18b9
commit 930d8c51ea
8 files changed
+290 -32

No files matched your search

+20 -3
View File
@@ -59,7 +59,7 @@ struct HatchInstance {
family_count: u32,
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip
_pad1: u32,
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
_pad2: u32,
}
@@ -272,12 +272,29 @@ fn check_family(
return inst.color;
} else if inst.mode == 2u {
let proj = v.xz.x * inst.grad_cos + v.xz.y * inst.grad_sin;
let t = clamp((proj - inst.grad_min) / inst.grad_range, 0.0, 1.0);
var t = clamp((proj - inst.grad_min) / inst.grad_range, 0.0, 1.0);
// Shape profile: cylinder mirrors around the middle, curved eases in.
let k = inst.grad_kind & 15u;
if k == 1u {
t = 1.0 - abs(2.0 * t - 1.0);
} else if k == 4u {
t = t * t;
}
if (inst.grad_kind & 16u) != 0u {
t = 1.0 - t;
}
return mix(inst.color, inst.color2, t);
} else if inst.mode == 3u {
// Radial gradient: centre is (grad_cos, grad_sin), radius is grad_range.
let d = length(v.xz - vec2<f32>(inst.grad_cos, inst.grad_sin));
let t = clamp(d / inst.grad_range, 0.0, 1.0);
var t = clamp(d / inst.grad_range, 0.0, 1.0);
// Hemispherical shades faster near the centre (dome profile).
if (inst.grad_kind & 15u) == 3u {
t = sqrt(t);
}
if (inst.grad_kind & 16u) != 0u {
t = 1.0 - t;
}
return mix(inst.color, inst.color2, t);
}
+23 -5
View File
@@ -222,16 +222,34 @@ fn check_family(
@fragment fn fs_main(v: VOut) -> @location(0) vec4<f32> {
if !in_polygon(v.xz) { discard; }
if h.mode == 1u {
let base_mode = h.mode & 0xFFu;
let gk = (h.mode >> 8u) & 15u;
let ginv = ((h.mode >> 8u) & 16u) != 0u;
if base_mode == 1u {
return h.color;
} else if h.mode == 2u {
} else if base_mode == 2u {
let proj = v.xz.x * h.grad_cos + v.xz.y * h.grad_sin;
let t = clamp((proj - h.grad_min) / h.grad_range, 0.0, 1.0);
var t = clamp((proj - h.grad_min) / h.grad_range, 0.0, 1.0);
// Shape profile: cylinder mirrors around the middle, curved eases in.
if gk == 1u {
t = 1.0 - abs(2.0 * t - 1.0);
} else if gk == 4u {
t = t * t;
}
if ginv {
t = 1.0 - t;
}
return mix(h.color, h.color2, t);
} else if h.mode == 3u {
} else if base_mode == 3u {
// Radial gradient: centre is (grad_cos, grad_sin), radius is grad_range.
let d = length(v.xz - vec2<f32>(h.grad_cos, h.grad_sin));
let t = clamp(d / h.grad_range, 0.0, 1.0);
var t = clamp(d / h.grad_range, 0.0, 1.0);
if gk == 3u {
t = sqrt(t);
}
if ginv {
t = 1.0 - t;
}
return mix(h.color, h.color2, t);
}