mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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
This commit is contained in:
committed by
Dennis Kobert
parent
4d8729d87c
commit
360321e0ce
@@ -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<Gradient>` 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<Gradient>` 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<BrushStroke>`, materializes as `List<BrushStroke>` 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::<Color>::new_from_element(color)),
|
||||
Self::Gradient(stops) => Box::new(List::<Gradient>::new_from_element(stops)),
|
||||
Self::GradientRamp(ramp) => Box::new(List::<Gradient>::new_from_element(Gradient::from(ramp))),
|
||||
Self::BrushStrokes(strokes) => {
|
||||
let list: List<BrushStroke> = 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::<Color>::new_from_element(color)),
|
||||
Self::Gradient(stops) => Arc::new(List::<Gradient>::new_from_element(stops)),
|
||||
Self::GradientRamp(ramp) => Arc::new(List::<Gradient>::new_from_element(Gradient::from(ramp))),
|
||||
Self::BrushStrokes(strokes) => {
|
||||
let list: List<BrushStroke> = 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::<DashPattern>(),
|
||||
Self::BoxCorners(_) => scalar::<BoxCorners>(),
|
||||
Self::Color(_) => leveled::<Color>(),
|
||||
Self::Gradient(_) => leveled::<Gradient>(),
|
||||
Self::GradientRamp(_) => leveled::<Gradient>(),
|
||||
Self::BrushStrokes(_) => leveled::<BrushStroke>(),
|
||||
$( Self::$identifier(_) => scalar::<$ty>(), )*
|
||||
Self::RenderOutput(_) => scalar::<RenderOutput>(),
|
||||
@@ -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::<List<f64>>() => Ok(TaggedValue::F64Array(downcast::<List<f64>>(input).unwrap().iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<DashPattern>() => Ok(TaggedValue::DashPattern(downcast::<DashPattern>(input).unwrap().0.iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<BoxCorners>() => Ok(TaggedValue::BoxCorners(downcast::<BoxCorners>(input).unwrap().0.iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<Color>() => Ok(TaggedValue::Color(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::Gradient(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(*downcast::<Gradient>(input).unwrap()))),
|
||||
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
@@ -442,8 +441,7 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<List<f64>>() => Ok(TaggedValue::F64Array(input.downcast_ref::<List<f64>>().unwrap().iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<DashPattern>() => Ok(TaggedValue::DashPattern(input.downcast_ref::<DashPattern>().unwrap().0.iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<BoxCorners>() => Ok(TaggedValue::BoxCorners(input.downcast_ref::<BoxCorners>().unwrap().0.iter_element_values().copied().collect())),
|
||||
x if x == TypeId::of::<Color>() => Ok(TaggedValue::Color(*input.downcast_ref::<Color>().unwrap())),
|
||||
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::Gradient(input.downcast_ref::<Gradient>().unwrap().clone())),
|
||||
x if x == TypeId::of::<Gradient>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::<Gradient>().unwrap()))),
|
||||
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Vec<BrushStroke>>().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::<List<Color>>()) { return Some(TaggedValue::Color(Color::default())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<List<Gradient>>()) { return Some(TaggedValue::Gradient(Gradient::default())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<List<Gradient>>()) { 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::<List<f64>>()) { return Some(TaggedValue::F64Array(Vec::new())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<DashPattern>()) { 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::<Color>()) { return Some(TaggedValue::Color(Color::default())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<Gradient>()) { return Some(TaggedValue::Gradient(Gradient::default())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<Gradient>()) { return Some(TaggedValue::GradientRamp(GradientRamp::default())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<BrushStroke>()) { return Some(TaggedValue::BrushStrokes(Vec::new())) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<Graphic>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Graphic>))) }
|
||||
if name == core_types::normalize_type_name(std::any::type_name::<Artboard>()) { return Some(TaggedValue::TypeDefault(core_types::descriptor!(List<Artboard>))) }
|
||||
@@ -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::<List<Color>>() => to_color(string).map(TaggedValue::Color)?,
|
||||
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
|
||||
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(TaggedValue::Color)?,
|
||||
() if ty == TypeId::of::<List<Gradient>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||
() if ty == TypeId::of::<List<Gradient>>() => 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::<Graphic>() => to_color(string).map(TaggedValue::Color)?,
|
||||
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||
() if ty == TypeId::of::<Gradient>() => to_gradient(string).map(|gradient| TaggedValue::GradientRamp(gradient.into()))?,
|
||||
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(core_types::misc::parse_f64_list(string)),
|
||||
() if ty == TypeId::of::<BoxCorners>() => TaggedValue::BoxCorners(core_types::misc::parse_f64_list(string)),
|
||||
@@ -792,7 +790,7 @@ impl TaggedValue {
|
||||
/// - `Vector` (or alias `VectorData`):
|
||||
/// - non-empty → `TaggedValue::VectorModification(<built from first element>)` (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<Vector>))`
|
||||
/// - `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<Vector>)),
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GradientRamp, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum GradientRampFormat {
|
||||
Ramp(GradientRamp),
|
||||
FlatStops(GradientStops<Color>),
|
||||
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<f64> = stops.iter().map(|(position, _)| *position).collect();
|
||||
let mut gradient = Gradient::from(stops.into_iter().map(|(_, color)| color).collect::<Vec<_>>());
|
||||
gradient.set_positions(&position);
|
||||
gradient.elide_default_attributes();
|
||||
GradientRamp::from(gradient)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod migration_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -21,10 +21,10 @@ pub enum GradientType {
|
||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||
pub struct Gradient(List<Color>);
|
||||
|
||||
/// A gradient's per-stop parallel arrays, generic over color format: `GradientStops<Color>` is the document serialization
|
||||
/// of `TaggedValue::Gradient`, while `GradientStops<SRGBA8>` is the JS-boundary shape used by the color picker UI.
|
||||
/// A gradient's per-stop parallel arrays, generic over color format: `GradientStops<Color>` nests inside the
|
||||
/// [`GradientRamp`] exchange struct, while `GradientStops<SRGBA8>` 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<C> {
|
||||
pub color: Vec<C>,
|
||||
@@ -96,34 +96,87 @@ impl GradientStops<SRGBA8> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Gradient {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
GradientStops::<Color>::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<C = Color> {
|
||||
pub stops: GradientStops<C>,
|
||||
}
|
||||
|
||||
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> {
|
||||
type Static = GradientRamp<C::Static>;
|
||||
}
|
||||
|
||||
impl<C> From<GradientStops<C>> for GradientRamp<C> {
|
||||
fn from(stops: GradientStops<C>) -> Self {
|
||||
Self { stops }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Gradient {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum GradientStopsFormat {
|
||||
Struct(GradientStops<Color>),
|
||||
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<f64> = stops.iter().map(|(p, _)| *p).collect();
|
||||
let mut gradient = Gradient::from(stops.into_iter().map(|(_, c)| c).collect::<Vec<_>>());
|
||||
gradient.set_positions(&position);
|
||||
gradient.elide_default_attributes();
|
||||
gradient
|
||||
}
|
||||
})
|
||||
impl From<Gradient> for GradientRamp {
|
||||
fn from(gradient: Gradient) -> Self {
|
||||
Self::from(&gradient)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GradientRamp> 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<SRGBA8> {
|
||||
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<SRGBA8>> for GradientRamp {
|
||||
fn from(stops: &GradientStops<SRGBA8>) -> Self {
|
||||
Self::from(Gradient::from(stops))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&GradientRamp> for GradientRamp<SRGBA8> {
|
||||
fn from(ramp: &GradientRamp) -> Self {
|
||||
Self { stops: ramp.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Gradient> for GradientRamp<SRGBA8> {
|
||||
fn from(gradient: &Gradient) -> Self {
|
||||
Self { stops: gradient.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&GradientRamp<SRGBA8>> for GradientRamp {
|
||||
fn from(ramp: &GradientRamp<SRGBA8>) -> 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::<Gradient>(&json).unwrap(), implicit);
|
||||
assert_eq!(serde_json::from_str::<GradientRamp>(&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::<Gradient>(&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::<GradientRamp>(&json).unwrap(), explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Color>` is the editor's in-memory
|
||||
/// form, while `FillChoice<SRGBA8>` 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<C = Color> {
|
||||
#[default]
|
||||
None,
|
||||
Solid(SRGBA8),
|
||||
Gradient(GradientStops<SRGBA8>),
|
||||
Solid(C),
|
||||
Gradient(GradientRamp<C>),
|
||||
}
|
||||
|
||||
impl From<&FillChoice> for FillChoiceUI {
|
||||
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for FillChoice<C> {
|
||||
type Static = FillChoice<C::Static>;
|
||||
}
|
||||
|
||||
impl From<&FillChoice> for FillChoice<SRGBA8> {
|
||||
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<SRGBA8>> for FillChoice {
|
||||
fn from(value: &FillChoice<SRGBA8>) -> 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<SRGBA8> {
|
||||
let Self::Solid(c) = self else { return None };
|
||||
Some(*c)
|
||||
impl<C: Copy> FillChoice<C> {
|
||||
pub fn as_solid(&self) -> Option<C> {
|
||||
let Self::Solid(color) = self else { return None };
|
||||
Some(*color)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_gradient(&self) -> Option<&GradientStops<SRGBA8>> {
|
||||
let Self::Gradient(g) = self else { return None };
|
||||
Some(g)
|
||||
impl<C> FillChoice<C> {
|
||||
pub fn as_gradient(&self) -> Option<&GradientRamp<C>> {
|
||||
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<SRGBA8> {
|
||||
/// 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<String> {
|
||||
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<Color> {
|
||||
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<String> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user