fix(gradient): align properties and editing

This commit is contained in:
ramox81 committed 2026-08-20 15:33:09 +03:00
1 parent dcedf945ea
commit aee3397f5d
9 files changed
+309 -166

No files matched your search

+63 -11
View File
@@ -90,6 +90,36 @@ fn family_from_stored_line(
}
}
/// Preserve the selected hue while moving its HSL lightness towards the
/// persisted one-colour tint/shade target (0 = black, 1 = white).
fn gradient_tint_color(base: [f32; 4], target: f32) -> [f32; 4] {
let max = base[0].max(base[1]).max(base[2]);
let min = base[0].min(base[1]).min(base[2]);
let lightness = (max + min) * 0.5;
let target = target.clamp(0.0, 1.0);
let mut result = base;
if target <= lightness {
let factor = if lightness > 1.0e-6 {
target / lightness
} else {
0.0
};
for channel in &mut result[..3] {
*channel *= factor;
}
} else {
let factor = if lightness < 1.0 - 1.0e-6 {
(target - lightness) / (1.0 - lightness)
} else {
1.0
};
for channel in &mut result[..3] {
*channel += (1.0 - *channel) * factor;
}
}
result
}
impl Scene {
// ── Entity management ─────────────────────────────────────────────────
@@ -1041,8 +1071,13 @@ impl Scene {
m.angle_offset = dxf.pattern_angle as f32;
m.scale = dxf.pattern_scale as f32;
}
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
model::hatch_model::HatchPattern::Gradient {
angle_deg,
shift,
..
} => {
*angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
*shift = dxf.gradient_color.shift as f32;
}
model::hatch_model::HatchPattern::Pattern(_)
| model::hatch_model::HatchPattern::Solid => {}
@@ -1522,6 +1557,7 @@ impl Scene {
let mut boundary: Vec<[f64; 2]> = Vec::new();
let mut boundary_exterior = Vec::new();
let mut boundary_sources = Vec::new();
for path in &dxf.paths {
// Skip TEXTBOX boundary paths (flag bit 3). These are text
@@ -1531,6 +1567,19 @@ impl Scene {
if path.flags.bits() & 8 != 0 {
continue;
}
let first_fill_path = boundary_exterior.is_empty();
let keep_for_style = match dxf.style {
acadrust::entities::HatchStyleType::Normal => true,
acadrust::entities::HatchStyleType::Outer => {
first_fill_path || path.flags.is_external() || path.flags.is_outermost()
}
acadrust::entities::HatchStyleType::Ignore => {
first_fill_path || path.flags.is_external()
}
};
if !keep_for_style {
continue;
}
let before_path = boundary.len();
if !boundary.is_empty() {
boundary.push([f64::NAN, f64::NAN]);
@@ -1556,6 +1605,7 @@ impl Scene {
continue;
}
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
boundary_sources.push(path.boundary_handles.clone());
if boundary.len() >= path_start + 3 {
let first = boundary[path_start];
let last = *boundary.last().unwrap();
@@ -1618,8 +1668,13 @@ impl Scene {
)
};
gradient_color1 = stop(0);
let color2 = stop(1).unwrap_or(color);
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
let color1 = gradient_color1.unwrap_or(color);
let color2 = if dxf.gradient_color.is_single_color {
gradient_tint_color(color1, dxf.gradient_color.color_tint as f32)
} else {
stop(1).unwrap_or(color)
};
let angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
let (kind, invert) =
model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name);
model::hatch_model::HatchPattern::Gradient {
@@ -1627,6 +1682,7 @@ impl Scene {
color2,
kind,
invert,
shift: dxf.gradient_color.shift as f32,
}
} else if dxf.is_solid {
model::hatch_model::HatchPattern::Solid
@@ -1768,13 +1824,7 @@ impl Scene {
boundary: std::sync::Arc::new(boundary_f32),
boundary_wcs: None,
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
boundary_sources: Some(std::sync::Arc::new(
dxf.paths
.iter()
.filter(|path| path.flags.bits() & 8 == 0)
.map(|path| path.boundary_handles.clone())
.collect(),
)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
pattern,
name,
// A gradient starts from its first stop; other fills use the
@@ -2254,6 +2304,7 @@ impl Scene {
color2,
kind,
invert,
shift,
} = &model.pattern
{
let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb {
@@ -2268,6 +2319,7 @@ impl Scene {
// (radians); the gradient record keeps its own copy for the file.
dxf.pattern_angle = (*angle_deg as f64).to_radians();
dxf.gradient_color.angle = (*angle_deg as f64).to_radians();
dxf.gradient_color.shift = *shift as f64;
dxf.gradient_color.is_single_color = false;
// Linear has no INV name in the standard set: persist an inverted
// linear by swapping the colour stops instead.
+66
View File
@@ -58,6 +58,21 @@ impl GradientKind {
GradientKind::Curved,
];
/// User-facing gradient definitions in their standard persisted order.
/// Inverted definitions are real named patterns rather than a separate
/// property, so Properties and the draw command share this single list.
pub const CHOICES: [(GradientKind, bool); 9] = [
(GradientKind::Linear, false),
(GradientKind::Cylinder, false),
(GradientKind::Cylinder, true),
(GradientKind::Spherical, false),
(GradientKind::Hemispherical, false),
(GradientKind::Curved, false),
(GradientKind::Spherical, true),
(GradientKind::Hemispherical, true),
(GradientKind::Curved, true),
];
/// Radial fills shade from the boundary centre outward.
pub fn radial(self) -> bool {
matches!(self, GradientKind::Spherical | GradientKind::Hemispherical)
@@ -77,6 +92,27 @@ impl GradientKind {
Self::ALL.iter().copied().find(|k| k.label() == label)
}
pub fn choice_label(self, inverted: bool) -> &'static str {
match (self, inverted) {
(GradientKind::Linear, _) => "Linear",
(GradientKind::Cylinder, false) => "Cylindrical",
(GradientKind::Cylinder, true) => "Inverted cylindrical",
(GradientKind::Spherical, false) => "Spherical",
(GradientKind::Spherical, true) => "Inverted spherical",
(GradientKind::Hemispherical, false) => "Hemispherical",
(GradientKind::Hemispherical, true) => "Inverted hemispherical",
(GradientKind::Curved, false) => "Curved",
(GradientKind::Curved, true) => "Inverted curved",
}
}
pub fn from_choice_label(label: &str) -> Option<(Self, bool)> {
Self::CHOICES
.iter()
.copied()
.find(|(kind, inverted)| kind.choice_label(*inverted).eq_ignore_ascii_case(label))
}
/// Parse the DXF gradient name (`LINEAR`, `INVCYLINDER`, …) into the kind
/// plus its inverted flag. Unknown names read as Linear.
pub fn from_name(name: &str) -> (Self, bool) {
@@ -139,9 +175,39 @@ pub enum HatchPattern {
color2: [f32; 4],
kind: GradientKind,
invert: bool,
/// 0 = centred, 1 = shifted towards the upper-left light source.
shift: f32,
},
}
/// Local-space focal offset for the persisted 0..1 gradient shift.
pub fn gradient_shift_offset(boundary: &[[f32; 2]], shift: f32) -> [f32; 2] {
let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
for &[x, y] in boundary {
if x.is_finite() && y.is_finite() {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
if !min_x.is_finite() {
return [0.0, 0.0];
}
let shift = shift.clamp(0.0, 1.0);
[-(max_x - min_x) * 0.25 * shift, (max_y - min_y) * 0.25 * shift]
}
pub fn gradient_radius(boundary: &[[f32; 2]], center: [f32; 2]) -> f32 {
boundary
.iter()
.filter(|point| point[0].is_finite() && point[1].is_finite())
.map(|point| (point[0] - center[0]).hypot(point[1] - center[1]))
.fold(0.0_f32, f32::max)
.max(1.0)
}
/// GPU-side separator between disconnected boundary sub-loops. A finite
/// magnitude sentinel instead of NaN: shaders detect it with a plain
/// `abs(x) < 1e29` compare, which every driver evaluates identically,
+18 -19
View File
@@ -262,21 +262,32 @@ impl StorageHatchBatch {
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, kind, invert } => {
HatchPattern::Gradient {
angle_deg,
color2,
kind,
invert,
shift,
} => {
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
let center = crate::scene::model::hatch_model::gradient_shift_offset(
&h.boundary,
*shift,
);
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.
let radius = radial_radius(&h.boundary);
(3u32, *color2, 0.0, 0.0, 0.0, radius)
let radius = crate::scene::model::hatch_model::gradient_radius(
&h.boundary,
center,
);
(3u32, *color2, center[0], center[1], 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)
let shifted_min = gmin + center[0] * r.cos() + center[1] * r.sin();
(2u32, *color2, r.cos(), r.sin(), shifted_min, grange)
}
}
HatchPattern::Pattern(fams) => {
@@ -659,18 +670,6 @@ fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) {
(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
// it explicitly anywhere else — but rust needs the type referenced to
// confirm the layout assumption above.
+26 -17
View File
@@ -165,7 +165,7 @@ impl TextureHatch {
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, kind, invert } => {
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.
@@ -239,7 +239,16 @@ impl TextureHatch {
});
// ── Gradient projection range (snapped-local space) ───────────────
let (grad_min, grad_range) = if mode == 2 {
let base_mode = mode & 0xFF;
let shift = match &model.pattern {
HatchPattern::Gradient { shift, .. } => *shift,
_ => 0.0,
};
let center = crate::scene::model::hatch_model::gradient_shift_offset(
&model.boundary,
shift,
);
let (grad_min, grad_range, radial_center) = if base_mode == 2 {
let projs: Vec<f32> = model
.boundary
.iter()
@@ -247,25 +256,25 @@ impl TextureHatch {
.map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin)
.collect();
if projs.is_empty() {
(0.0, 1.0)
(0.0, 1.0, [0.0, 0.0])
} else {
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
// Floor matches the storage backend.
(proj_min, (proj_max - proj_min).max(1.0))
(
proj_min + center[0] * grad_cos + center[1] * grad_sin,
(proj_max - proj_min).max(1.0),
[0.0, 0.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 if base_mode == 3 {
let radius = crate::scene::model::hatch_model::gradient_radius(
&model.boundary,
center,
);
(0.0, radius, center)
} else {
(0.0, 1.0)
(0.0, 1.0, [0.0, 0.0])
};
// ── Pack the data texture: families | dashes ─────────────────────
@@ -355,8 +364,8 @@ impl TextureHatch {
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
// → round(perp/0)=NaN → an invisible hatch.
scale: model.scale.max(1e-6),
grad_cos,
grad_sin,
grad_cos: if base_mode == 3 { radial_center[0] } else { grad_cos },
grad_sin: if base_mode == 3 { radial_center[1] } else { grad_sin },
grad_min,
grad_range,
origin: [origin[0] as f32, origin[1] as f32],
+2
View File
@@ -1345,6 +1345,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
color2,
kind,
invert,
shift,
} => {
2_u8.hash(&mut h);
angle_deg.to_bits().hash(&mut h);
@@ -1353,6 +1354,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
}
kind.shader_kind().hash(&mut h);
invert.hash(&mut h);
shift.to_bits().hash(&mut h);
}
}
}