From 57c01ed413637460ed6b9f2d98eaeffaddb4b60c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 3 Aug 2026 23:23:41 -0700 Subject: [PATCH] Wrap serialized gradient stops in a new GradientRamp struct and unify FillChoice (#4400) * Introduce the GradientRamp exchange struct as the serialized TaggedValue::Gradient payload * Unify FillChoice and FillChoiceUI into one enum generic over color format, carrying GradientRamp stops * Rename the TaggedValue::Gradient variant to GradientRamp to match its payload * Move the Color variant into the tagged_value macro list since its stored and wire forms match --- .../color_picker_message_handler.rs | 25 ++-- .../src/messages/frontend/frontend_message.rs | 4 +- .../messages/layout/layout_message_handler.rs | 8 +- .../utility_types/widgets/button_widgets.rs | 7 +- .../utility_types/widgets/input_widgets.rs | 4 +- .../data_panel/data_panel_message_handler.rs | 6 +- .../graph_operation_message.rs | 2 + .../document/graph_operation/utility_types.rs | 9 +- .../document/node_graph/node_properties.rs | 59 ++++---- .../document/node_graph/utility_types.rs | 2 +- .../document/overlays/grid_overlays.rs | 4 +- .../storage_tests/round_trip_tests.rs | 7 +- .../messages/portfolio/document_migration.rs | 4 +- .../common_functionality/color_selector.rs | 7 +- .../graph_modification_utils.rs | 16 +-- .../messages/tool/tool_messages/brush_tool.rs | 5 +- .../messages/tool/tool_messages/fill_tool.rs | 4 +- .../tool/tool_messages/gradient_tool.rs | 60 ++++---- .../messages/tool/tool_messages/text_tool.rs | 4 +- .../floating-menus/ColorPicker.svelte | 6 +- .../src/components/panels/Document.svelte | 4 +- .../src/components/widgets/WidgetSpan.svelte | 6 +- .../widgets/inputs/ColorInput.svelte | 12 +- .../widgets/inputs/WorkingColorsInput.svelte | 6 +- frontend/src/stores/color-picker.ts | 4 +- frontend/src/utility-functions/colors.ts | 22 +-- frontend/wrapper/src/editor_commands.rs | 16 ++- node-graph/graph-craft/src/document/value.rs | 125 +++++++++++++---- node-graph/libraries/graphic-types/src/lib.rs | 35 ++++- .../libraries/vector-types/src/gradient.rs | 131 ++++++++++++------ node-graph/libraries/vector-types/src/lib.rs | 2 +- .../vector-types/src/vector/style.rs | 93 +++++-------- 32 files changed, 411 insertions(+), 288 deletions(-) diff --git a/editor/src/messages/color_picker/color_picker_message_handler.rs b/editor/src/messages/color_picker/color_picker_message_handler.rs index 7d86c738cf..3ae10d14a0 100644 --- a/editor/src/messages/color_picker/color_picker_message_handler.rs +++ b/editor/src/messages/color_picker/color_picker_message_handler.rs @@ -5,7 +5,7 @@ use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::color::SRGBA8; use graphene_std::core_types::misc::parse_css_color; -use graphene_std::vector::style::{FillChoice, FillChoiceUI, Gradient, GradientStops}; +use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientStops}; /// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops). const MIN_MIDPOINT: f64 = 0.01; @@ -79,11 +79,12 @@ impl MessageHandler for ColorPickerMessageHandler { self.active_marker_is_midpoint = false; self.adopt_color(color); } - FillChoice::Gradient(stops) => { + FillChoice::Gradient(ramp) => { self.active_marker_index = Some(0); self.active_marker_is_midpoint = false; - let first_color = stops.color(0).unwrap_or(Color::BLACK); - self.gradient = Some(stops); + let gradient = Gradient::from(ramp); + let first_color = gradient.color(0).unwrap_or(Color::BLACK); + self.gradient = Some(gradient); self.adopt_color(first_color); } } @@ -154,7 +155,7 @@ impl MessageHandler for ColorPickerMessageHandler { match preset { FillChoice::None => { self.set_new_hsva(0., 0., 0., 1., true); - responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None }); + responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::::None }); } FillChoice::Solid(color) => { self.adopt_color(color); @@ -179,7 +180,7 @@ impl MessageHandler for ColorPickerMessageHandler { self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4); if self.is_none { - responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None }); + responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::::None }); } else { self.emit_color(responses); } @@ -269,15 +270,12 @@ impl ColorPickerMessageHandler { && (active_index as usize) < gradient.len() { gradient.set_color(active_index as usize, color); - let stops = gradient.clone(); - let fill_choice = FillChoice::Gradient(stops); responses.add(FrontendMessage::ColorPickerColorChanged { - value: FillChoiceUI::from(&fill_choice), + value: FillChoice::Gradient(GradientRamp::from(&*gradient)), }); } else { - let fill_choice = FillChoice::Solid(color); responses.add(FrontendMessage::ColorPickerColorChanged { - value: FillChoiceUI::from(&fill_choice), + value: FillChoice::Solid(SRGBA8::from(color)), }); } } @@ -399,11 +397,10 @@ impl ColorPickerMessageHandler { SpectrumInputUpdate::ActiveMarker { .. } => unreachable!("handled above"), } - self.gradient = Some(gradient.clone()); - let fill_choice = FillChoice::Gradient(gradient); responses.add(FrontendMessage::ColorPickerColorChanged { - value: FillChoiceUI::from(&fill_choice), + value: FillChoice::Gradient(GradientRamp::from(&gradient)), }); + self.gradient = Some(gradient); self.send_layouts(responses); } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e227c6821a..d7d537857a 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -14,7 +14,7 @@ use crate::messages::prelude::*; use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary; use graph_craft::document::NodeId; use graphene_std::color::SRGBA8; -use graphene_std::vector::style::FillChoiceUI; +use graphene_std::vector::style::FillChoice; use std::path::PathBuf; #[cfg(not(target_family = "wasm"))] @@ -165,7 +165,7 @@ pub enum FrontendMessage { }, /// The Rust color picker handler picked a new color/gradient. The frontend `` forwards this as its `colorOrGradient` event. ColorPickerColorChanged { - value: FillChoiceUI, + value: FillChoice, }, /// The Rust color picker handler is starting an undo transaction. The frontend `` forwards this as its `startHistoryTransaction` event. ColorPickerStartHistoryTransaction, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index ccfc0c062e..174ca67d63 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::prelude::*; use graphene_std::color::SRGBA8; -use graphene_std::vector::style::FillChoiceUI; +use graphene_std::vector::style::FillChoice; use serde_json::Value; use std::collections::HashMap; @@ -197,11 +197,11 @@ impl LayoutMessageHandler { let callback_message = match action { WidgetValueAction::Commit => (color_button.on_commit.callback)(&()), WidgetValueAction::Update => { - let Ok(fill_choice_ui) = serde_json::from_value::(value) else { - warn!("ColorInput update was not able to be parsed as FillChoiceUI: {color_button:?}"); + let Ok(fill_choice) = serde_json::from_value::>(value) else { + warn!("ColorInput update was not able to be parsed as FillChoice: {color_button:?}"); return; }; - color_button.value = fill_choice_ui; + color_button.value = fill_choice; (color_button.on_update.callback)(color_button) } }; diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 82ac8b8e3c..b32603a925 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -4,7 +4,8 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback; use derivative::*; -use graphene_std::vector::style::FillChoiceUI; +use graphene_std::color::SRGBA8; +use graphene_std::vector::style::FillChoice; use graphite_proc_macros::WidgetBuilder; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -191,9 +192,9 @@ pub struct ImageButton { pub struct ColorInput { // Content #[widget_builder(constructor)] - pub value: FillChoiceUI, + pub value: FillChoice, /// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time. - /// `None` when `value` is `FillChoiceUI::None`, in which case the frontend uses its "none" fallback styling. + /// `None` when `value` is `FillChoice::::None`, in which case the frontend uses its "none" fallback styling. #[serde(rename = "chosenGradient")] #[widget_builder(skip)] pub chosen_gradient: Option, diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 96abe66ff3..e4bf83e02f 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -6,7 +6,7 @@ use derivative::*; use graphene_std::Color; use graphene_std::color::SRGBA8; use graphene_std::transform::ReferencePoint; -use graphene_std::vector::style::{FillChoiceUI, GradientStops}; +use graphene_std::vector::style::{FillChoice, GradientStops}; use graphite_proc_macros::WidgetBuilder; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -520,7 +520,7 @@ pub struct ColorPresetsInput { #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ColorPresetsInputUpdate { - Preset(FillChoiceUI), + Preset(FillChoice), EyedropperColorCode(String), } diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index fb025102ed..d33e6f2754 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType}; use graphene_std::vector::misc::{ ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, }; -use graphene_std::vector::style::{DashPattern, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{DashPattern, FillChoice, GradientRamp, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector}; use graphene_std::{Artboard, Color, Graphic}; use std::any::Any; @@ -726,7 +726,7 @@ impl TableItemLayout for Color { } fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec { vec![ - ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(*self))) + ColorInput::new(FillChoice::::from(&FillChoice::Solid(*self))) .disabled(true) .menu_direction(Some(MenuDirection::Top)) .narrow(true) @@ -757,7 +757,7 @@ impl TableItemLayout for Gradient { .narrow(true) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(self.clone()))) + ColorInput::new(FillChoice::::Gradient(GradientRamp::from(self))) .menu_direction(Some(MenuDirection::Top)) .disabled(true) .narrow(true) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index ace7d9a928..8ef3f3827e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -22,6 +22,7 @@ pub enum GraphOperationMessage { }, FillGradientSet { layer: LayerNodeIdentifier, + #[serde(skip)] gradient: Gradient, gradient_type: GradientType, spread_method: GradientSpreadMethod, @@ -33,6 +34,7 @@ pub enum GraphOperationMessage { }, GradientStopsSet { layer: LayerNodeIdentifier, + #[serde(skip)] stops: Gradient, }, GradientPositionsSet { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4872f6a812..294a93c35e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -18,7 +18,7 @@ use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke}; -use graphene_std::vector::{Gradient, PointId, Vector, VectorModification, VectorModificationType}; +use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::{Artboard, Color, Graphic}; #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] @@ -410,12 +410,13 @@ impl<'a> ModifyInputsContext<'a> { }; let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput); - self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true); + let ramp = GradientRamp::from(gradient); + self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true); // Skip the rerender on all but the last input so the whole update triggers a single graph run self.set_input_with_refresh( InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput), - NodeInput::value(TaggedValue::Gradient(gradient), false), + NodeInput::value(TaggedValue::GradientRamp(ramp), false), true, ); @@ -549,7 +550,7 @@ impl<'a> ModifyInputsContext<'a> { }; let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(GradientRamp::from(stops)), false), false); } /// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 4b14fd9837..eea364b92d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -33,7 +33,9 @@ use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::{ ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, }; -use graphene_std::vector::style::{FillChoiceUI, Gradient, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation}; +use graphene_std::vector::style::{ + FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation, +}; use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification}; use graphene_std::{NodeParameter, ParameterRef}; @@ -1157,9 +1159,9 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: // Add the color input let widget_value = match &**tagged_value { - TaggedValue::Color(color) => FillChoiceUI::Solid(SRGBA8::from(*color)), - TaggedValue::Gradient(stops) => FillChoiceUI::Gradient(GradientStops::from(stops)), - value if value.is_no_paint() => FillChoiceUI::None, + TaggedValue::Color(color) => FillChoice::::Solid(SRGBA8::from(*color)), + TaggedValue::GradientRamp(ramp) => FillChoice::::Gradient(GradientRamp::from(ramp)), + value if value.is_no_paint() => FillChoice::::None, x => { warn!("Color {x:?}"); return LayoutGroup::row(widgets); @@ -1170,12 +1172,12 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: // while a plain color or gradient input always keeps its own value type let on_update: fn(&ColorInput) -> TaggedValue = if color_button.allow_none { |input| match &input.value { - FillChoiceUI::None => TaggedValue::no_paint(), - FillChoiceUI::Solid(srgba) => TaggedValue::Color(Color::from(*srgba)), - FillChoiceUI::Gradient(gradient_ui) => TaggedValue::Gradient(Gradient::from(gradient_ui)), + FillChoice::::None => TaggedValue::no_paint(), + FillChoice::::Solid(srgba) => TaggedValue::Color(Color::from(*srgba)), + FillChoice::::Gradient(ramp) => TaggedValue::GradientRamp(GradientRamp::from(ramp)), } - } else if matches!(&**tagged_value, TaggedValue::Gradient(_)) { - |input| TaggedValue::Gradient(input.value.as_gradient().map(Gradient::from).unwrap_or_else(Gradient::black_to_white)) + } else if matches!(&**tagged_value, TaggedValue::GradientRamp(_)) { + |input| TaggedValue::GradientRamp(input.value.as_gradient().map(GradientRamp::from).unwrap_or_else(GradientRamp::black_to_white)) } else { |input| TaggedValue::Color(input.value.as_solid().map(Color::from).unwrap_or(Color::TRANSPARENT)) }; @@ -2424,7 +2426,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte Ok(document_node) => match document_node.input_value(FillInput) { Some(TaggedValue::Color(color)) => ResolvedFill::Solid(Some(*color)), Some(value) if value.is_no_paint() => ResolvedFill::Solid(None), - Some(TaggedValue::Gradient(_)) => { + Some(TaggedValue::GradientRamp(_)) => { match graph_modification_utils::read_fill_node_gradient(document_node, || { layer.map_or([DVec2::ZERO, DVec2::ONE], |layer| context.network_interface.document_metadata().nonzero_bounding_box(layer)) }) { @@ -2450,12 +2452,12 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte _ => None, }; let backup_stops = match document_node.input_value(BackupGradientInput) { - Some(TaggedValue::Gradient(stops)) => stops.clone(), - _ => Gradient::black_to_white(), + Some(TaggedValue::GradientRamp(ramp)) => ramp.clone(), + _ => GradientRamp::black_to_white(), }; (backup_color, backup_stops) } - Err(_) => (None, Gradient::black_to_white()), + Err(_) => (None, GradientRamp::black_to_white()), }; match &fill { @@ -2465,7 +2467,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte let reverse_button = IconButton::new("Reverse", 24) .tooltip_label("Reverse Stops") .tooltip_description("Reverse the gradient color stops.") - .on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput)) + .on_update(update_value(move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed())), node_id, FillInput)) .widget_instance(); widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets_first_row.push(reverse_button); @@ -2473,16 +2475,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte _ => add_blank_assist(&mut widgets_first_row), } - let fill_choice_ui = match &fill { + let widget_value = match &fill { ResolvedFill::Solid(color) => { if let Some(color) = color { - FillChoiceUI::Solid(SRGBA8::from(*color)) + FillChoice::::Solid(SRGBA8::from(*color)) } else { - FillChoiceUI::None + FillChoice::::None } } - ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStops::from(stops)), - ResolvedFill::Other => FillChoiceUI::None, + ResolvedFill::Gradient { gradient: stops, .. } => FillChoice::::Gradient(GradientRamp::from(stops)), + ResolvedFill::Other => FillChoice::::None, }; let solid_set_messages = move |color: Option| { @@ -2507,18 +2509,18 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte Message::Batched { messages: messages.into() } }; - let gradient_set_messages = move |gradient: Gradient| Message::Batched { + let gradient_set_messages = move |ramp: GradientRamp| Message::Batched { messages: Box::new([ NodeGraphMessage::SetInputValue { node_id, input_index: FillInput::INDEX, - value: TaggedValue::Gradient(gradient.clone()).into(), + value: Box::new(TaggedValue::GradientRamp(ramp.clone())), } .into(), NodeGraphMessage::SetInputValue { node_id, input_index: BackupGradientInput::INDEX, - value: Box::new(TaggedValue::Gradient(gradient)), + value: Box::new(TaggedValue::GradientRamp(ramp)), } .into(), ]), @@ -2527,17 +2529,14 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets_first_row.push( ColorInput::default() - .value(fill_choice_ui) + .value(widget_value) .on_update(move |x: &ColorInput| match &x.value { - FillChoiceUI::None => solid_set_messages(None), - FillChoiceUI::Solid(srgba8) => { + FillChoice::::None => solid_set_messages(None), + FillChoice::::Solid(srgba8) => { let color = Some(Color::from(*srgba8)); solid_set_messages(color) } - FillChoiceUI::Gradient(gradient_stops_ui) => { - let gradient = Gradient::from(gradient_stops_ui); - gradient_set_messages(gradient) - } + FillChoice::::Gradient(ramp) => gradient_set_messages(GradientRamp::from(ramp)), }) .on_commit(commit_value) .widget_instance(), @@ -2556,7 +2555,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte .on_commit(commit_value), RadioEntryData::new("gradient") .label("Gradient") - .on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput)) + .on_update(update_value(move |_| TaggedValue::GradientRamp(backup_gradient.clone()), node_id, FillInput)) .on_commit(commit_value), ]; diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index cf377381fd..b52619305d 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -27,7 +27,7 @@ impl FrontendGraphDataType { match TaggedValue::from_type_or_none(input) { TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number, TaggedValue::Color(_) => Self::Color, - TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => Self::Gradient, + TaggedValue::LegacyGradient(_) | TaggedValue::GradientRamp(_) => Self::Gradient, TaggedValue::String(_) => Self::Typography, // Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name. TaggedValue::TypeDefault(td) => match td.name.as_ref() { diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index ede2a26091..cff2a33139 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -5,7 +5,7 @@ use crate::messages::prelude::*; use glam::DVec2; use graphene_std::color::SRGBA8; use graphene_std::renderer::Quad; -use graphene_std::vector::style::FillChoiceUI; +use graphene_std::vector::style::FillChoice; fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; @@ -274,7 +274,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { Separator::new(SeparatorStyle::Related).widget_instance(), ]); color_widgets.push( - ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK))) + ColorInput::new(FillChoice::::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK))) .tooltip_label("Grid Display Color") .allow_none(false) .on_update(update_val::(grid, |grid, color| { diff --git a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs index df01cca7a0..550cd5e416 100644 --- a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs +++ b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs @@ -801,12 +801,13 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() { "the transform input should hold a matrix, but became {transform:?}" ); - // The Sample Gradient parameter held the tuple-form stops, which parse as the stops value with even positions elided + // The Sample Gradient parameter held the tuple-form stops, which parse as the ramp value with even positions elided let sample_gradient_node = &network.nodes[&graph_craft::document::NodeId(2)]; let stops = sample_gradient_node.input_value(graphene_std::math_nodes::sample_gradient::GradientInput); - let Some(TaggedValue::Gradient(stops)) = stops else { - panic!("the legacy stops parameter should become a gradient stops value, but became {stops:?}"); + let Some(TaggedValue::GradientRamp(ramp)) = stops else { + panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}"); }; + let stops = graphene_std::vector::Gradient::from(ramp); assert_eq!(stops.len(), 2); assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize"); } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 27f4eb9219..0d817edf1e 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1673,7 +1673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], let fill_value = match old_fill { graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(), graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color), - graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()), + graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::GradientRamp(gradient.stops.clone()), }; document .network_interface @@ -1721,7 +1721,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() { document.network_interface.set_input( &InputConnector::node_at_index(*node_id, 3), - NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), + NodeInput::value(TaggedValue::GradientRamp(g.stops.clone()), false), network_path, ); diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 0bd902e005..2a9636300c 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -6,7 +6,8 @@ use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::utility_types::DocumentToolData; use graphene_std::Color; -use graphene_std::vector::style::{FillChoice, FillChoiceUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::color::SRGBA8; +use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; /// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar. pub struct ToolColorOptions { @@ -85,8 +86,8 @@ impl ToolColorOptions { // In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position. // `FillChoice::None` gives it a neutral starting point. let mixed_color = self.fill_choice.is_none(); - // Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoiceUI` (with `SRGBA8` colors) for the widget value. - let widget_value = FillChoiceUI::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None)); + // Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoice` (with `SRGBA8` colors) for the widget value. + let widget_value = FillChoice::::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None)); let mixed_enabled = self.enabled.is_none(); // In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash. // The frontend's click handler sends `true` when the user resolves the mixed state by clicking. diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 47947fd002..8da45a44ec 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -316,14 +316,14 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe .get(&fill_node_id) .and_then(|node| node.input(graphene_std::vector::fill::FillInput)) .and_then(|input| input.as_value()) - .and_then(|value| if let TaggedValue::Gradient(gradient) = value { Some(gradient.clone()) } else { None }); + .and_then(|value| if let TaggedValue::GradientRamp(ramp) = value { Some(Gradient::from(ramp)) } else { None }); } let gradient_value_node = network_interface.document_network().nodes.get(&get_upstream_gradient_value_node_id(layer, network_interface)?)?; - let TaggedValue::Gradient(stops) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else { + let TaggedValue::GradientRamp(ramp) = gradient_value_node.input(graphene_std::math_nodes::gradient_value::GradientInput)?.as_value()? else { return None; }; - let mut stops = stops.clone(); + let mut stops = Gradient::from(ramp); // The chain's stop placement comes from the closest-to-layer 'Gradient Positions'/'Gradient Midpoints' nodes, // matching the runtime where each later node overwrites the whole attribute @@ -662,10 +662,10 @@ pub struct FillNodeGradient { pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOnce() -> [DVec2; 2]) -> Option { use graphene_std::vector::fill; - let TaggedValue::Gradient(stops) = fill_node.input(fill::FillInput)?.as_value()? else { + let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else { return None; }; - let stops = stops.clone(); + let stops = Gradient::from(ramp); let gradient_type = match fill_node.input(fill::GradientTypeInput).and_then(|input| input.as_value()) { Some(&TaggedValue::GradientType(value)) => value, _ => GradientType::default(), @@ -731,7 +731,7 @@ pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option Some(FillChoice::Solid(*color)), - TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())), + TaggedValue::GradientRamp(ramp) => Some(FillChoice::Gradient(ramp.clone())), value if value.is_no_paint() => Some(FillChoice::None), _ => None, } @@ -815,7 +815,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document match &fill_choice { FillChoice::None => responses.add(GraphOperationMessage::FillColorSet { layer, color: None }), FillChoice::Solid(color) => responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) }), - FillChoice::Gradient(stops) => { + FillChoice::Gradient(ramp) => { use graphene_std::vector::fill; let fill_parameters = NodeGraphLayer::new(layer, &document.network_interface).find_node_parameters(fill::IDENTIFIER); @@ -836,7 +836,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document responses.add(GraphOperationMessage::FillGradientSet { layer, - gradient: stops.clone(), + gradient: Gradient::from(ramp), gradient_type, spread_method, transform, diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index e166f696a1..286a82cb07 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -9,8 +9,9 @@ use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Color; use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; +use graphene_std::color::SRGBA8; use graphene_std::raster::BlendMode; -use graphene_std::vector::style::{FillChoice, FillChoiceUI}; +use graphene_std::vector::style::FillChoice; const BRUSH_MAX_SIZE: f64 = 5000.; @@ -104,7 +105,7 @@ impl ToolMetadata for BrushTool { impl LayoutHolder for BrushTool { fn layout(&self) -> Layout { let mut widgets = vec![ - ColorInput::new(FillChoiceUI::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None))) + ColorInput::new(FillChoice::::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None))) .mixed(self.options.color.fill_choice.is_none()) .narrow(true) .on_update(|color: &ColorInput| { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 5506972d22..4d95e0f3ed 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::color_selector::solid; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use graphene_std::color::SRGBA8; use graphene_std::raster::color::Color; -use graphene_std::vector::style::FillChoiceUI; +use graphene_std::vector::style::FillChoice; #[derive(Default, ExtractField)] pub struct FillTool { @@ -44,7 +44,7 @@ impl ToolMetadata for FillTool { impl LayoutHolder for FillTool { fn layout(&self) -> Layout { let widgets = vec![ - ColorInput::new(FillChoiceUI::from(&solid(self.primary_color))) + ColorInput::new(FillChoice::::from(&solid(self.primary_color))) .narrow(true) .on_update(|color: &ColorInput| { FillToolMessage::SetColor { diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 9e3e3f2c72..9f610d438f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -16,7 +16,7 @@ use glam::DMat2; use graph_craft::document::value::TaggedValue; use graphene_std::color::SRGBA8; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, FillChoiceUI, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, build_transform_with_y_preservation}; +use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStop, GradientStops, GradientType, build_transform_with_y_preservation}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -257,33 +257,27 @@ impl LayoutHolder for GradientTool { .widget_instance(); // Display priority: the selected layer's stops, then any user-customized tool default, then the working colors - let stops_value = self - .data - .current_gradient_stops - .clone() - .or_else(|| self.data.default_gradient_stops.clone()) - .map(FillChoice::Gradient) - .unwrap_or_else(|| { - FillChoice::Gradient(Gradient::new([ - GradientStop { - position: 0., - midpoint: 0.5, - color: self.data.primary_color, - }, - GradientStop { - position: 1., - midpoint: 0.5, - color: self.data.secondary_color, - }, - ])) - }); - let stops_widget = ColorInput::new(FillChoiceUI::from(&stops_value)) + let stops_value = self.data.current_gradient_stops.clone().or_else(|| self.data.default_gradient_stops.clone()).unwrap_or_else(|| { + Gradient::new([ + GradientStop { + position: 0., + midpoint: 0.5, + color: self.data.primary_color, + }, + GradientStop { + position: 1., + midpoint: 0.5, + color: self.data.secondary_color, + }, + ]) + }); + let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp::from(&stops_value))) .allow_none(false) .narrow(true) .tooltip_label("Gradient Stops") .tooltip_description("Edit the gradient's color stops.") .on_update(|input: &ColorInput| { - let stops = input.value.as_gradient().cloned().unwrap_or_default(); + let stops = input.value.as_gradient().map(|ramp| ramp.stops.clone()).unwrap_or_default(); GradientToolMessage::UpdateStops { stops }.into() }) .on_commit(|_| DocumentMessage::AddTransaction.into()) @@ -2024,7 +2018,7 @@ mod test_gradient { use graph_craft::document::value::TaggedValue; use graphene_std::color::SRGBA8; use graphene_std::vector::style::{GradientSpreadMethod, build_transform_with_y_preservation}; - use graphene_std::vector::{Gradient, GradientStop, fill}; + use graphene_std::vector::{Gradient, GradientRamp, GradientStop, fill}; use super::gradient_space_transform; @@ -2067,7 +2061,7 @@ mod test_gradient { let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?; let stops = match fill_node.input(fill::FillInput)?.as_value()? { - TaggedValue::Gradient(stops) => stops.clone(), + TaggedValue::GradientRamp(ramp) => Gradient::from(ramp), _ => return None, }; @@ -2156,7 +2150,7 @@ mod test_gradient { .handle_message(NodeGraphMessage::SetInputValue { node_id: gradient_node_id, input_index: 1, - value: Box::new(TaggedValue::Gradient(Gradient::new([ + value: Box::new(TaggedValue::GradientRamp(GradientRamp::from(Gradient::new([ GradientStop { position: 0., midpoint: 0.5, @@ -2167,7 +2161,7 @@ mod test_gradient { midpoint: 0.5, color: Color::BLUE, }, - ]))), + ])))), }) .await; @@ -2190,10 +2184,10 @@ mod test_gradient { .and_then(|node| node.input(graphene_std::math_nodes::gradient_value::GradientInput)) .and_then(|input| input.as_value()) .cloned(); - let Some(TaggedValue::Gradient(stops)) = stops else { + let Some(TaggedValue::GradientRamp(ramp)) = stops else { panic!("expected a gradient default, got {stops:?}") }; - assert_eq!(stops.positions(), vec![0., 1.], "the parameter default should be the black-to-white starting gradient"); + assert_eq!(Gradient::from(ramp).positions(), vec![0., 1.], "the parameter default should be the black-to-white starting gradient"); } async fn create_fill_gradient_chain_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier { @@ -2215,7 +2209,7 @@ mod test_gradient { .handle_message(NodeGraphMessage::SetInputValue { node_id: gradient_node_id, input_index: 1, - value: Box::new(TaggedValue::Gradient(Gradient::new([ + value: Box::new(TaggedValue::GradientRamp(GradientRamp::from(Gradient::new([ GradientStop { position: 0., midpoint: 0.5, @@ -2226,7 +2220,7 @@ mod test_gradient { midpoint: 0.5, color: Color::BLUE, }, - ]))), + ])))), }) .await; @@ -2858,7 +2852,7 @@ mod test_gradient { .handle_message(NodeGraphMessage::SetInputValue { node_id: gradient_value_id, input_index: 1, - value: Box::new(TaggedValue::Gradient(Gradient::new([ + value: Box::new(TaggedValue::GradientRamp(GradientRamp::from(Gradient::new([ GradientStop { position: 0., midpoint: 0.5, @@ -2869,7 +2863,7 @@ mod test_gradient { midpoint: 0.5, color: Color::BLUE, }, - ]))), + ])))), }) .await; diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 52a7854752..db18e432ed 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -24,7 +24,7 @@ use graphene_std::choice_type::ChoiceTypeStatic; use graphene_std::color::SRGBA8; use graphene_std::renderer::Quad; use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping}; -use graphene_std::vector::style::{FillChoice, FillChoiceUI}; +use graphene_std::vector::style::FillChoice; use graphene_std::{Color, NodeParameter}; #[derive(Default, ExtractField)] @@ -261,7 +261,7 @@ impl TextTool { fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout { let mut widgets = vec![ - ColorInput::new(FillChoiceUI::from(self.options.fill.fill_choice.as_ref().unwrap_or(&FillChoice::None))) + ColorInput::new(FillChoice::::from(self.options.fill.fill_choice.as_ref().unwrap_or(&FillChoice::None))) .mixed(self.options.fill.fill_choice.is_none()) .narrow(true) .on_update(|color: &ColorInput| { diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index faabf12cd9..e8046b3ac3 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -5,14 +5,14 @@ import LayoutRow from "/src/components/layout/LayoutRow.svelte"; import WidgetLayout from "/src/components/widgets/WidgetLayout.svelte"; import type { ColorPickerCallbacks, ColorPickerStore } from "/src/stores/color-picker"; - import type { EditorWrapper, FillChoiceUI, MenuDirection } from "/wrapper/pkg/graphite_wasm_wrapper"; + import type { EditorWrapper, FillChoice, MenuDirection, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; - const dispatch = createEventDispatcher<{ colorOrGradient: FillChoiceUI; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>(); + const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>(); const editor = getContext("editor"); const colorPickerStore = getContext("colorPicker"); - export let colorOrGradient: FillChoiceUI; + export let colorOrGradient: FillChoice; export let allowNone = false; // export let allowTransparency = false; // TODO: Implement export let disabled = false; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5044490212..caeb146025 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -13,7 +13,7 @@ import type { DocumentStore } from "/src/stores/document"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import type { MessageBody } from "/src/subscriptions-router"; - import { fillChoiceUIColor, createSRgba8 } from "/src/utility-functions/colors"; + import { fillChoiceColor, createSRgba8 } from "/src/utility-functions/colors"; import { pasteFile } from "/src/utility-functions/files"; import { textInputCleanup } from "/src/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization"; @@ -680,7 +680,7 @@ }} colorOrGradient={{ Solid: gradientStopPickerColor || createSRgba8(0, 0, 0, 255) }} on:colorOrGradient={({ detail }) => { - const color = fillChoiceUIColor(detail); + const color = fillChoiceColor(detail); if (color) editor.updateGradientStopColor(color); }} on:startHistoryTransaction={() => editor.startGradientStopColorTransaction()} diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index aa37ad21f2..1c7023730d 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -26,7 +26,7 @@ import ShortcutLabel from "/src/components/widgets/labels/ShortcutLabel.svelte"; import TextLabel from "/src/components/widgets/labels/TextLabel.svelte"; import type { ColorPickerStore } from "/src/stores/color-picker"; - import { parseFillChoiceUI } from "/src/utility-functions/colors"; + import { parseFillChoice } from "/src/utility-functions/colors"; import type { EditorWrapper, LayoutTarget, Widget, WidgetInstance } from "/wrapper/pkg/graphite_wasm_wrapper"; // Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...) @@ -138,7 +138,7 @@ component: ColorInput, getProps: (props, index) => ({ ...props, - value: parseFillChoiceUI(props.value), + value: parseFillChoice(props.value), $$events: { value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), startHistoryTransaction: () => widgetValueCommit(index, props.value), @@ -150,7 +150,7 @@ getProps: (props, index) => ({ ...props, $$events: { - // The widget dispatches `"None"` or a bare `SRGBA8`, wrap the color in `{ Solid: ... }` so the payload matches Rust's `FillChoiceUI` shape (which the `Preset` variant expects). + // The widget dispatches `"None"` or a bare `SRGBA8`, wrap the color in `{ Solid: ... }` so the payload matches the Rust `FillChoice` shape the `Preset` variant expects preset: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { Preset: e.detail === "None" ? "None" : { Solid: e.detail } }, true), eyedropperColorCode: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { EyedropperColorCode: e.detail }, true), }, diff --git a/frontend/src/components/widgets/inputs/ColorInput.svelte b/frontend/src/components/widgets/inputs/ColorInput.svelte index d2c9306e76..d6ff57ad92 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend/src/components/widgets/inputs/ColorInput.svelte @@ -2,13 +2,13 @@ import { createEventDispatcher } from "svelte"; import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte"; import LayoutCol from "/src/components/layout/LayoutCol.svelte"; - import { contrastingOutlineFactor, fillChoiceUIColor, fillChoiceUIGradient } from "/src/utility-functions/colors"; - import type { FillChoiceUI, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper"; + import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradient } from "/src/utility-functions/colors"; + import type { FillChoice, MenuDirection, ActionShortcut, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; - const dispatch = createEventDispatcher<{ value: FillChoiceUI; startHistoryTransaction: undefined }>(); + const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>(); // Content - export let value: FillChoiceUI; + export let value: FillChoice; export let chosenGradient: string | undefined = undefined; export let allowNone = false; // export let allowTransparency = false; // TODO: Implement @@ -29,8 +29,8 @@ $: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01); $: outlined = outlineFactor > 0.0001; - $: gradient = fillChoiceUIGradient(value); - $: solidColor = fillChoiceUIColor(value); + $: gradient = fillChoiceGradient(value); + $: solidColor = fillChoiceColor(value); $: none = value === "None"; $: transparency = gradient ? gradient.color.some((color) => color.alpha < 255) : solidColor ? solidColor.alpha < 255 : false; diff --git a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte index da6ec2d09d..bfebf40f4a 100644 --- a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte +++ b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte @@ -3,7 +3,7 @@ import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte"; import LayoutCol from "/src/components/layout/LayoutCol.svelte"; import LayoutRow from "/src/components/layout/LayoutRow.svelte"; - import { fillChoiceUIColor, sRgba8ToRgbaCSS } from "/src/utility-functions/colors"; + import { fillChoiceColor, sRgba8ToRgbaCSS } from "/src/utility-functions/colors"; import type { SRGBA8, EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; const editor = getContext("editor"); @@ -42,7 +42,7 @@ on:open={({ detail }) => (primaryOpen = detail)} colorOrGradient={{ Solid: primary }} on:colorOrGradient={({ detail }) => { - const color = fillChoiceUIColor(detail); + const color = fillChoiceColor(detail); if (color) primaryColorChanged(color); }} direction="Right" @@ -55,7 +55,7 @@ on:open={({ detail }) => (secondaryOpen = detail)} colorOrGradient={{ Solid: secondary }} on:colorOrGradient={({ detail }) => { - const color = fillChoiceUIColor(detail); + const color = fillChoiceColor(detail); if (color) secondaryColorChanged(color); }} direction="Right" diff --git a/frontend/src/stores/color-picker.ts b/frontend/src/stores/color-picker.ts index 93dd2ce7f3..7e0ff582b9 100644 --- a/frontend/src/stores/color-picker.ts +++ b/frontend/src/stores/color-picker.ts @@ -2,10 +2,10 @@ import { writable } from "svelte/store"; import type { Writable } from "svelte/store"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { patchLayout } from "/src/utility-functions/widgets"; -import type { FillChoiceUI, Layout } from "/wrapper/pkg/graphite_wasm_wrapper"; +import type { FillChoice, Layout, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; export type ColorPickerCallbacks = { - onColorChanged?: (value: FillChoiceUI) => void; + onColorChanged?: (value: FillChoice) => void; onStartTransaction?: () => void; onCommitTransaction?: () => void; }; diff --git a/frontend/src/utility-functions/colors.ts b/frontend/src/utility-functions/colors.ts index 67b27602b9..196c01b177 100644 --- a/frontend/src/utility-functions/colors.ts +++ b/frontend/src/utility-functions/colors.ts @@ -1,4 +1,4 @@ -import type { FillChoiceUI, GradientStops, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; +import type { FillChoice, GradientRamp, GradientStops, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; // Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers export type HSV = { h: number; s: number; v: number }; @@ -150,7 +150,7 @@ export function sRgba8ContrastingColor(color: SRGBA8 | undefined): "black" | "wh return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white"; } -export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: string | [string, string], proximityRange: number): number { +export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number { const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor]; const [range1, range2] = pair.map((color) => sRgba8FromCSS(window.getComputedStyle(document.body).getPropertyValue(color))); @@ -167,7 +167,7 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st return (1 - Math.min(distance / proximityRange, 1)) * (1 - sRgba8ToHSV(color).s); }; - const gradient = fillChoiceUIGradient(value); + const gradient = fillChoiceGradient(value); if (gradient) { if (gradient.color.length === 0) return 0; @@ -177,7 +177,7 @@ export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: st return Math.min(first, last); } - return contrast(fillChoiceUIColor(value)); + return contrast(fillChoiceColor(value)); } // GRADIENT UTILITY FUNCTIONS @@ -186,21 +186,25 @@ export function isGradientStops(value: unknown): value is GradientStops return typeof value === "object" && value !== null && "color" in value && Array.isArray(value.color); } +export function isGradientRamp(value: unknown): value is GradientRamp { + return typeof value === "object" && value !== null && "stops" in value && isGradientStops(value.stops); +} + // FILL CHOICE UTILITY FUNCTIONS -export function fillChoiceUIColor(value: FillChoiceUI): SRGBA8 | undefined { +export function fillChoiceColor(value: FillChoice): SRGBA8 | undefined { if (typeof value === "object" && "Solid" in value) return value.Solid; return undefined; } -export function fillChoiceUIGradient(value: FillChoiceUI): GradientStops | undefined { - if (typeof value === "object" && "Gradient" in value) return value.Gradient; +export function fillChoiceGradient(value: FillChoice): GradientStops | undefined { + if (typeof value === "object" && "Gradient" in value) return value.Gradient.stops; return undefined; } -export function parseFillChoiceUI(value: unknown): FillChoiceUI { +export function parseFillChoice(value: unknown): FillChoice { if (value === "None" || value === undefined || value === null) return "None"; if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid }; - if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient }; + if (typeof value === "object" && value !== null && "Gradient" in value && isGradientRamp(value.Gradient)) return { Gradient: value.Gradient }; return "None"; } diff --git a/frontend/wrapper/src/editor_commands.rs b/frontend/wrapper/src/editor_commands.rs index 34b85a8b3a..b0610e127d 100644 --- a/frontend/wrapper/src/editor_commands.rs +++ b/frontend/wrapper/src/editor_commands.rs @@ -390,9 +390,9 @@ mod editor_commands { } /// Initialize the Rust color picker handler with a starting value (used when the frontend `` opens). - fn open_color_picker(initial_value: FillChoiceUI, allow_none: bool, disabled: bool) -> Message { + fn open_color_picker(initial_value: FillChoiceSRGBA8, allow_none: bool, disabled: bool) -> Message { ColorPickerMessage::Open { - initial_value: FillChoice::from(&initial_value), + initial_value: FillChoice::from(&initial_value.0), allow_none, disabled, } @@ -659,6 +659,17 @@ mod editor_commands { } } +#[cfg(feature = "editor")] +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(from_wasm_abi)] +pub struct FillChoiceSRGBA8( + /// Concrete wasm boundary form of the generic [`FillChoice`], since a `#[wasm_bindgen]` argument's TS declaration names its type without the generic's argument. + #[tsify(type = "FillChoice")] + pub graphene_std::vector::style::FillChoice, +); +#[cfg(not(feature = "editor"))] +pub type FillChoiceSRGBA8 = Any; + #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(from_wasm_abi)] pub struct Any(#[tsify(type = "any")] serde_json::Value); @@ -685,5 +696,4 @@ editor_proxy_types! { DockingSplitDirection = editor::messages::portfolio::utility_types::DockingSplitDirection; PanelTypes = Vec; SRGBA8 = graphene_std::color::SRGBA8; - FillChoiceUI = graphene_std::vector::style::FillChoiceUI; } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 3b08690fa9..e36647e78b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -20,7 +20,7 @@ use graphene_application_io::resource::ResourceId; use graphic_types::raster_types::{CPU, Image, Raster}; use graphic_types::vector_types::vector::misc::BoxCorners; use graphic_types::vector_types::vector::style::DashPattern; -use graphic_types::vector_types::vector::style::Gradient; +use graphic_types::vector_types::vector::style::{Gradient, GradientRamp}; use graphic_types::vector_types::vector::{self, ReferencePoint}; use graphic_types::{Artboard, Graphic, Vector}; use rendering::RenderMetadata; @@ -74,10 +74,10 @@ macro_rules! tagged_value { #[serde(deserialize_with = "core_types::misc::migrate_to_color")] // TODO: Eventually remove this document upgrade code #[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")] Color(Color), - /// Stored as the `{ color, position?, midpoint? }` stops struct, materializes as a single-row `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. - /// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `LegacyGradient` by `deserialize_tagged_value_with_legacy_migration`.) - #[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")] - Gradient(Gradient), + /// Stored as the `GradientRamp` exchange struct (nested `{ stops: { color, position?, midpoint? } }`), materializes as a single-row `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. + /// (Old documents stored flat stops, a tuple list, or the ancient full `Gradient` struct under the legacy `"Gradient"` tag, all routed by `deserialize_tagged_value_with_legacy_migration`.) + #[serde(alias = "Gradient", alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")] + GradientRamp(GradientRamp), /// Stored compactly as a `Vec`, materializes as `List` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. #[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this document upgrade code #[serde(alias = "BrushStrokeTable")] @@ -124,7 +124,7 @@ macro_rules! tagged_value { Self::DashPattern(lengths) => lengths.cache_hash(state), Self::BoxCorners(values) => values.cache_hash(state), Self::Color(color) => color.cache_hash(state), - Self::Gradient(stops) => stops.cache_hash(state), + Self::GradientRamp(ramp) => ramp.cache_hash(state), Self::BrushStrokes(strokes) => strokes.cache_hash(state), // ======================= // NON-SERIALIZED VARIANTS @@ -167,7 +167,7 @@ macro_rules! tagged_value { Self::DashPattern(lengths) => Box::new(DashPattern::from(lengths)), Self::BoxCorners(values) => Box::new(BoxCorners::from(values)), Self::Color(color) => Box::new(List::::new_from_element(color)), - Self::Gradient(stops) => Box::new(List::::new_from_element(stops)), + Self::GradientRamp(ramp) => Box::new(List::::new_from_element(Gradient::from(ramp))), Self::BrushStrokes(strokes) => { let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); Box::new(list) @@ -213,7 +213,7 @@ macro_rules! tagged_value { Self::DashPattern(lengths) => Arc::new(DashPattern::from(lengths)), Self::BoxCorners(values) => Arc::new(BoxCorners::from(values)), Self::Color(color) => Arc::new(List::::new_from_element(color)), - Self::Gradient(stops) => Arc::new(List::::new_from_element(stops)), + Self::GradientRamp(ramp) => Arc::new(List::::new_from_element(Gradient::from(ramp))), Self::BrushStrokes(strokes) => { let list: List = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); Arc::new(list) @@ -256,7 +256,7 @@ macro_rules! tagged_value { Self::DashPattern(_) => concrete!(DashPattern), Self::BoxCorners(_) => concrete!(BoxCorners), Self::Color(_) => concrete!(Color), - Self::Gradient(_) => concrete!(Gradient), + Self::GradientRamp(_) => concrete!(Gradient), Self::BrushStrokes(_) => concrete!(BrushStroke), // ======================= // AUTO-GENERATED VARIANTS @@ -307,7 +307,7 @@ macro_rules! tagged_value { Self::DashPattern(_) => scalar::(), Self::BoxCorners(_) => scalar::(), Self::Color(_) => leveled::(), - Self::Gradient(_) => leveled::(), + Self::GradientRamp(_) => leveled::(), Self::BrushStrokes(_) => leveled::(), $( Self::$identifier(_) => scalar::<$ty>(), )* Self::RenderOutput(_) => scalar::(), @@ -352,7 +352,7 @@ macro_rules! tagged_value { Self::DashPattern(lengths) => Ok(record_value_source(DashPattern::from(lengths))), Self::BoxCorners(values) => Ok(record_value_source(BoxCorners::from(values))), Self::Color(color) => Ok(leveled_record_value_source(vec![color])), - Self::Gradient(stops) => Ok(leveled_record_value_source(vec![stops])), + Self::GradientRamp(ramp) => Ok(leveled_record_value_source(vec![Gradient::from(ramp)])), Self::BrushStrokes(strokes) => Ok(leveled_record_value_source(strokes)), // ======================= // AUTO-GENERATED VARIANTS @@ -412,8 +412,7 @@ macro_rules! tagged_value { x if x == TypeId::of::>() => Ok(TaggedValue::F64Array(downcast::>(input).unwrap().iter_element_values().copied().collect())), x if x == TypeId::of::() => Ok(TaggedValue::DashPattern(downcast::(input).unwrap().0.iter_element_values().copied().collect())), x if x == TypeId::of::() => Ok(TaggedValue::BoxCorners(downcast::(input).unwrap().0.iter_element_values().copied().collect())), - x if x == TypeId::of::() => Ok(TaggedValue::Color(*downcast(input).unwrap())), - x if x == TypeId::of::() => Ok(TaggedValue::Gradient(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::GradientRamp(GradientRamp::from(*downcast::(input).unwrap()))), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())), // ======================= // AUTO-GENERATED VARIANTS @@ -442,8 +441,7 @@ macro_rules! tagged_value { x if x == TypeId::of::>() => Ok(TaggedValue::F64Array(input.downcast_ref::>().unwrap().iter_element_values().copied().collect())), x if x == TypeId::of::() => Ok(TaggedValue::DashPattern(input.downcast_ref::().unwrap().0.iter_element_values().copied().collect())), x if x == TypeId::of::() => Ok(TaggedValue::BoxCorners(input.downcast_ref::().unwrap().0.iter_element_values().copied().collect())), - x if x == TypeId::of::() => Ok(TaggedValue::Color(*input.downcast_ref::().unwrap())), - x if x == TypeId::of::() => Ok(TaggedValue::Gradient(input.downcast_ref::().unwrap().clone())), + x if x == TypeId::of::() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::().unwrap()))), x if x == TypeId::of::>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::>().unwrap().clone())), // ======================= // AUTO-GENERATED VARIANTS @@ -471,7 +469,7 @@ macro_rules! tagged_value { if name == core_types::normalize_type_name(std::any::type_name::<()>()) { return Some(TaggedValue::None) } // List-wrapped types need a single-item default with the element's default, not an empty list if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::Color(Color::default())) } - if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::Gradient(Gradient::default())) } + if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::GradientRamp(GradientRamp::default())) } $( if name == core_types::normalize_type_name(std::any::type_name::<$ty>()) { return Some(TaggedValue::$identifier(Default::default())) } )* if name == core_types::normalize_type_name(std::any::type_name::>()) { return Some(TaggedValue::F64Array(Vec::new())) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::DashPattern(Vec::new())) } @@ -480,7 +478,7 @@ macro_rules! tagged_value { // Leveled inputs type by their element; each element name maps to the // same tagged default as its legacy list form. if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::Color(Color::default())) } - if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::Gradient(Gradient::default())) } + if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::GradientRamp(GradientRamp::default())) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::BrushStrokes(Vec::new())) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List))) } if name == core_types::normalize_type_name(std::any::type_name::()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List))) } @@ -515,7 +513,7 @@ macro_rules! tagged_value { Self::DashPattern(lengths) => format!("DashPattern({lengths:?})"), Self::BoxCorners(values) => format!("BoxCorners({values:?})"), Self::Color(color) => format!("Color({color:?})"), - Self::Gradient(stops) => format!("Gradient({stops:?})"), + Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"), Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"), // ======================= // AUTO-GENERATED VARIANTS @@ -745,10 +743,10 @@ impl TaggedValue { () if ty == TypeId::of::>() => to_color(string).map(TaggedValue::Color)?, // The Fill and Stroke nodes' paint connectors default to `List`, their first registered implementation row () if ty == TypeId::of::>() => to_color(string).map(TaggedValue::Color)?, - () if ty == TypeId::of::>() => to_gradient(string).map(TaggedValue::Gradient)?, + () if ty == TypeId::of::>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?, // A paint default also parses against the bare element forms, as a color or gradient literal () if ty == TypeId::of::() => to_color(string).map(TaggedValue::Color)?, - () if ty == TypeId::of::() => to_gradient(string).map(TaggedValue::Gradient)?, + () if ty == TypeId::of::() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?, () if ty == TypeId::of::() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, () if ty == TypeId::of::() => TaggedValue::DashPattern(core_types::misc::parse_f64_list(string)), () if ty == TypeId::of::() => TaggedValue::BoxCorners(core_types::misc::parse_f64_list(string)), @@ -792,7 +790,7 @@ impl TaggedValue { /// - `Vector` (or alias `VectorData`): /// - non-empty → `TaggedValue::VectorModification()` (the document_migration's Path pass disambiguates this between SVG-import legacy and a discardable modern baked value via the input's `exposed` flag) /// - empty → `TaggedValue::TypeDefault(descriptor!(List))` -/// - `FillChoice` → `TaggedValue::Color` (solid), `TaggedValue::Gradient` (gradient), or `TaggedValue::no_paint()` (none) +/// - `FillChoice` → `TaggedValue::Color` (solid), `TaggedValue::GradientRamp` (gradient), or `TaggedValue::no_paint()` (none) /// /// All other tags (including ones with the modern shape) fall through to the standard derived `Deserialize` for `TaggedValue`. // TODO: Eventually remove this document upgrade code @@ -848,14 +846,14 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize return Ok(MemoHash::new(TaggedValue::Color(color))); } if let Some(gradient) = payload.get("Gradient") { - let gradient: Gradient = serde_json::from_value(gradient.clone()).map_err(serde::de::Error::custom)?; - return Ok(MemoHash::new(TaggedValue::Gradient(gradient))); + let ramp = graphic_types::migrations::migrate_to_gradient_ramp(gradient.clone()).map_err(serde::de::Error::custom)?; + return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp))); } } return Ok(MemoHash::new(TaggedValue::no_paint())); } // The gradient tags carried several shapes over time, disambiguated here: the ancient full struct (`start`/`end` keys) becomes `LegacyGradient`, - // while the current stops struct, the old tuple list, and the legacy one-element table wrapper all parse as the stops value directly + // while the current ramp, the flat stops struct, the old tuple list, and the legacy one-element table wrapper all parse as the ramp value directly "Gradient" | "GradientTable" | "GradientPositions" | "GradientStops" => { let table_element = content .as_object() @@ -866,7 +864,7 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize if let Some(array) = table_element && array.is_empty() { - return Ok(MemoHash::new(TaggedValue::Gradient(Gradient::default()))); + return Ok(MemoHash::new(TaggedValue::GradientRamp(GradientRamp::default()))); } let payload = table_element.and_then(|array| array.first()).unwrap_or(content); @@ -876,8 +874,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient))); } - let gradient: Gradient = serde_json::from_value(payload.clone()).map_err(serde::de::Error::custom)?; - return Ok(MemoHash::new(TaggedValue::Gradient(gradient))); + let ramp = graphic_types::migrations::migrate_to_gradient_ramp(payload.clone()).map_err(serde::de::Error::custom)?; + return Ok(MemoHash::new(TaggedValue::GradientRamp(ramp))); } _ => {} } @@ -1028,7 +1026,7 @@ mod leveled_edges { TaggedValue::F64Array(vec![1.]), TaggedValue::Bool(true), TaggedValue::TypeDefault(descriptor!(List)), - TaggedValue::Gradient(Default::default()), + TaggedValue::GradientRamp(Default::default()), ] { let layout = value.value_layout().unwrap(); let edge = value.to_edge().unwrap(); @@ -1085,3 +1083,74 @@ mod paint_default_parsing { } } } + +#[cfg(test)] +mod gradient_shape_migration { + use super::*; + + fn load(payload: serde_json::Value) -> TaggedValue { + deserialize_tagged_value_with_legacy_migration(payload) + .expect("The gradient payload should deserialize") + .into_inner() + .as_ref() + .clone() + } + + fn white() -> serde_json::Value { + serde_json::to_value(Color::WHITE).unwrap() + } + + #[test] + fn modern_ramp_payload_round_trips() { + let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); + gradient.set_positions(&[0.2, 0.9]); + let value = TaggedValue::GradientRamp(GradientRamp::from(gradient)); + + let json = serde_json::to_value(&value).unwrap(); + assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}"); + assert_eq!(load(json), value); + } + + // TODO: Eventually remove this document upgrade code + #[test] + fn legacy_flat_stops_parse_faithfully() { + let json = serde_json::json!({ "Gradient": { "color": [white(), white()], "position": [0., 0.25], "midpoint": [0.5, 0.5] } }); + let TaggedValue::GradientRamp(ramp) = load(json) else { + panic!("the flat stops should become a gradient ramp value") + }; + + let gradient = Gradient::from(ramp); + assert_eq!(gradient.positions(), vec![0., 0.25]); + assert!(gradient.has_midpoint_attribute(), "the flat form must parse faithfully"); + } + + // TODO: Eventually remove this document upgrade code + #[test] + fn legacy_tuple_stops_parse_with_defaults_elided() { + let json = serde_json::json!({ "Gradient": [[0., white()], [1., white()]] }); + let TaggedValue::GradientRamp(ramp) = load(json) else { + panic!("the tuple stops should become a gradient ramp value") + }; + + let gradient = Gradient::from(ramp); + assert_eq!(gradient.positions(), vec![0., 1.]); + assert!(!gradient.has_position_attribute(), "even legacy tuple positions should elide"); + } + + // TODO: Eventually remove this document upgrade code + #[test] + fn empty_legacy_gradient_table_degrades_to_the_default() { + let json = serde_json::json!({ "GradientTable": { "element": [] } }); + assert_eq!(load(json), TaggedValue::GradientRamp(GradientRamp::default())); + } + + // TODO: Eventually remove this document upgrade code + #[test] + fn ancient_full_struct_routes_to_legacy_gradient() { + let json = serde_json::json!({ "Gradient": { "stops": [[0., white()], [1., white()]], "gradient_type": "Linear", "start": [0., 0.], "end": [1., 0.] } }); + let TaggedValue::LegacyGradient(legacy) = load(json) else { + panic!("the ancient full struct should become a legacy gradient value") + }; + assert_eq!(Gradient::from(legacy.stops).positions(), vec![0., 1.], "the nested tuple stops should parse through the field adapter"); + } +} diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 0dda0cbe43..9cc9f97cf5 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -15,6 +15,9 @@ pub use markers::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE}; pub mod migrations { use crate::Vector; + use core_types::Color; + use vector_types::gradient::GradientStops; + use vector_types::{Gradient, GradientRamp}; // Storing legacy structs that are only used in document migration. // TODO: Eventually remove this document upgrade code @@ -23,11 +26,12 @@ pub mod migrations { use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke}; - use vector_types::{Gradient, Vector, vector}; + use vector_types::{GradientRamp, Vector, vector}; #[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct LegacyGradient { - pub stops: Gradient, + #[serde(deserialize_with = "crate::migrations::migrate_to_gradient_ramp")] + pub stops: GradientRamp, pub gradient_type: vector::style::GradientType, pub start: DVec2, pub end: DVec2, @@ -145,6 +149,33 @@ pub mod migrations { }) } + // TODO: Eventually remove this document upgrade code + /// Recovers a [`GradientRamp`] from any of its on-disk shapes: the current nested form, the flat stops struct + /// that preceded it, or the ancient position-color tuple list (whose even positions elide back to absence). + pub fn migrate_to_gradient_ramp<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { + use serde::Deserialize; + + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum GradientRampFormat { + Ramp(GradientRamp), + FlatStops(GradientStops), + Tuples(Vec<(f64, Color)>), + } + + Ok(match GradientRampFormat::deserialize(deserializer)? { + GradientRampFormat::Ramp(ramp) => ramp, + GradientRampFormat::FlatStops(stops) => GradientRamp::from(stops), + GradientRampFormat::Tuples(stops) => { + let position: Vec = stops.iter().map(|(position, _)| *position).collect(); + let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::>()); + gradient.set_positions(&position); + gradient.elide_default_attributes(); + GradientRamp::from(gradient) + } + }) + } + #[cfg(test)] mod migration_tests { use super::*; diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 1bf4c4d680..0de5f5c863 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -21,10 +21,10 @@ pub enum GradientType { #[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] pub struct Gradient(List); -/// A gradient's per-stop parallel arrays, generic over color format: `GradientStops` is the document serialization -/// of `TaggedValue::Gradient`, while `GradientStops` is the JS-boundary shape used by the color picker UI. +/// A gradient's per-stop parallel arrays, generic over color format: `GradientStops` nests inside the +/// [`GradientRamp`] exchange struct, while `GradientStops` is the JS-boundary shape used by the color picker UI. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GradientStops { pub color: Vec, @@ -96,34 +96,87 @@ impl GradientStops { } } -#[cfg(feature = "serde")] -impl serde::Serialize for Gradient { - fn serialize(&self, serializer: S) -> Result { - GradientStops::::from(self).serialize(serializer) +/// The serialized exchange form of a gradient: its stops, nested so that whole-ramp settings +/// like spread method can join as sibling fields opted in from their defaults. +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct GradientRamp { + pub stops: GradientStops, +} + +unsafe impl dyn_any::StaticType for GradientRamp { + type Static = GradientRamp; +} + +impl From> for GradientRamp { + fn from(stops: GradientStops) -> Self { + Self { stops } } } -// TODO: Eventually remove this document upgrade code -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for Gradient { - fn deserialize>(deserializer: D) -> Result { - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum GradientStopsFormat { - Struct(GradientStops), - Tuples(Vec<(f64, Color)>), - } +impl From<&Gradient> for GradientRamp { + fn from(gradient: &Gradient) -> Self { + Self { stops: gradient.into() } + } +} - Ok(match GradientStopsFormat::deserialize(deserializer)? { - GradientStopsFormat::Struct(stops) => Gradient::from(stops), - GradientStopsFormat::Tuples(stops) => { - let position: Vec = stops.iter().map(|(p, _)| *p).collect(); - let mut gradient = Gradient::from(stops.into_iter().map(|(_, c)| c).collect::>()); - gradient.set_positions(&position); - gradient.elide_default_attributes(); - gradient - } - }) +impl From for GradientRamp { + fn from(gradient: Gradient) -> Self { + Self::from(&gradient) + } +} + +impl From for Gradient { + fn from(ramp: GradientRamp) -> Self { + Gradient::from(ramp.stops) + } +} + +impl From<&GradientRamp> for Gradient { + fn from(ramp: &GradientRamp) -> Self { + Gradient::from(ramp.stops.clone()) + } +} + +impl From<&GradientRamp> for GradientStops { + fn from(ramp: &GradientRamp) -> Self { + Self { + position: ramp.stops.position.clone(), + midpoint: ramp.stops.midpoint.clone(), + color: ramp.stops.color.iter().map(|&color| SRGBA8::from(color)).collect(), + } + } +} + +// Color picker round-trip: routes through the runtime type so default-restating attributes elide +impl From<&GradientStops> for GradientRamp { + fn from(stops: &GradientStops) -> Self { + Self::from(Gradient::from(stops)) + } +} + +impl From<&GradientRamp> for GradientRamp { + fn from(ramp: &GradientRamp) -> Self { + Self { stops: ramp.into() } + } +} + +impl From<&Gradient> for GradientRamp { + fn from(gradient: &Gradient) -> Self { + Self { stops: gradient.into() } + } +} + +impl From<&GradientRamp> for GradientRamp { + fn from(ramp: &GradientRamp) -> Self { + Self::from(&ramp.stops) + } +} + +impl GradientRamp { + pub fn black_to_white() -> Self { + Self::from(Gradient::black_to_white()) } } @@ -793,31 +846,17 @@ mod tests { #[test] fn serde_round_trip_preserves_attribute_absence() { - let implicit = Gradient::from(vec![Color::BLACK, Color::WHITE]); + let implicit = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])); let json = serde_json::to_string(&implicit).unwrap(); assert!(!json.contains("position") && !json.contains("midpoint"), "absent attributes must not serialize: {json}"); - assert_eq!(serde_json::from_str::(&json).unwrap(), implicit); + assert_eq!(serde_json::from_str::(&json).unwrap(), implicit); - let mut explicit = implicit.clone(); + let mut explicit = Gradient::from(vec![Color::BLACK, Color::WHITE]); explicit.set_positions(&[0.2, 0.9]); explicit.set_midpoints(&[0.3, 0.5]); + let explicit = GradientRamp::from(explicit); let json = serde_json::to_string(&explicit).unwrap(); - assert_eq!(serde_json::from_str::(&json).unwrap(), explicit); - } - - #[test] - fn legacy_tuple_format_deserializes_with_defaults_elided() { - let color = serde_json::to_value(Color::WHITE).unwrap(); - - let struct_format = serde_json::json!({ "position": [0., 0.25], "midpoint": [0.5, 0.5], "color": [color, color] }); - let gradient: Gradient = serde_json::from_value(struct_format).unwrap(); - assert_eq!(gradient.positions(), vec![0., 0.25]); - assert!(gradient.has_midpoint_attribute(), "the struct form must parse faithfully"); - - let tuple_format = serde_json::json!([[0., color], [1., color]]); - let gradient: Gradient = serde_json::from_value(tuple_format).unwrap(); - assert_eq!(gradient.positions(), vec![0., 1.]); - assert!(!gradient.has_position_attribute(), "even legacy tuple positions should elide"); + assert_eq!(serde_json::from_str::(&json).unwrap(), explicit); } #[test] diff --git a/node-graph/libraries/vector-types/src/lib.rs b/node-graph/libraries/vector-types/src/lib.rs index 2ba9d3648b..9d821e0d39 100644 --- a/node-graph/libraries/vector-types/src/lib.rs +++ b/node-graph/libraries/vector-types/src/lib.rs @@ -9,7 +9,7 @@ pub mod vector; // Re-export commonly used types at the crate root pub use core_types as gcore; -pub use gradient::{Gradient, GradientSpreadMethod, GradientStop, GradientType}; +pub use gradient::{Gradient, GradientRamp, GradientSpreadMethod, GradientStop, GradientType}; pub use markers::{ATTR_EDITOR_CLICK_TARGET, ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD}; pub use math::{QuadExt, RectExt}; pub use subpath::Subpath; diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 09ba107a3c..4fa9a8844c 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -9,68 +9,65 @@ use dyn_any::DynAny; use glam::DAffine2; use std::f64::consts::{PI, TAU}; -/// The editor's in-memory paint picker state, storing color or gradient stops without gradient placement metadata. -/// Not stored in documents: paint inputs hold the picked value as a plain color, gradient, or no-paint type default. +/// The paint picker's choice of fill, generic over color format: `FillChoice` is the editor's in-memory +/// form, while `FillChoice` is the JS-boundary shape used by the color picker UI. Stores a color or +/// gradient ramp without gradient placement metadata, and is not stored in documents: paint inputs hold the +/// picked value as a plain color, gradient, or no-paint type default. /// -/// Can be None, a solid [Color], or a linear/radial [Gradient]. +/// Can be None, a solid color, or the [`GradientRamp`] of a linear/radial gradient. /// /// In the future we'll probably also add a pattern fill. -/// -/// Use [`FillChoiceUI`] at the JS boundary. #[repr(C)] -#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FillChoice { - #[default] - None, - Solid(Color), - Gradient(Gradient), -} - -// TODO: Deprecate [`FillChoice`] and keep this, renamed, as the main widget-controlling type -/// JS-boundary version of [`FillChoice`] where the solid color is [`SRGBA8`] and the gradient is its [`GradientStops`] exchange form. #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))] -#[derive(Default, Debug, Clone, PartialEq, DynAny)] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FillChoiceUI { +pub enum FillChoice { #[default] None, - Solid(SRGBA8), - Gradient(GradientStops), + Solid(C), + Gradient(GradientRamp), } -impl From<&FillChoice> for FillChoiceUI { +unsafe impl dyn_any::StaticType for FillChoice { + type Static = FillChoice; +} + +impl From<&FillChoice> for FillChoice { fn from(value: &FillChoice) -> Self { match value { FillChoice::None => Self::None, FillChoice::Solid(color) => Self::Solid(SRGBA8::from(*color)), - FillChoice::Gradient(stops) => Self::Gradient(stops.into()), + FillChoice::Gradient(ramp) => Self::Gradient(ramp.into()), } } } -impl From<&FillChoiceUI> for FillChoice { - fn from(value: &FillChoiceUI) -> Self { +impl From<&FillChoice> for FillChoice { + fn from(value: &FillChoice) -> Self { match value { - FillChoiceUI::None => Self::None, - FillChoiceUI::Solid(srgba) => Self::Solid(Color::from(*srgba)), - FillChoiceUI::Gradient(stops) => Self::Gradient(Gradient::from(stops)), + FillChoice::None => Self::None, + FillChoice::Solid(srgba) => Self::Solid(Color::from(*srgba)), + FillChoice::Gradient(ramp) => Self::Gradient(ramp.into()), } } } -impl FillChoiceUI { - pub fn as_solid(&self) -> Option { - let Self::Solid(c) = self else { return None }; - Some(*c) +impl FillChoice { + pub fn as_solid(&self) -> Option { + let Self::Solid(color) = self else { return None }; + Some(*color) } +} - pub fn as_gradient(&self) -> Option<&GradientStops> { - let Self::Gradient(g) = self else { return None }; - Some(g) +impl FillChoice { + pub fn as_gradient(&self) -> Option<&GradientRamp> { + let Self::Gradient(ramp) = self else { return None }; + Some(ramp) } +} - /// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoiceUI::None`]. +impl FillChoice { + /// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoice::None`]. /// Solid colors become a degenerate gradient between the same color so the CSS variable can always be assigned to a `background-image`. pub fn to_css_background_image(&self) -> Option { match self { @@ -79,31 +76,7 @@ impl FillChoiceUI { let hex = srgba.to_rgba_hex(); Some(format!("linear-gradient(#{hex}, #{hex})")) } - Self::Gradient(stops) => Some(stops.to_css_linear_gradient()), - } - } -} - -impl FillChoice { - pub fn as_solid(&self) -> Option { - let Self::Solid(color) = self else { return None }; - Some(*color) - } - - pub fn as_gradient(&self) -> Option<&Gradient> { - let Self::Gradient(gradient) = self else { return None }; - Some(gradient) - } - - /// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoice::None`]. Solid colors become a degenerate gradient between the same color so the CSS variable can always be assigned to a `background-image`. - pub fn to_css_background_image(&self) -> Option { - match self { - Self::None => None, - Self::Solid(color) => { - let hex = SRGBA8::from(*color).to_rgba_hex(); - Some(format!("linear-gradient(#{hex}, #{hex})")) - } - Self::Gradient(stops) => Some(stops.to_css_linear_gradient()), + Self::Gradient(ramp) => Some(ramp.stops.to_css_linear_gradient()), } } }