fix(gradient): align properties and editing
This commit is contained in:
parent
dcedf945ea
commit
aee3397f5d
9 changed files with 309 additions and 166 deletions
|
|
@ -668,8 +668,9 @@ impl OpenCADStudio {
|
|||
|
||||
"GRADIENT" => {
|
||||
use crate::modules::draw::draw::hatch::GradientCommand;
|
||||
let outlines = self.tabs[i].scene.closed_outlines();
|
||||
let new_cmd = GradientCommand::new(outlines);
|
||||
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
|
||||
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
|
||||
let new_cmd = GradientCommand::new(outlines, boundary_sources);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1222,7 +1222,8 @@ impl OpenCADStudio {
|
|||
acadrust::EntityType::Text(_)
|
||||
| acadrust::EntityType::MText(_)
|
||||
| acadrust::EntityType::Insert(_)
|
||||
| acadrust::EntityType::Leader(_) => Some(("annotative", None)),
|
||||
| acadrust::EntityType::Leader(_)
|
||||
| acadrust::EntityType::Hatch(_) => Some(("annotative", None)),
|
||||
acadrust::EntityType::MultiLeader(_) => {
|
||||
Some(("enable_annotation_scale", None))
|
||||
}
|
||||
|
|
@ -1265,7 +1266,8 @@ impl OpenCADStudio {
|
|||
},
|
||||
),
|
||||
acadrust::EntityType::Text(_)
|
||||
| acadrust::EntityType::Insert(_) => set_row_value(
|
||||
| acadrust::EntityType::Insert(_)
|
||||
| acadrust::EntityType::Hatch(_) => set_row_value(
|
||||
&mut sections,
|
||||
"annotative",
|
||||
crate::scene::model::object::PropValue::BoolToggle {
|
||||
|
|
|
|||
|
|
@ -345,59 +345,66 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||
if g.enabled {
|
||||
// ── Gradient fill ──────────────────────────────────────────────────
|
||||
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
|
||||
let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" };
|
||||
let mut sections = vec![
|
||||
let (kind, inverted) =
|
||||
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
|
||||
let mut pattern_props = vec![
|
||||
ro(t!("Type").as_ref(), "fill_kind", t!("Gradient").into_owned()),
|
||||
Property {
|
||||
label: t!("Color mode").into_owned(),
|
||||
field: "fill_type",
|
||||
value: PropValue::Choice {
|
||||
selected: grad_type.to_string(),
|
||||
options: vec!["One color".into(), "Two color".into()],
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Gradient type").into_owned(),
|
||||
field: "gradient_type",
|
||||
value: PropValue::Choice {
|
||||
selected: kind.choice_label(inverted).to_string(),
|
||||
options: crate::scene::model::hatch_model::GradientKind::CHOICES
|
||||
.iter()
|
||||
.map(|(kind, inverted)| kind.choice_label(*inverted).to_string())
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Color 1").into_owned(),
|
||||
field: "gradient_color_1",
|
||||
value: PropValue::ColorChoice(grad_c1),
|
||||
},
|
||||
];
|
||||
if g.is_single_color {
|
||||
pattern_props.push(edit(
|
||||
t!("Tint/Shade").as_ref(),
|
||||
"gradient_tint",
|
||||
g.color_tint.clamp(0.0, 1.0),
|
||||
));
|
||||
} else {
|
||||
pattern_props.push(Property {
|
||||
label: t!("Color 2").into_owned(),
|
||||
field: "gradient_color_2",
|
||||
value: PropValue::ColorChoice(grad_c2),
|
||||
});
|
||||
}
|
||||
pattern_props.push(edit_angle(
|
||||
t!("Angle").as_ref(),
|
||||
"pattern_angle",
|
||||
g.angle.to_degrees(),
|
||||
));
|
||||
pattern_props.push(Property {
|
||||
label: t!("Centered").into_owned(),
|
||||
field: "gradient_centered",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "gradient_centered",
|
||||
value: g.shift < 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
return vec![
|
||||
PropSection {
|
||||
title: t!("Pattern").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Type").into_owned(),
|
||||
field: "fill_type",
|
||||
value: PropValue::Choice {
|
||||
selected: grad_type.to_string(),
|
||||
options: vec!["Two color".into(), "One color".into()],
|
||||
},
|
||||
},
|
||||
{
|
||||
let (kind, invert) =
|
||||
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
|
||||
let _ = invert;
|
||||
Property {
|
||||
label: t!("Gradient type").into_owned(),
|
||||
field: "gradient_type",
|
||||
value: PropValue::Choice {
|
||||
selected: kind.label().to_string(),
|
||||
options: crate::scene::model::hatch_model::GradientKind::ALL
|
||||
.iter()
|
||||
.map(|k| k.label().to_string())
|
||||
.collect(),
|
||||
},
|
||||
}
|
||||
},
|
||||
Property {
|
||||
label: t!("Invert").into_owned(),
|
||||
field: "gradient_invert",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "gradient_invert",
|
||||
value: crate::scene::model::hatch_model::GradientKind::from_name(
|
||||
&g.name,
|
||||
)
|
||||
.1,
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Color 1").into_owned(),
|
||||
field: "gradient_color_1",
|
||||
value: PropValue::ColorChoice(grad_c1),
|
||||
},
|
||||
Property {
|
||||
label: t!("Color 2").into_owned(),
|
||||
field: "gradient_color_2",
|
||||
value: PropValue::ColorChoice(grad_c2),
|
||||
},
|
||||
edit_angle(t!("Angle").as_ref(), "pattern_angle", g.angle.to_degrees()),
|
||||
ro(t!("Centered").as_ref(), "gradient_centered", centered),
|
||||
],
|
||||
props: pattern_props,
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
|
|
@ -410,33 +417,26 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||
PropSection {
|
||||
title: t!("Misc").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Associative").as_ref(),
|
||||
"associative",
|
||||
if h.is_associative { "Yes" } else { "No" },
|
||||
),
|
||||
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
||||
ro(t!("Island detection style").as_ref(), "style", style),
|
||||
Property {
|
||||
label: t!("Background").into_owned(),
|
||||
field: "bg_enabled",
|
||||
label: t!("Associative").into_owned(),
|
||||
field: "associative",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "bg_enabled",
|
||||
value: bg_on,
|
||||
field: "associative",
|
||||
value: h.is_associative,
|
||||
},
|
||||
},
|
||||
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
||||
Property {
|
||||
label: t!("Island detection style").into_owned(),
|
||||
field: "style",
|
||||
value: PropValue::Choice {
|
||||
selected: style.to_string(),
|
||||
options: vec!["Normal".into(), "Outer".into(), "Ignore".into()],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
if bg_on {
|
||||
if let Some(sec) = sections.last_mut() {
|
||||
sec.props.push(Property {
|
||||
label: t!("Background color").into_owned(),
|
||||
field: "background_color",
|
||||
value: PropValue::ColorChoice(bg_col),
|
||||
});
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -567,11 +567,22 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||
return;
|
||||
}
|
||||
"associative" => {
|
||||
h.is_associative = if value == "toggle" {
|
||||
let requested = if value == "toggle" {
|
||||
!h.is_associative
|
||||
} else {
|
||||
value == "true"
|
||||
};
|
||||
// An associative flag without source handles is inert and cannot
|
||||
// update from boundary edits. Only enable it when a real
|
||||
// relationship is available; disabling retains the handles so the
|
||||
// user can turn it back on later.
|
||||
if !requested
|
||||
|| h.paths
|
||||
.iter()
|
||||
.any(|path| !path.boundary_handles.is_empty())
|
||||
{
|
||||
h.is_associative = requested;
|
||||
}
|
||||
return;
|
||||
}
|
||||
"pattern_type_label" => {
|
||||
|
|
@ -593,31 +604,27 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||
return;
|
||||
}
|
||||
"fill_type" => {
|
||||
h.gradient_color.is_single_color = value == "One color";
|
||||
let one_color = value == "One color";
|
||||
if one_color && !h.gradient_color.is_single_color {
|
||||
h.gradient_color.color_tint = 1.0;
|
||||
}
|
||||
h.gradient_color.is_single_color = one_color;
|
||||
return;
|
||||
}
|
||||
// Gradient shape selection + stop inversion (#415). Both re-derive the
|
||||
// standard DXF gradient name; Linear has no INV name, so inverting a
|
||||
// linear swaps the colour stops instead.
|
||||
"gradient_type" => {
|
||||
use crate::scene::model::hatch_model::GradientKind;
|
||||
let (_, invert) = GradientKind::from_name(&h.gradient_color.name);
|
||||
if let Some(kind) = GradientKind::from_label(value) {
|
||||
if let Some((kind, invert)) = GradientKind::from_choice_label(value) {
|
||||
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
||||
}
|
||||
return;
|
||||
}
|
||||
"gradient_invert" => {
|
||||
use crate::scene::model::hatch_model::GradientKind;
|
||||
let (kind, invert) = GradientKind::from_name(&h.gradient_color.name);
|
||||
let invert = !invert;
|
||||
if matches!(kind, GradientKind::Linear) {
|
||||
if h.gradient_color.colors.len() >= 2 {
|
||||
h.gradient_color.colors.swap(0, 1);
|
||||
}
|
||||
"gradient_centered" => {
|
||||
let centered = if value == "toggle" {
|
||||
h.gradient_color.shift >= 0.5
|
||||
} else {
|
||||
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
||||
}
|
||||
value == "true"
|
||||
};
|
||||
h.gradient_color.shift = if centered { 0.0 } else { 1.0 };
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -630,6 +637,9 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||
h.gradient_color.angle = v.to_radians();
|
||||
h.pattern_angle = v.to_radians();
|
||||
}
|
||||
"gradient_tint" if h.gradient_color.enabled => {
|
||||
h.gradient_color.color_tint = v.clamp(0.0, 1.0);
|
||||
}
|
||||
"pattern_angle" => {
|
||||
let angle = v.to_radians();
|
||||
let delta = angle - h.pattern_angle;
|
||||
|
|
|
|||
|
|
@ -643,6 +643,7 @@ impl CadCommand for HatchCommand {
|
|||
|
||||
pub struct GradientCommand {
|
||||
outlines: Vec<Vec<[f64; 2]>>,
|
||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||
mode: Mode,
|
||||
manual_pts: Vec<DVec3>,
|
||||
missed: bool,
|
||||
|
|
@ -653,9 +654,13 @@ pub struct GradientCommand {
|
|||
}
|
||||
|
||||
impl GradientCommand {
|
||||
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
||||
pub fn new(
|
||||
outlines: Vec<Vec<[f64; 2]>>,
|
||||
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
outlines,
|
||||
boundary_sources,
|
||||
mode: Mode::PickInside,
|
||||
manual_pts: vec![],
|
||||
missed: false,
|
||||
|
|
@ -666,6 +671,14 @@ impl GradientCommand {
|
|||
|
||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
||||
let (rel, origin, wcs) = pack_rings(&rings);
|
||||
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||
.into_iter()
|
||||
.map(|depth| depth % 2 == 0)
|
||||
.collect();
|
||||
let boundary_sources = rings
|
||||
.iter()
|
||||
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
||||
.collect();
|
||||
HatchModel {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(rel),
|
||||
|
|
@ -674,6 +687,7 @@ impl GradientCommand {
|
|||
color2: [0.18, 0.18, 0.18, 0.0],
|
||||
kind: self.kind,
|
||||
invert: self.invert,
|
||||
shift: 0.0,
|
||||
},
|
||||
name: self.kind.dxf_name(self.invert).into(),
|
||||
color: [0.30, 0.60, 0.95, 0.80],
|
||||
|
|
@ -683,8 +697,8 @@ impl GradientCommand {
|
|||
scale: 1.0,
|
||||
world_origin: origin,
|
||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -703,15 +717,10 @@ impl CadCommand for GradientCommand {
|
|||
} else {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
};
|
||||
let invert = if self.invert {
|
||||
t!(", inverted")
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
};
|
||||
t!(
|
||||
"GRADIENT (%{kind}%{invert}) Pick internal point:%{miss}",
|
||||
kind = t!(self.kind.label()),
|
||||
invert = invert,
|
||||
kind = t!(self.kind.choice_label(self.invert)),
|
||||
invert = std::borrow::Cow::Borrowed(""),
|
||||
miss = miss
|
||||
)
|
||||
.into_owned()
|
||||
|
|
@ -731,15 +740,14 @@ impl CadCommand for GradientCommand {
|
|||
match &self.mode {
|
||||
Mode::PickInside => {
|
||||
let mut opts = vec![CmdOption::new("Draw manually", "S")];
|
||||
for k in crate::scene::model::hatch_model::GradientKind::ALL {
|
||||
if k != self.kind {
|
||||
opts.push(CmdOption::new(k.label(), k.label()));
|
||||
for (kind, inverted) in
|
||||
crate::scene::model::hatch_model::GradientKind::CHOICES
|
||||
{
|
||||
if kind != self.kind || inverted != self.invert {
|
||||
let label = kind.choice_label(inverted);
|
||||
opts.push(CmdOption::new(label, label));
|
||||
}
|
||||
}
|
||||
opts.push(CmdOption::new(
|
||||
if self.invert { "Invert: on" } else { "Invert: off" },
|
||||
"I",
|
||||
));
|
||||
opts
|
||||
}
|
||||
Mode::Manual => {
|
||||
|
|
@ -804,17 +812,11 @@ impl CadCommand for GradientCommand {
|
|||
self.missed = false;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
// Gradient type keywords / buttons + the invert toggle (#415).
|
||||
if t.eq_ignore_ascii_case("i") || t.eq_ignore_ascii_case("invert") {
|
||||
self.invert = !self.invert;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if let Some(k) = crate::scene::model::hatch_model::GradientKind::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|k| k.label().eq_ignore_ascii_case(t))
|
||||
if let Some((kind, inverted)) =
|
||||
crate::scene::model::hatch_model::GradientKind::from_choice_label(t)
|
||||
{
|
||||
self.kind = k;
|
||||
self.kind = kind;
|
||||
self.invert = inverted;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
None
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue