fix(hatch): radial gradients and correct gradient stop colours
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>
This commit is contained in:
parent
69354eca07
commit
fc29139056
8 changed files with 93 additions and 23 deletions
|
|
@ -395,6 +395,7 @@ impl GradientCommand {
|
||||||
pattern: HatchPattern::Gradient {
|
pattern: HatchPattern::Gradient {
|
||||||
angle_deg: 0.0,
|
angle_deg: 0.0,
|
||||||
color2: [0.18, 0.18, 0.18, 0.0],
|
color2: [0.18, 0.18, 0.18, 0.0],
|
||||||
|
radial: false,
|
||||||
},
|
},
|
||||||
name: "LINEAR".into(),
|
name: "LINEAR".into(),
|
||||||
color: [0.30, 0.60, 0.95, 0.80],
|
color: [0.30, 0.60, 0.95, 0.80],
|
||||||
|
|
|
||||||
|
|
@ -626,7 +626,12 @@ impl Scene {
|
||||||
// emitted first so LessEqual layering keeps it underneath.
|
// emitted first so LessEqual layering keeps it underneath.
|
||||||
let mut backdrop: Option<HatchModel> = None;
|
let mut backdrop: Option<HatchModel> = None;
|
||||||
if let Some(e) = entity {
|
if let Some(e) = entity {
|
||||||
m.color = self.render_style(e).0;
|
// A gradient's colour is its first stop (already baked into
|
||||||
|
// the cached model); only solid / pattern fills take the
|
||||||
|
// entity's resolved colour.
|
||||||
|
if !matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
|
||||||
|
m.color = self.render_style(e).0;
|
||||||
|
}
|
||||||
if let EntityType::Hatch(dxf) = e {
|
if let EntityType::Hatch(dxf) = e {
|
||||||
if let Some(bg) = crate::entities::hatch::background_color(dxf) {
|
if let Some(bg) = crate::entities::hatch::background_color(dxf) {
|
||||||
let mut b = m.clone();
|
let mut b = m.clone();
|
||||||
|
|
@ -1421,16 +1426,27 @@ impl Scene {
|
||||||
&& !dxf.gradient_color.is_enabled()
|
&& !dxf.gradient_color.is_enabled()
|
||||||
&& !dxf.pattern.lines.is_empty();
|
&& !dxf.pattern.lines.is_empty();
|
||||||
|
|
||||||
|
// The gradient's first stop is the fill's start colour (not the
|
||||||
|
// entity colour); capture it so the HatchModel draws stop-0 → stop-1.
|
||||||
|
let mut gradient_color1: Option<[f32; 4]> = None;
|
||||||
let mut pattern = if dxf.gradient_color.is_enabled() {
|
let mut pattern = if dxf.gradient_color.is_enabled() {
|
||||||
let color2 = dxf
|
let stop = |i: usize| {
|
||||||
.gradient_color
|
dxf.gradient_color.colors.get(i).and_then(|e| e.color.rgb()).map(
|
||||||
.colors
|
|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0],
|
||||||
.get(1)
|
)
|
||||||
.and_then(|e| e.color.rgb())
|
};
|
||||||
.map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
|
gradient_color1 = stop(0);
|
||||||
.unwrap_or(color);
|
let color2 = stop(1).unwrap_or(color);
|
||||||
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
||||||
model::hatch_model::HatchPattern::Gradient { angle_deg, color2 }
|
// Spherical / hemispherical / curved names are radial fills; linear
|
||||||
|
// and cylinder names run along the angle.
|
||||||
|
let n = dxf.gradient_color.name.to_ascii_uppercase();
|
||||||
|
let radial = n.contains("SPHERICAL") || n.contains("CURVED");
|
||||||
|
model::hatch_model::HatchPattern::Gradient {
|
||||||
|
angle_deg,
|
||||||
|
color2,
|
||||||
|
radial,
|
||||||
|
}
|
||||||
} else if dxf.is_solid {
|
} else if dxf.is_solid {
|
||||||
model::hatch_model::HatchPattern::Solid
|
model::hatch_model::HatchPattern::Solid
|
||||||
} else if prebaked {
|
} else if prebaked {
|
||||||
|
|
@ -1571,7 +1587,9 @@ impl Scene {
|
||||||
boundary_wcs: None,
|
boundary_wcs: None,
|
||||||
pattern,
|
pattern,
|
||||||
name,
|
name,
|
||||||
color,
|
// A gradient starts from its first stop; other fills use the
|
||||||
|
// entity colour.
|
||||||
|
color: gradient_color1.unwrap_or(color),
|
||||||
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
|
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
|
||||||
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
|
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
|
||||||
world_origin,
|
world_origin,
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,14 @@ pub enum HatchPattern {
|
||||||
Solid,
|
Solid,
|
||||||
/// One or more line families (PAT format).
|
/// One or more line families (PAT format).
|
||||||
Pattern(Vec<PatFamily>),
|
Pattern(Vec<PatFamily>),
|
||||||
/// Linear gradient from `color` to `color2` along `angle_deg`.
|
/// Two-stop gradient from `color` to `color2`. Linear along `angle_deg`
|
||||||
Gradient { angle_deg: f32, color2: [f32; 4] },
|
/// unless `radial`, in which case it runs from the boundary centre outward
|
||||||
|
/// (spherical / hemispherical / curved fills).
|
||||||
|
Gradient {
|
||||||
|
angle_deg: f32,
|
||||||
|
color2: [f32; 4],
|
||||||
|
radial: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A hatched region defined by a closed polygon boundary.
|
/// A hatched region defined by a closed polygon boundary.
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,21 @@ impl HatchGpu {
|
||||||
|
|
||||||
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
|
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::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
|
||||||
HatchPattern::Gradient { angle_deg, color2 } => {
|
HatchPattern::Gradient { angle_deg, color2, radial } => {
|
||||||
let r = angle_deg.to_radians();
|
if *radial {
|
||||||
// Gradient projection range (proj_min / proj_range) —
|
// Radial fill: the boundary is stored relative to its
|
||||||
// computed at upload time, identical to per-hatch path.
|
// centre (`world_origin`), so the centre is the local
|
||||||
let (gmin, gmax) = boundary_projection_range(&h.boundary, r);
|
// origin; radius = the farthest boundary vertex. mode 3.
|
||||||
let grange = (gmax - gmin).max(1.0);
|
let radius = radial_radius(&h.boundary);
|
||||||
(2u32, *color2, r.cos(), r.sin(), gmin, grange)
|
(3u32, *color2, 0.0, 0.0, 0.0, radius)
|
||||||
|
} else {
|
||||||
|
let r = angle_deg.to_radians();
|
||||||
|
// Gradient projection range (proj_min / proj_range) —
|
||||||
|
// computed at upload time, identical to per-hatch path.
|
||||||
|
let (gmin, gmax) = boundary_projection_range(&h.boundary, r);
|
||||||
|
let grange = (gmax - gmin).max(1.0);
|
||||||
|
(2u32, *color2, r.cos(), r.sin(), gmin, grange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HatchPattern::Pattern(fams) => {
|
HatchPattern::Pattern(fams) => {
|
||||||
for fam in fams {
|
for fam in fams {
|
||||||
|
|
@ -559,6 +567,18 @@ fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) {
|
||||||
(lo, hi)
|
(lo, hi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Radius of a radial gradient — the distance from the boundary centre (the
|
||||||
|
/// local origin, since vertices are stored relative to it) to the farthest
|
||||||
|
/// vertex, so `t = 1` (the end colour) reaches the corners.
|
||||||
|
fn radial_radius(boundary: &[[f32; 2]]) -> f32 {
|
||||||
|
boundary
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
||||||
|
.map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
|
||||||
|
.fold(0.0_f32, f32::max)
|
||||||
|
.max(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
// PatFamily is re-exported by hatch_model so we don't need to import
|
// PatFamily is re-exported by hatch_model so we don't need to import
|
||||||
// it explicitly anywhere else — but rust needs the type referenced to
|
// it explicitly anywhere else — but rust needs the type referenced to
|
||||||
// confirm the layout assumption above.
|
// confirm the layout assumption above.
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,14 @@ impl HatchWebGpu {
|
||||||
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
||||||
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Gradient { angle_deg, color2 } => {
|
HatchPattern::Gradient { angle_deg, color2, radial } => {
|
||||||
let r = angle_deg.to_radians();
|
if *radial {
|
||||||
(2u32, *color2, r.cos(), r.sin())
|
// Radial: centre is the local origin; grad_cos/sin unused.
|
||||||
|
(3u32, *color2, 0.0, 0.0)
|
||||||
|
} else {
|
||||||
|
let r = angle_deg.to_radians();
|
||||||
|
(2u32, *color2, r.cos(), r.sin())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -181,6 +186,16 @@ impl HatchWebGpu {
|
||||||
// Floor matches the desktop hatch renderer (hatch_gpu.rs).
|
// Floor matches the desktop hatch renderer (hatch_gpu.rs).
|
||||||
(proj_min, (proj_max - proj_min).max(1.0))
|
(proj_min, (proj_max - proj_min).max(1.0))
|
||||||
}
|
}
|
||||||
|
} else if mode == 3 {
|
||||||
|
// Radial: range = the farthest boundary vertex from the centre.
|
||||||
|
let radius = model
|
||||||
|
.boundary
|
||||||
|
.iter()
|
||||||
|
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
||||||
|
.map(|&[x, y]| (x * x + y * y).sqrt())
|
||||||
|
.fold(0.0_f32, f32::max)
|
||||||
|
.max(1.0);
|
||||||
|
(0.0, radius)
|
||||||
} else {
|
} else {
|
||||||
(0.0, 1.0)
|
(0.0, 1.0)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ impl WipeoutGpu {
|
||||||
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
||||||
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Gradient { angle_deg, color2 } => {
|
HatchPattern::Gradient { angle_deg, color2, .. } => {
|
||||||
let r = angle_deg.to_radians();
|
let r = angle_deg.to_radians();
|
||||||
(2u32, *color2, r.cos(), r.sin())
|
(2u32, *color2, r.cos(), r.sin())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,11 @@ fn check_family(
|
||||||
let proj = v.xz.x * inst.grad_cos + v.xz.y * inst.grad_sin;
|
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);
|
let t = clamp((proj - inst.grad_min) / inst.grad_range, 0.0, 1.0);
|
||||||
return mix(inst.color, inst.color2, 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);
|
||||||
|
return mix(inst.color, inst.color2, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Pattern LOD: when the densest family's spacing projects below
|
// 3. Pattern LOD: when the densest family's spacing projects below
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,11 @@ fn check_family(
|
||||||
let proj = v.xz.x * h.grad_cos + v.xz.y * h.grad_sin;
|
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);
|
let t = clamp((proj - h.grad_min) / h.grad_range, 0.0, 1.0);
|
||||||
return mix(h.color, h.color2, t);
|
return mix(h.color, h.color2, t);
|
||||||
|
} else if h.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);
|
||||||
|
return mix(h.color, h.color2, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern LOD: when the densest family projects below 2 px, lines blur into
|
// Pattern LOD: when the densest family projects below 2 px, lines blur into
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue