Split Fill's optional transform into a has-transform toggle and add the DashPattern and BoxCorners value types

This commit is contained in:
Keavon Chambers
2026-07-20 16:13:58 -07:00
committed by Dennis Kobert
parent 8e233d3229
commit 194c3d0a72
13 changed files with 449 additions and 122 deletions

View File

@@ -2,6 +2,7 @@ use super::PointId;
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::subpath::{BezierHandles, ManipulatorGroup};
use crate::vector::{SegmentId, Vector};
use core_types::list::{Item, List};
use dyn_any::DynAny;
use glam::DVec2;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
@@ -49,6 +50,77 @@ pub enum RowsOrColumns {
Columns,
}
/// A box's four corner values, such as a rectangle's corner radii, expanded on read from any number of stored
/// values by the CSS `border-radius` shorthand rules.
///
/// Wraps a `List<f64>` so the Data panel can introspect its values, mirroring how `DashPattern` wraps its lengths,
/// while remaining a single rank-0 value on the wire.
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
pub struct BoxCorners(pub List<f64>);
impl BoxCorners {
/// Expands the stored values to the four corners, clockwise from the top-left, by the CSS `border-radius` shorthand rules.
/// - `[]` → `[0, 0, 0, 0]`
/// - `[a]` → `[a, a, a, a]`
/// - `[a, b]` → `[a, b, a, b]`
/// - `[a, b, c]` → `[a, b, c, b]`
/// - `[a, b, c, d, …]` → `[a, b, c, d]`
pub fn to_corner_values(&self) -> [f64; 4] {
let values: Vec<f64> = self.0.iter_element_values().copied().collect();
match values.as_slice() {
[] => [0., 0., 0., 0.],
&[a] => [a, a, a, a],
&[a, b] => [a, b, a, b],
&[a, b, c] => [a, b, c, b],
&[a, b, c, d, ..] => [a, b, c, d],
}
}
}
// `List<f64>` is a runtime-only wire type, so serialize the corners as their bare values to keep documents stable
#[cfg(feature = "serde")]
impl serde::Serialize for BoxCorners {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.0.iter_element_values())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for BoxCorners {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::from(<Vec<f64> as serde::Deserialize>::deserialize(deserializer)?))
}
}
impl From<f64> for BoxCorners {
fn from(value: f64) -> Self {
Self(List::new_from_element(value))
}
}
impl From<Vec<f64>> for BoxCorners {
fn from(values: Vec<f64>) -> Self {
Self(values.into_iter().map(Item::new_from_element).collect())
}
}
impl From<&str> for BoxCorners {
fn from(text: &str) -> Self {
Self::from(
text.split([',', ' '])
.filter(|piece| !piece.is_empty())
.filter_map(|piece| piece.parse::<f64>().ok())
.collect::<Vec<f64>>(),
)
}
}
impl From<String> for BoxCorners {
fn from(text: String) -> Self {
Self::from(text.as_str())
}
}
pub trait AsU64 {
fn as_u64(&self) -> u64;
}

View File

@@ -3,12 +3,14 @@
pub use crate::gradient::*;
use core_types::Color;
use core_types::color::SRGBA8;
use core_types::list::{Item, List};
use core_types::transform::Transform;
use dyn_any::DynAny;
use glam::DAffine2;
use std::f64::consts::{PI, TAU};
/// Describes an editable fill choice, storing color or gradient stops without gradient placement metadata.
/// 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.
///
/// Can be None, a solid [Color], or a linear/radial [Gradient].
///
@@ -201,6 +203,65 @@ fn daffine2_identity() -> DAffine2 {
DAffine2::IDENTITY
}
/// A stroke's dash pattern: a sequence of lengths that alternate dash, gap, dash, gap, and so on. An odd-length
/// sequence repeats with the dash and gap roles swapped.
///
/// Wraps a `List<f64>` so the Data panel can introspect its lengths, mirroring how `Artboard` wraps a `List<Graphic>`,
/// while remaining a single rank-0 value on the wire.
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
pub struct DashPattern(pub List<f64>);
impl DashPattern {
/// Returns the dash lengths with any negative values clamped to zero.
pub fn clamped_lengths(&self) -> Vec<f64> {
self.0.iter_element_values().map(|length| length.max(0.)).collect()
}
}
// `List<f64>` is a runtime-only wire type, so serialize the pattern as its bare lengths to keep documents stable
#[cfg(feature = "serde")]
impl serde::Serialize for DashPattern {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.0.iter_element_values())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for DashPattern {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::from(<Vec<f64> as serde::Deserialize>::deserialize(deserializer)?))
}
}
impl From<f64> for DashPattern {
fn from(length: f64) -> Self {
Self(List::new_from_element(length))
}
}
impl From<Vec<f64>> for DashPattern {
fn from(lengths: Vec<f64>) -> Self {
Self(lengths.into_iter().map(Item::new_from_element).collect())
}
}
impl From<&str> for DashPattern {
fn from(text: &str) -> Self {
Self::from(
text.split([',', ' '])
.filter(|piece| !piece.is_empty())
.filter_map(|piece| piece.parse::<f64>().ok())
.collect::<Vec<f64>>(),
)
}
}
impl From<String> for DashPattern {
fn from(text: String) -> Self {
Self::from(text.as_str())
}
}
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]