mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 02:18:13 +08:00
Add dashed links between spectrum markers and typed marker scales to the shared spectrum sections (#4534)
This commit is contained in:
@@ -705,12 +705,25 @@ pub struct SpectrumMarker {
|
||||
/// discarding any transparency so the handle always shows the RGB that steers the interpolation.
|
||||
#[serde(rename = "handleColorCSS")]
|
||||
handle_color_css: String,
|
||||
/// Whether a dashed line runs from this marker to the next through the lane below the track. Dragging it carries both markers.
|
||||
#[serde(rename = "dashedToNext")]
|
||||
dashed_to_next: bool,
|
||||
}
|
||||
|
||||
impl SpectrumMarker {
|
||||
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
||||
let handle_color_css = format!("#{}", SRGBA8::from(handle_color).to_rgb_hex());
|
||||
Self { position, midpoint, handle_color_css }
|
||||
Self {
|
||||
position,
|
||||
midpoint,
|
||||
handle_color_css,
|
||||
dashed_to_next: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dash_to_next(mut self) -> Self {
|
||||
self.dashed_to_next = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1399,36 +1399,101 @@ pub(crate) fn transfer_curves_properties(node_id: NodeId, context: &mut NodeProp
|
||||
pub(crate) fn levels_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::levels::*;
|
||||
|
||||
// (parameter, marker handle color, default percentage for double-click reset)
|
||||
let input_range_params = [
|
||||
(ShadowsInput.into(), Color::BLACK, 0.),
|
||||
(MidtonesInput.into(), Color::MIDDLE_GRAY, 50.),
|
||||
(HighlightsInput.into(), Color::WHITE, 100.),
|
||||
SpectrumSectionParam::new(ShadowsInput, Color::BLACK, 0., MarkerScale::Percent),
|
||||
SpectrumSectionParam::new(MidtonesInput, Color::MIDDLE_GRAY, 50., MarkerScale::Percent),
|
||||
SpectrumSectionParam::new(HighlightsInput, Color::WHITE, 100., MarkerScale::Percent),
|
||||
];
|
||||
let output_range_params = [
|
||||
SpectrumSectionParam::new(OutputMinimumsInput, Color::BLACK, 0., MarkerScale::Percent),
|
||||
SpectrumSectionParam::new(OutputMaximumsInput, Color::WHITE, 100., MarkerScale::Percent),
|
||||
];
|
||||
let output_range_params = [(OutputMinimumsInput.into(), Color::BLACK, 0.), (OutputMaximumsInput.into(), Color::WHITE, 100.)];
|
||||
|
||||
let mut layout = Vec::with_capacity(5);
|
||||
build_shared_spectrum_section(node_id, context, &input_range_params, &mut layout);
|
||||
build_shared_spectrum_section(node_id, context, &output_range_params, &mut layout);
|
||||
build_shared_spectrum_section(node_id, context, &bw_track(), &input_range_params, &mut layout);
|
||||
build_shared_spectrum_section(node_id, context, &bw_track(), &output_range_params, &mut layout);
|
||||
layout
|
||||
}
|
||||
|
||||
/// Append a section of related percentage parameters as rows: a shared black-to-white spectrum (with one marker per non-exposed parameter) sits on the first non-exposed row
|
||||
/// How a shared spectrum marker's value maps onto its track.
|
||||
#[derive(Clone, Copy)]
|
||||
enum MarkerScale {
|
||||
/// A 0..100 percentage, placed linearly.
|
||||
Percent,
|
||||
/// A gamma of 0.01..9.99 running from 9.99 at the left to 0.01 at the right, logarithmic on each side of the 1 at its center.
|
||||
Gamma,
|
||||
}
|
||||
|
||||
impl MarkerScale {
|
||||
fn position(self, value: f64) -> f64 {
|
||||
match self {
|
||||
Self::Percent => value / 100.,
|
||||
Self::Gamma if value >= 1. => 0.5 - 0.5 * value.log10() / 9.99_f64.log10(),
|
||||
Self::Gamma => 0.5 + 0.5 * value.log10() / 0.01_f64.log10(),
|
||||
}
|
||||
.clamp(0., 1.)
|
||||
}
|
||||
|
||||
fn value(self, position: f64) -> f64 {
|
||||
match self {
|
||||
Self::Percent => (position * 100.).clamp(0., 100.),
|
||||
Self::Gamma if position <= 0.5 => 9.99_f64.powf(1. - 2. * position).clamp(1., 9.99),
|
||||
Self::Gamma => 0.01_f64.powf(2. * position - 1.).clamp(0.01, 1.),
|
||||
}
|
||||
}
|
||||
|
||||
fn number_input(self) -> NumberInput {
|
||||
match self {
|
||||
Self::Percent => NumberInput::default().mode_increment().unit("%").min(0.).max(100.).display_decimal_places(0),
|
||||
Self::Gamma => NumberInput::default().mode_increment().min(0.01).max(9.99).display_decimal_places(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One parameter of a shared spectrum section and how its marker sits on the track.
|
||||
struct SpectrumSectionParam {
|
||||
parameter: ParameterRef,
|
||||
handle_color: Color,
|
||||
/// The value a double-click resets to.
|
||||
default_value: f64,
|
||||
scale: MarkerScale,
|
||||
/// Whether a dashed line joins the marker to the next parameter's marker.
|
||||
dash_to_next: bool,
|
||||
}
|
||||
|
||||
impl SpectrumSectionParam {
|
||||
fn new(parameter: impl Into<ParameterRef>, handle_color: Color, default_value: f64, scale: MarkerScale) -> Self {
|
||||
Self {
|
||||
parameter: parameter.into(),
|
||||
handle_color,
|
||||
default_value,
|
||||
scale,
|
||||
dash_to_next: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn dash_to_next(mut self) -> Self {
|
||||
self.dash_to_next = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a section of related parameters as rows: a shared spectrum over `track` (with one marker per non-exposed parameter) sits on the first non-exposed row
|
||||
/// alongside its 60px number input, and the remaining non-exposed rows show only their 60px number input. Exposed parameters render as the standard exposed-row display.
|
||||
/// Marker positions are clamped to non-decreasing display order so they never visually cross even if the underlying values do.
|
||||
fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesContext, params: &[(ParameterRef, Color, f64)], layout: &mut Vec<LayoutGroup>) {
|
||||
fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesContext, track: &Gradient, params: &[SpectrumSectionParam], layout: &mut Vec<LayoutGroup>) {
|
||||
// Snapshot exposure and values before the mutable-borrow loop
|
||||
let exposure_and_value: Vec<(bool, f64)> = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => params
|
||||
.iter()
|
||||
.map(|(parameter, _, _)| {
|
||||
let input = document_node.inputs.get(parameter.input_index);
|
||||
.map(|param| {
|
||||
let input = document_node.inputs.get(param.parameter.input_index);
|
||||
let exposed = input.is_some_and(|input| input.is_exposed());
|
||||
let percent = input
|
||||
let value = input
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|tagged| if let TaggedValue::F32(value) = tagged { Some(*value as f64) } else { None })
|
||||
.unwrap_or(0.);
|
||||
(exposed, percent)
|
||||
(exposed, value)
|
||||
})
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
@@ -1437,20 +1502,23 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
}
|
||||
};
|
||||
|
||||
// Build markers for all non-exposed params
|
||||
// Build markers for all non-exposed params, linking one to the next only when both have markers
|
||||
let mut marker_input_indices = Vec::new();
|
||||
let mut marker_default_percents = Vec::new();
|
||||
let mut marker_default_positions = Vec::new();
|
||||
let mut marker_scales = Vec::new();
|
||||
let mut marker_positions = Vec::new();
|
||||
let mut handle_colors = Vec::new();
|
||||
for (i, &(ref parameter, handle_color, default_percent)) in params.iter().enumerate() {
|
||||
let (exposed, percent) = exposure_and_value[i];
|
||||
let mut marker_colors_and_links = Vec::new();
|
||||
for (i, param) in params.iter().enumerate() {
|
||||
let (exposed, value) = exposure_and_value[i];
|
||||
if exposed {
|
||||
continue;
|
||||
}
|
||||
marker_positions.push((percent / 100.).clamp(0., 1.));
|
||||
marker_input_indices.push(parameter.input_index);
|
||||
marker_default_percents.push(default_percent);
|
||||
handle_colors.push(handle_color);
|
||||
let next_has_marker = exposure_and_value.get(i + 1).is_some_and(|&(next_exposed, _)| !next_exposed);
|
||||
marker_positions.push(param.scale.position(value));
|
||||
marker_input_indices.push(param.parameter.input_index);
|
||||
marker_default_positions.push(param.scale.position(param.default_value));
|
||||
marker_scales.push(param.scale);
|
||||
marker_colors_and_links.push((param.handle_color, param.dash_to_next && next_has_marker));
|
||||
}
|
||||
|
||||
// Enforce non-decreasing order so markers never visually cross, matching the node's algorithm where shadows takes precedence
|
||||
@@ -1460,13 +1528,16 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
|
||||
let spectrum_markers: Vec<SpectrumMarker> = marker_positions
|
||||
.iter()
|
||||
.zip(&handle_colors)
|
||||
.map(|(&position, &handle_color)| SpectrumMarker::new(position, 0.5, handle_color))
|
||||
.zip(&marker_colors_and_links)
|
||||
.map(|(&position, &(handle_color, dashed))| {
|
||||
let marker = SpectrumMarker::new(position, 0.5, handle_color);
|
||||
if dashed { marker.dash_to_next() } else { marker }
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build the shared spectrum widget (placed on the first non-exposed row)
|
||||
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
||||
SpectrumInput::new(GradientStops::from(&bw_track()))
|
||||
SpectrumInput::new(GradientStops::from(track))
|
||||
.track_space(GradientSpace::RgbGamma)
|
||||
.markers(spectrum_markers)
|
||||
.show_midpoints(false)
|
||||
@@ -1476,31 +1547,31 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
.narrow(true)
|
||||
.on_update({
|
||||
let marker_input_indices = marker_input_indices.clone();
|
||||
let marker_default_percents = marker_default_percents.clone();
|
||||
let marker_default_positions = marker_default_positions.clone();
|
||||
let marker_scales = marker_scales.clone();
|
||||
let marker_positions = marker_positions.clone();
|
||||
move |update: &SpectrumInputUpdate| {
|
||||
let (input_index, percent) = match update {
|
||||
SpectrumInputUpdate::MoveMarker { index, position } => match marker_input_indices.get(*index as usize) {
|
||||
Some(&input_index) => (input_index, *position * 100.),
|
||||
None => return Message::NoOp,
|
||||
},
|
||||
SpectrumInputUpdate::ResetMarker { index } => {
|
||||
let i = *index as usize;
|
||||
let Some(&input_index) = marker_input_indices.get(i) else { return Message::NoOp };
|
||||
let Some(&default_percent) = marker_default_percents.get(i) else { return Message::NoOp };
|
||||
// Falls back to midpoint between neighbors if the default would cross one
|
||||
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
|
||||
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
|
||||
let default_position = default_percent / 100.;
|
||||
let new_position = if (left..=right).contains(&default_position) { default_position } else { (left + right) / 2. };
|
||||
(input_index, new_position * 100.)
|
||||
}
|
||||
let i = match update {
|
||||
SpectrumInputUpdate::MoveMarker { index, .. } | SpectrumInputUpdate::ResetMarker { index } => *index as usize,
|
||||
_ => return Message::NoOp,
|
||||
};
|
||||
let (Some(&input_index), Some(&scale), Some(&default_position)) = (marker_input_indices.get(i), marker_scales.get(i), marker_default_positions.get(i)) else {
|
||||
return Message::NoOp;
|
||||
};
|
||||
let left = if i == 0 { 0. } else { marker_positions[i - 1] };
|
||||
let right = marker_positions.get(i + 1).copied().unwrap_or(1.);
|
||||
|
||||
let scale_position = match update {
|
||||
SpectrumInputUpdate::MoveMarker { position, .. } => *position,
|
||||
// A default that would cross a neighbor falls back to the midpoint between them
|
||||
SpectrumInputUpdate::ResetMarker { .. } if (left..=right).contains(&default_position) => default_position,
|
||||
SpectrumInputUpdate::ResetMarker { .. } => (left + right) / 2.,
|
||||
_ => return Message::NoOp,
|
||||
};
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index,
|
||||
value: TaggedValue::F32(percent.clamp(0., 100.) as f32).into(),
|
||||
value: TaggedValue::F32(scale.value(scale_position) as f32).into(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1510,12 +1581,11 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
});
|
||||
let spectrum_owner = marker_input_indices.first().copied();
|
||||
|
||||
let number_input = NumberInput::default().mode_increment().unit("%").min(0.).max(100.);
|
||||
|
||||
// One row per parameter: first non-exposed carries the shared spectrum, others get just a number input
|
||||
for (i, (parameter, _, _)) in params.iter().enumerate() {
|
||||
for (i, param) in params.iter().enumerate() {
|
||||
let (exposed, current) = exposure_and_value[i];
|
||||
let input_index = parameter.input_index;
|
||||
let input_index = param.parameter.input_index;
|
||||
let number_input = param.scale.number_input();
|
||||
|
||||
if exposed {
|
||||
let row = number_widget(ParameterWidgetsInfo::at_index(node_id, input_index, true, context), number_input.clone());
|
||||
@@ -1537,7 +1607,6 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
||||
.value(Some(current))
|
||||
.min_width(60)
|
||||
.max_width(60)
|
||||
.display_decimal_places(0)
|
||||
.on_update(update_value_at_index(
|
||||
move |widget: &NumberInput| TaggedValue::F32(widget.value.unwrap_or(0.) as f32),
|
||||
node_id,
|
||||
@@ -1772,10 +1841,13 @@ fn spectrum_slider_row(
|
||||
pub(crate) fn threshold_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::threshold::*;
|
||||
|
||||
let params: &[(ParameterRef, Color, f64)] = &[(MinLuminanceInput.into(), Color::BLACK, 50.), (MaxLuminanceInput.into(), Color::WHITE, 100.)];
|
||||
let params = [
|
||||
SpectrumSectionParam::new(MinLuminanceInput, Color::WHITE, 50., MarkerScale::Percent).dash_to_next(),
|
||||
SpectrumSectionParam::new(MaxLuminanceInput, Color::WHITE, 100., MarkerScale::Percent),
|
||||
];
|
||||
|
||||
let mut layout = Vec::with_capacity(2);
|
||||
build_shared_spectrum_section(node_id, context, params, &mut layout);
|
||||
build_shared_spectrum_section(node_id, context, &bw_track(), ¶ms, &mut layout);
|
||||
|
||||
layout
|
||||
}
|
||||
@@ -2143,11 +2215,31 @@ pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodeProp
|
||||
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::exposure::*;
|
||||
|
||||
let exposure = number_widget(ParameterWidgetsInfo::new(node_id, ExposureInput, true, context), NumberInput::default().min(-20.).max(20.));
|
||||
let offset = number_widget(ParameterWidgetsInfo::new(node_id, OffsetInput, true, context), NumberInput::default().min(-0.5).max(0.5));
|
||||
let gamma_correction = number_widget(
|
||||
let exposure = range_slider_widget(
|
||||
ParameterWidgetsInfo::new(node_id, ExposureInput, true, context),
|
||||
NumberInput::default().min(-20.).max(20.),
|
||||
SliderRange {
|
||||
min: -20.,
|
||||
max: 20.,
|
||||
default: Some(0.),
|
||||
},
|
||||
);
|
||||
let offset = range_slider_widget(
|
||||
ParameterWidgetsInfo::new(node_id, OffsetInput, true, context),
|
||||
NumberInput::default().min(-0.5).max(0.5),
|
||||
SliderRange {
|
||||
min: -0.5,
|
||||
max: 0.5,
|
||||
default: Some(0.),
|
||||
},
|
||||
);
|
||||
|
||||
let gamma_correction = slider_row(
|
||||
ParameterWidgetsInfo::new(node_id, GammaCorrectionInput, true, context),
|
||||
NumberInput::default().min(0.01).max(9.99).increment_step(0.1),
|
||||
MarkerScale::Gamma.number_input().increment_step(0.1),
|
||||
Some(1.),
|
||||
|gamma| MarkerScale::Gamma.position(gamma),
|
||||
|position| MarkerScale::Gamma.value(position),
|
||||
);
|
||||
|
||||
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
|
||||
|
||||
Reference in New Issue
Block a user