mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Split Fill's optional transform into a has-transform toggle and add the DashPattern and BoxCorners value types
This commit is contained in:
committed by
Dennis Kobert
parent
6acc0638e4
commit
7ddab438d1
@@ -16,7 +16,10 @@ use dyn_any::DynAny;
|
||||
pub use dyn_any::StaticType;
|
||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphene_application_io::resource::ResourceHash;
|
||||
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::{self, ReferencePoint};
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
@@ -533,19 +536,21 @@ tagged_value! {
|
||||
DVec2(DVec2),
|
||||
#[serde(alias = "Affine2")]
|
||||
DAffine2(DAffine2),
|
||||
OptionalDAffine2(Option<DAffine2>),
|
||||
#[serde(alias = "FillGradient")]
|
||||
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
||||
Font(Font),
|
||||
Footprint(Footprint),
|
||||
VectorModification(Box<VectorModification>),
|
||||
ImageData(Image<Color>),
|
||||
Resource(graphene_application_io::resource::ResourceId),
|
||||
Resource(ResourceId),
|
||||
// Legacy
|
||||
#[serde(alias = "OptionalDAffine2")]
|
||||
LegacyOptionalDAffine2(Option<DAffine2>),
|
||||
#[serde(alias = "FillGradient")]
|
||||
LegacyGradient(graphic_types::migrations::legacy::LegacyGradient),
|
||||
#[serde(alias = "Fill")]
|
||||
LegacyFill(graphic_types::migrations::legacy::LegacyFill),
|
||||
// ==========
|
||||
// ENUM TYPES
|
||||
// ==========
|
||||
#[serde(alias = "Fill")]
|
||||
LegacyFill(graphic_types::migrations::legacy::LegacyFill),
|
||||
BlendMode(core_types::blending::BlendMode),
|
||||
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
||||
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
||||
@@ -575,6 +580,8 @@ tagged_value! {
|
||||
StrokeJoin(vector::style::StrokeJoin),
|
||||
StrokeAlign(vector::style::StrokeAlign),
|
||||
PaintOrder(vector::style::PaintOrder),
|
||||
DashPattern(vector::style::DashPattern),
|
||||
BoxCorners(vector::misc::BoxCorners),
|
||||
GradientType(vector::style::GradientType),
|
||||
GradientSpreadMethod(vector::style::GradientSpreadMethod),
|
||||
ReferencePoint(vector::ReferencePoint),
|
||||
@@ -723,6 +730,8 @@ impl TaggedValue {
|
||||
() 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::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
() if ty == TypeId::of::<DashPattern>() => TaggedValue::DashPattern(DashPattern::from(string)),
|
||||
() if ty == TypeId::of::<BoxCorners>() => TaggedValue::BoxCorners(BoxCorners::from(string)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(ty)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -4,44 +4,10 @@ use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use graphic_types::Vector;
|
||||
use vector_types::subpath;
|
||||
use vector_types::vector::misc::{ArcType, AsU64, GridType};
|
||||
use vector_types::vector::misc::{ArcType, AsU64, BoxCorners, GridType};
|
||||
use vector_types::vector::misc::{HandleId, SpiralType};
|
||||
use vector_types::vector::{PointId, SegmentId, StrokeId};
|
||||
|
||||
/// Expands the corner-radius lanes to four corners using the CSS
|
||||
/// `border-radius` shorthand rules, then builds the rounded rectangle.
|
||||
/// - `[a]` (also a plain scalar radius) expands to `[a, a, a, a]`
|
||||
/// - `[a, b]` expands to `[a, b, a, b]`
|
||||
/// - `[a, b, c]` expands to `[a, b, c, b]`
|
||||
/// - `[a, b, c, d, …]` truncates to `[a, b, c, d]`
|
||||
/// - `[]` expands to `[0, 0, 0, 0]`
|
||||
fn rounded_rectangle(values: &[f64], size: DVec2, clamped: bool) -> Vector {
|
||||
let radii: [f64; 4] = match values {
|
||||
[] => [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],
|
||||
};
|
||||
|
||||
let clamped_radius = if clamped {
|
||||
// Algorithm follows the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
|
||||
|
||||
let mut scale_factor: f64 = 1.;
|
||||
for i in 0..4 {
|
||||
let side_length = if i % 2 == 0 { size.x } else { size.y };
|
||||
let adjacent_corner_radius_sum = radii[i] + radii[(i + 1) % 4];
|
||||
if side_length < adjacent_corner_radius_sum {
|
||||
scale_factor = scale_factor.min(side_length / adjacent_corner_radius_sum);
|
||||
}
|
||||
}
|
||||
radii.map(|x| x * scale_factor)
|
||||
} else {
|
||||
radii
|
||||
};
|
||||
Vector::from_subpath(subpath::Subpath::new_rounded_rectangle(size / -2., size / 2., clamped_radius))
|
||||
}
|
||||
|
||||
/// Generates a circle shape with a chosen radius.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn circle(
|
||||
@@ -143,12 +109,43 @@ fn rectangle(
|
||||
#[unit(" px")]
|
||||
#[default(100)]
|
||||
height: f64,
|
||||
_individual_corner_radii: bool, // TODO: Move this to the bottom once we have a migration capability
|
||||
corner_radius: IList<f64>,
|
||||
corner_radius: BoxCorners,
|
||||
#[default(true)] clamped: bool,
|
||||
_individual_corner_radii: bool,
|
||||
) -> Vector {
|
||||
let values: Vec<f64> = (0..corner_radius.len()).map(|index| corner_radius.get(index)).collect();
|
||||
rounded_rectangle(&values, DVec2::new(width, height), clamped)
|
||||
let size = DVec2::new(width, height);
|
||||
let radii = corner_radius.to_corner_values();
|
||||
|
||||
// Scale down overlapping adjacent radii to fit, following the CSS spec: <https://drafts.csswg.org/css-backgrounds/#corner-overlap>
|
||||
let radii = if clamped {
|
||||
let radii = radii.map(|radius| radius.max(0.));
|
||||
|
||||
let mut scale_factor: f64 = 1.;
|
||||
for i in 0..4 {
|
||||
let side_length = if i % 2 == 0 { size.x } else { size.y };
|
||||
let adjacent_corner_radius_sum = radii[i] + radii[(i + 1) % 4];
|
||||
if side_length < adjacent_corner_radius_sum {
|
||||
scale_factor = scale_factor.min((side_length / adjacent_corner_radius_sum).max(0.));
|
||||
}
|
||||
}
|
||||
|
||||
radii.map(|radius| radius * scale_factor)
|
||||
} else {
|
||||
radii
|
||||
};
|
||||
|
||||
Vector::from_subpath(subpath::Subpath::new_rounded_rectangle(size / -2., size / 2., radii))
|
||||
}
|
||||
|
||||
/// Builds a set of four corner values, such as a rectangle's corner radii, from a list of one, two, three, or four values.
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn box_corners(
|
||||
_: impl Ctx,
|
||||
/// The corner values, filling the four corners clockwise from the top-left. Give one value for all corners, two for opposite pairs, three for top-left, the two sides, then bottom-right, or four for each corner.
|
||||
values: IList<f64>,
|
||||
) -> BoxCorners {
|
||||
let values: Vec<f64> = (0..values.len()).map(|index| values.get(index)).collect();
|
||||
BoxCorners::from(values)
|
||||
}
|
||||
|
||||
/// Generates an regular polygon shape like a triangle, square, pentagon, hexagon, heptagon, octagon, or any higher n-gon.
|
||||
|
||||
@@ -37,7 +37,7 @@ use vector_types::vector::misc::{
|
||||
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
||||
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
||||
};
|
||||
use vector_types::vector::style::{Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
|
||||
use vector_types::{GradientSpreadMethod, GradientType};
|
||||
@@ -325,10 +325,11 @@ fn fill<'e>(
|
||||
_backup_gradient: IList<Gradient>,
|
||||
_gradient_type: GradientType,
|
||||
_spread_method: GradientSpreadMethod,
|
||||
_transform: Option<DAffine2>,
|
||||
_has_transform: bool,
|
||||
_transform: DAffine2,
|
||||
) -> Result<(Vector, Attr<'e, Fill>), Interrupt> {
|
||||
let mut paint = paint_table(fill);
|
||||
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _spread_method, _transform);
|
||||
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _spread_method, _has_transform.then_some(_transform));
|
||||
let parked = park_paint(ctx.arena(), paint)?;
|
||||
Ok((element, Attr(Some(parked))))
|
||||
}
|
||||
@@ -345,14 +346,15 @@ fn fill_graphic_leveled<'e>(
|
||||
_backup_gradient: IList<Gradient>,
|
||||
_gradient_type: GradientType,
|
||||
_spread_method: GradientSpreadMethod,
|
||||
_transform: Option<DAffine2>,
|
||||
_has_transform: bool,
|
||||
_transform: DAffine2,
|
||||
) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> {
|
||||
let bounds = match BoundingBox::bounding_box(&element, DAffine2::IDENTITY, false) {
|
||||
RenderBoundingBox::Rectangle(bounds) => Some(bounds),
|
||||
_ => None,
|
||||
};
|
||||
let mut paint = paint_table(fill);
|
||||
default_gradient_paint(&mut paint, bounds, _gradient_type, _spread_method, _transform);
|
||||
default_gradient_paint(&mut paint, bounds, _gradient_type, _spread_method, _has_transform.then_some(_transform));
|
||||
let parked = park_paint(ctx.arena(), paint)?;
|
||||
Ok((element, Attr(Some(parked))))
|
||||
}
|
||||
@@ -381,13 +383,13 @@ fn stroke<'e>(
|
||||
miter_limit: f64,
|
||||
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
||||
paint_order: PaintOrder,
|
||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_lengths: IList<f64>,
|
||||
/// The stroke dash pattern. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_pattern: DashPattern,
|
||||
/// The phase offset distance from the starting point of the dash pattern.
|
||||
#[unit(" px")]
|
||||
dash_offset: f64,
|
||||
) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> {
|
||||
let dash_lengths = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
|
||||
let dash_lengths = dash_pattern.clamped_lengths();
|
||||
let mut stroke = Stroke {
|
||||
weight,
|
||||
dash_lengths,
|
||||
@@ -442,10 +444,10 @@ fn stroke_graphic_leveled<'e>(
|
||||
join: StrokeJoin,
|
||||
#[default(4.)] miter_limit: f64,
|
||||
paint_order: PaintOrder,
|
||||
dash_lengths: IList<f64>,
|
||||
dash_pattern: DashPattern,
|
||||
#[unit(" px")] dash_offset: f64,
|
||||
) -> Result<(Graphic<'static>, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> {
|
||||
let dash_lengths = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
|
||||
let dash_lengths = dash_pattern.clamped_lengths();
|
||||
let stroke = Stroke {
|
||||
weight,
|
||||
dash_lengths,
|
||||
|
||||
Reference in New Issue
Block a user