mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Remove the legacy Fill, Gradient, and PathStyle types now that paints live in attributes (#4297)
* Remove `Stroke.color` * Remove legacy Gradient struct usage * Fix gradient tool tests * Remove `Fill` enum * Fix direction of `gradient_orientation_rightward` * Remove `Gradient` struct * Remove `PathStyle` struct * Fix old `VectorData` migration * Refactor - Remove unnecessary wrapper functions - Use `as_ref` instead of `clone` for stroke if possible - Comment cleanup * Clean up stale style comments and bindings left by the paint attribute migration --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -13,7 +13,7 @@ pub use dyn_any::StaticType;
|
||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphene_application_io::resource::ResourceHash;
|
||||
use graphic_types::raster_types::{CPU, Image, Raster};
|
||||
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops};
|
||||
use graphic_types::vector_types::vector::style::GradientStops;
|
||||
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
use rendering::RenderMetadata;
|
||||
@@ -65,7 +65,7 @@ macro_rules! tagged_value {
|
||||
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
|
||||
Color(Option<Color>),
|
||||
/// Stored compactly as a `GradientStops`, materializes as a single-row `List<GradientStops>` 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 `FillGradient` by `deserialize_tagged_value_with_legacy_migration`.)
|
||||
/// (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(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
|
||||
#[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")]
|
||||
Gradient(GradientStops),
|
||||
@@ -403,7 +403,8 @@ tagged_value! {
|
||||
#[serde(alias = "Affine2")]
|
||||
DAffine2(DAffine2),
|
||||
OptionalDAffine2(Option<DAffine2>),
|
||||
FillGradient(Gradient),
|
||||
#[serde(alias = "FillGradient")]
|
||||
LegacyGradient(graphic_types::migrations::legacy::Gradient),
|
||||
Font(Font),
|
||||
Footprint(Footprint),
|
||||
VectorModification(Box<VectorModification>),
|
||||
@@ -412,7 +413,8 @@ tagged_value! {
|
||||
// ==========
|
||||
// ENUM TYPES
|
||||
// ==========
|
||||
Fill(vector::style::Fill),
|
||||
#[serde(alias = "Fill")]
|
||||
LegacyFill(graphic_types::migrations::legacy::Fill),
|
||||
BlendMode(core_types::blending::BlendMode),
|
||||
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
||||
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
||||
@@ -587,7 +589,6 @@ impl TaggedValue {
|
||||
// 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(|color| TaggedValue::Color(Some(color)))?,
|
||||
() if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
|
||||
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
||||
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
_ => return None,
|
||||
};
|
||||
@@ -655,11 +656,11 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
|
||||
}
|
||||
return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Vector>))));
|
||||
}
|
||||
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `FillGradient`), and now carries an `Option<GradientStops>`.
|
||||
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<GradientStops>`.
|
||||
// Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `GradientStops` has none of those (it has `position`/`midpoint`/`color`).
|
||||
"Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => {
|
||||
let gradient: Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
|
||||
return Ok(MemoHash::new(TaggedValue::FillGradient(gradient)));
|
||||
let gradient: graphic_types::migrations::legacy::Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
|
||||
return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -137,13 +137,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
|
||||
@@ -250,14 +248,12 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List};
|
||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, ItemAttributeValues, List};
|
||||
use core_types::ops::{FromAnchorPosition, ListConvert};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use std::borrow::Cow;
|
||||
use vector_types::GradientStops;
|
||||
pub use vector_types::Vector;
|
||||
use vector_types::vector::style::Fill;
|
||||
|
||||
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
||||
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
|
||||
@@ -191,30 +190,6 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
||||
output
|
||||
}
|
||||
|
||||
/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
|
||||
match fill {
|
||||
Fill::None => None,
|
||||
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
|
||||
Fill::Gradient(gradient) => {
|
||||
let gradient_item = Item::new_from_element(gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient.transform * gradient.to_transform())
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
|
||||
let gradient_list = List::new_from_item(gradient_item);
|
||||
|
||||
Some(List::new_from_element(Graphic::Gradient(gradient_list)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a `Color` into the `List<Graphic>` representation used as paint storage.
|
||||
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||
pub fn color_to_graphic_list(color: Option<Color>) -> Option<List<Graphic>> {
|
||||
color.as_ref().map(|color| List::new_from_element((*color).into()))
|
||||
}
|
||||
|
||||
/// Whether a normalized paint graphic list actually carries renderable paint.
|
||||
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
|
||||
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
|
||||
@@ -225,7 +200,7 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
|
||||
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
|
||||
list.attribute::<List<Graphic>>(attribute, index)
|
||||
.map(Cow::Borrowed)
|
||||
// Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it.
|
||||
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
|
||||
.filter(|graphic_list| is_paint_present(graphic_list))
|
||||
}
|
||||
|
||||
@@ -245,86 +220,6 @@ pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, pa
|
||||
list.set_attribute(key, index, paint.into_graphic_list());
|
||||
}
|
||||
|
||||
/// Look up the fill paint graphics for a vector item, falling back to the legacy
|
||||
/// `style.fill` when the attribute is absent or empty.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn fill_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
|
||||
graphic_list_at(list, index, ATTR_FILL).or_else(|| {
|
||||
let vector = list.element(index)?;
|
||||
fill_to_graphic_list(vector.style.fill()).map(Cow::Owned)
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up the stroke paint graphics for a vector item, falling back to the legacy
|
||||
/// `style.stroke.color` when the attribute is absent or empty.
|
||||
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||
pub fn stroke_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
|
||||
graphic_list_at(list, index, ATTR_STROKE).or_else(|| {
|
||||
let vector = list.element(index)?;
|
||||
color_to_graphic_list(vector.style.stroke().and_then(|s| s.color())).map(Cow::Owned)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether the fill paint for a vector item is fully opaque, falling back to
|
||||
/// the legacy `style.fill` when the attribute is absent.
|
||||
/// This avoids the `List<Graphic>` allocation that the legacy `Fill` fallback path performs.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn is_fill_opaque_at(list: &List<Vector>, index: usize) -> bool {
|
||||
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_FILL) {
|
||||
return graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque());
|
||||
}
|
||||
let Some(vector) = list.element(index) else { return false };
|
||||
match vector.style.fill() {
|
||||
Fill::None => false,
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|stop| stop.color.is_opaque()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the fill paint for a vector item is fully transparent, falling back to
|
||||
/// the legacy `style.fill` when the attribute is absent.
|
||||
/// This avoids the `List<Graphic>` allocation that the legacy `Fill` fallback path performs.
|
||||
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
|
||||
pub fn is_fill_fully_transparent_at(list: &List<Vector>, index: usize) -> bool {
|
||||
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_FILL) {
|
||||
return graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent());
|
||||
}
|
||||
let Some(vector) = list.element(index) else { return false };
|
||||
match vector.style.fill() {
|
||||
Fill::None => true,
|
||||
Fill::Solid(color) => color.a() == 0.,
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|stop| stop.color.a() == 0.),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the stroke paint for a vector item is fully opaque, falling back to
|
||||
/// the legacy `style.stroke.color` when the attribute is absent.
|
||||
/// This avoids the `List<Graphic>` allocation that the legacy `Stroke.color` fallback path performs.
|
||||
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||
pub fn is_stroke_opaque_at(list: &List<Vector>, index: usize) -> bool {
|
||||
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_STROKE) {
|
||||
return graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque());
|
||||
}
|
||||
let Some(color) = list.element(index).and_then(|vector| vector.style.stroke()).and_then(|stroke| stroke.color()) else {
|
||||
return false;
|
||||
};
|
||||
color.is_opaque()
|
||||
}
|
||||
|
||||
/// Check whether the stroke paint for a vector item is fully transparent, falling back to
|
||||
/// the legacy `style.stroke.color` when the attribute is absent.
|
||||
/// This avoids the `List<Graphic>` allocation that the legacy `Stroke.color` fallback path performs.
|
||||
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||
pub fn is_stroke_fully_transparent_at(list: &List<Vector>, index: usize) -> bool {
|
||||
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_STROKE) {
|
||||
return graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent());
|
||||
}
|
||||
let Some(color) = list.element(index).and_then(|vector| vector.style.stroke()).and_then(|stroke| stroke.color()) else {
|
||||
return true;
|
||||
};
|
||||
color.a() == 0.
|
||||
}
|
||||
|
||||
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
|
||||
/// canonical `List<Graphic>` fill and stroke attributes.
|
||||
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
|
||||
@@ -544,17 +439,10 @@ impl Graphic {
|
||||
let Some(element) = vector.element(index) else { return false };
|
||||
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
|
||||
let fill_opaque_or_absent = match graphic_list_at(vector, index, ATTR_FILL) {
|
||||
Some(graphic_list) => graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()),
|
||||
None => element.style.fill().is_opaque(),
|
||||
};
|
||||
let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
|
||||
|
||||
let stroke_invisible_or_transparent = element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
|| if let Some(graphic_list) = graphic_list_at(vector, index, ATTR_STROKE) {
|
||||
graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())
|
||||
} else {
|
||||
element.style.stroke().and_then(|stroke| stroke.color()).is_none_or(|color| color.a() == 0.)
|
||||
};
|
||||
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
|| graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||
|
||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
||||
}),
|
||||
@@ -566,13 +454,15 @@ impl Graphic {
|
||||
match self {
|
||||
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
||||
Graphic::Vector(list) => {
|
||||
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
|
||||
|
||||
!list.is_empty()
|
||||
&& (0..list.len()).all(|i| {
|
||||
let Some(vector) = list.element(i) else { return false };
|
||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
||||
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_fill_opaque_at(list, i);
|
||||
let stroke_opaque_or_invisible = vector.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_stroke_opaque_at(list, i);
|
||||
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i);
|
||||
let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i);
|
||||
opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible
|
||||
})
|
||||
}
|
||||
@@ -587,13 +477,16 @@ impl Graphic {
|
||||
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
||||
Graphic::Vector(list) => (0..list.len()).all(|i| {
|
||||
let Some(vector) = list.element(i) else { return false };
|
||||
let is_paint_fully_transparent_at =
|
||||
|key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
||||
|
||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
||||
if opacity <= f64::EPSILON {
|
||||
return true;
|
||||
}
|
||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
||||
let fill_invisible = opacity_fill <= f64::EPSILON || is_fill_fully_transparent_at(list, i);
|
||||
let stroke_invisible = vector.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_stroke_fully_transparent_at(list, i);
|
||||
let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i);
|
||||
let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i);
|
||||
fill_invisible && stroke_invisible
|
||||
}),
|
||||
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
|
||||
@@ -785,6 +678,7 @@ mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod graphic_is_opaque_tests {
|
||||
use core_types::ATTR_SPREAD_METHOD;
|
||||
use vector_types::{GradientSpreadMethod, GradientStop};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -11,50 +11,174 @@ pub use artboard::Artboard;
|
||||
pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector};
|
||||
|
||||
pub mod migrations {
|
||||
use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId};
|
||||
|
||||
use crate::Vector;
|
||||
|
||||
// Storing legacy structs that are only used in document migration.
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (a single `Vector`, the old `OldVectorData` flat struct, or any of the historical `List<Vector>` variants).
|
||||
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
pub mod legacy {
|
||||
use core_types::Color;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke};
|
||||
use vector_types::{GradientStops, Vector, vector};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Gradient {
|
||||
pub stops: GradientStops,
|
||||
pub gradient_type: vector::style::GradientType,
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
#[serde(default)]
|
||||
pub spread_method: vector::style::GradientSpreadMethod,
|
||||
#[serde(default)]
|
||||
pub absolute: bool,
|
||||
#[serde(default)]
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
|
||||
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
|
||||
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
|
||||
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
|
||||
let start = bounding_box.transform_point2(self.start);
|
||||
let end = bounding_box.transform_point2(self.end);
|
||||
let direction = end - start;
|
||||
|
||||
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
|
||||
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
|
||||
let radial_invertible = self.gradient_type == vector::style::GradientType::Radial
|
||||
&& layer_transform.is_finite()
|
||||
&& layer_transform.matrix2.determinant().recip().is_finite()
|
||||
&& direction.length_squared() > 1e-20;
|
||||
let transform = if radial_invertible {
|
||||
let radius = (layer_transform.matrix2 * direction).length();
|
||||
let circle = DAffine2 {
|
||||
matrix2: glam::DMat2::from_diagonal(DVec2::splat(radius)),
|
||||
translation: layer_transform.transform_point2(start),
|
||||
};
|
||||
let base = DAffine2::from_cols(direction, direction.perp(), start);
|
||||
(layer_transform.inverse() * circle) * base.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
absolute: true,
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0, 0) -> (1, 0).
|
||||
pub fn to_transform(&self) -> DAffine2 {
|
||||
let direction = self.end - self.start;
|
||||
DAffine2::from_cols(direction, direction.perp(), self.start)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Fill {
|
||||
#[default]
|
||||
None,
|
||||
Solid(Color),
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
||||
/// recovers editable vector data. The fill/stroke paints are migrated from the the node inputs.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
||||
pub(super) struct PathStyle {
|
||||
pub stroke: Option<Stroke>,
|
||||
}
|
||||
|
||||
/// Old documents stored a `Vector` flattened with list attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OldVectorData {
|
||||
style: PathStyle,
|
||||
colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
point_domain: PointDomain,
|
||||
segment_domain: SegmentDomain,
|
||||
region_domain: RegionDomain,
|
||||
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
||||
pub(super) struct VectorData {
|
||||
pub style: PathStyle,
|
||||
pub colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
pub point_domain: PointDomain,
|
||||
pub segment_domain: SegmentDomain,
|
||||
pub region_domain: RegionDomain,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct LegacyTable {
|
||||
pub(super) struct Table {
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<Vector>,
|
||||
pub element: Vec<Vector>,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a single `Vector`, or any of the historical `List<Vector>` variants).
|
||||
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum VectorFormat {
|
||||
// Old vector data must be tried first. Serde would otherwise ignore its `style` field and
|
||||
// deserialize the missing optional `stroke` field as `None` in the current `Vector`.
|
||||
OldVectorData(legacy::VectorData),
|
||||
Vector(Vector),
|
||||
OldVectorData(OldVectorData),
|
||||
List(LegacyTable),
|
||||
List(legacy::Table),
|
||||
}
|
||||
|
||||
Ok(match VectorFormat::deserialize(deserializer)? {
|
||||
VectorFormat::Vector(vector) => Some(vector),
|
||||
VectorFormat::OldVectorData(old) => Some(Vector {
|
||||
style: old.style,
|
||||
stroke: old.style.stroke,
|
||||
colinear_manipulators: old.colinear_manipulators,
|
||||
point_domain: old.point_domain,
|
||||
segment_domain: old.segment_domain,
|
||||
region_domain: old.region_domain,
|
||||
}),
|
||||
VectorFormat::Vector(vector) => Some(vector),
|
||||
VectorFormat::List(list) => list.element.into_iter().next(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod migration_tests {
|
||||
use super::*;
|
||||
use vector_types::vector::style::Stroke;
|
||||
|
||||
#[test]
|
||||
fn preserves_stroke_from_old_vector_data_style() {
|
||||
let old_vector = legacy::VectorData {
|
||||
style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut value = serde_json::to_value(old_vector).unwrap();
|
||||
value
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.get_mut("style")
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
|
||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
||||
|
||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_stroke_from_current_vector_data() {
|
||||
let vector = Vector {
|
||||
stroke: Some(Stroke::new(12.)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&vector).unwrap();
|
||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
||||
|
||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_hash::CacheHashWrapper;
|
||||
use graphene_resource::Resource;
|
||||
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, stroke_graphic_list_at};
|
||||
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
|
||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
|
||||
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
||||
use graphic_types::vector_types::subpath::Subpath;
|
||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
|
||||
use num_traits::Zero;
|
||||
@@ -459,12 +459,10 @@ pub struct RenderMetadata {
|
||||
pub text_frames: HashMap<NodeId, DAffine2>,
|
||||
pub clip_targets: HashSet<NodeId>,
|
||||
pub vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
/// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read paint
|
||||
/// information that lives on the list rather than on `PathStyle.fill`.
|
||||
/// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
||||
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read
|
||||
/// stroke paint information that lives on the list rather than on `Stroke.color`.
|
||||
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
||||
pub backgrounds: Vec<Background>,
|
||||
@@ -1067,7 +1065,7 @@ impl Render for List<Vector> {
|
||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
|
||||
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
|
||||
let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform);
|
||||
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
|
||||
@@ -1087,26 +1085,26 @@ impl Render for List<Vector> {
|
||||
path.push_str(bezpath.to_svg().as_str());
|
||||
}
|
||||
|
||||
let mask_type = if vector.style.stroke().map(|x| x.align) == Some(StrokeAlign::Inside) {
|
||||
let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) {
|
||||
MaskType::Clip
|
||||
} else {
|
||||
MaskType::Mask
|
||||
};
|
||||
|
||||
let fill_graphic_list = fill_graphic_list_at(self, index);
|
||||
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
|
||||
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));
|
||||
|
||||
let stroke_graphic_list = stroke_graphic_list_at(self, index);
|
||||
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
|
||||
let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0));
|
||||
|
||||
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
|
||||
let can_draw_aligned_stroke = path_is_closed
|
||||
&& vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||
&& vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
|
||||
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
|
||||
|
||||
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
|
||||
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
|
||||
let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
|
||||
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
|
||||
let use_face_fill = vector.use_face_fill();
|
||||
|
||||
@@ -1127,7 +1125,7 @@ impl Render for List<Vector> {
|
||||
let id = format!("alignment-{}", generate_uuid());
|
||||
|
||||
let mut cloned_vector = vector.clone();
|
||||
cloned_vector.style.clear_stroke();
|
||||
cloned_vector.stroke = None;
|
||||
|
||||
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
|
||||
// The wrapping SVG group (above) handles the user-set opacity.
|
||||
@@ -1167,7 +1165,7 @@ impl Render for List<Vector> {
|
||||
if let Some((ref id, mask_type, ref vector_item)) = push_id {
|
||||
let mut svg = SvgRender::new();
|
||||
vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
|
||||
let stroke = vector.style.stroke().unwrap();
|
||||
let stroke = vector.stroke.as_ref().unwrap();
|
||||
// `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed`
|
||||
let (largest_scale, _) = singular_values(applied_stroke_transform);
|
||||
let inflation = stroke.max_aabb_inflation(true) * largest_scale;
|
||||
@@ -1195,8 +1193,8 @@ impl Render for List<Vector> {
|
||||
render_params.override_paint_order = override_paint_order;
|
||||
|
||||
let stroke_shape_attribute = vector
|
||||
.style
|
||||
.stroke()
|
||||
.stroke
|
||||
.as_ref()
|
||||
.map(|stroke| {
|
||||
if stroke_graphic_list.as_deref().is_some_and(is_paint_present) {
|
||||
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
|
||||
@@ -1207,7 +1205,7 @@ impl Render for List<Vector> {
|
||||
.unwrap_or_default();
|
||||
|
||||
// Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback
|
||||
let stroke_visible = vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
|
||||
let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
|
||||
let stroke_attribute = if stroke_visible {
|
||||
stroke_graphic_list
|
||||
.as_deref()
|
||||
@@ -1282,7 +1280,7 @@ impl Render for List<Vector> {
|
||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
let multiplied_transform = parent_transform * item_transform;
|
||||
let has_real_stroke = element.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
|
||||
let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
|
||||
let mut element_transform = set_stroke_transform
|
||||
@@ -1306,8 +1304,8 @@ impl Render for List<Vector> {
|
||||
}
|
||||
}
|
||||
|
||||
let fill_graphic_list = fill_graphic_list_at(self, index);
|
||||
let stroke_graphic_list = stroke_graphic_list_at(self, index);
|
||||
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
|
||||
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
|
||||
|
||||
// If we're using opacity or a blend mode, we need to push a layer
|
||||
let blend_mode = match render_params.render_mode {
|
||||
@@ -1319,10 +1317,10 @@ impl Render for List<Vector> {
|
||||
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
|
||||
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
|
||||
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
|
||||
let stroke = element.style.stroke();
|
||||
let stroke = element.stroke.as_ref();
|
||||
let stroke_fully_transparent = stroke_graphic_list.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
|
||||
let can_draw_aligned_stroke =
|
||||
!stroke_fully_transparent && stroke.as_ref().is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
|
||||
!stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
|
||||
|
||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||
if opacity < 1. || blend_mode_attr != BlendMode::default() {
|
||||
@@ -1330,7 +1328,7 @@ impl Render for List<Vector> {
|
||||
// `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
|
||||
// Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
|
||||
let (_, smallest_scale) = singular_values(applied_stroke_transform);
|
||||
let stroke_inflation = stroke.as_ref().map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
|
||||
let stroke_inflation = stroke.map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
|
||||
let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. };
|
||||
let quad = Quad::from_box(layer_bounds).inflate(inflate_amount);
|
||||
let layer_bounds = quad.bounding_box();
|
||||
@@ -1344,7 +1342,7 @@ impl Render for List<Vector> {
|
||||
}
|
||||
|
||||
let use_layer = can_draw_aligned_stroke;
|
||||
let wants_stroke_below = stroke.as_ref().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
|
||||
let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
|
||||
|
||||
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
|
||||
let Some(fill_graphic) = fill_graphic_list.as_deref() else { return };
|
||||
@@ -1401,7 +1399,7 @@ impl Render for List<Vector> {
|
||||
|
||||
let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| {
|
||||
let Some(stroke_graphic_list) = stroke_graphic_list.as_deref() else { return };
|
||||
let Some(stroke) = element.style.stroke() else { return };
|
||||
let Some(stroke) = stroke else { return };
|
||||
|
||||
for paint_index in 0..stroke_graphic_list.len() {
|
||||
let Some(stroke_graphic) = stroke_graphic_list.element(paint_index) else {
|
||||
@@ -1474,7 +1472,7 @@ impl Render for List<Vector> {
|
||||
_ => {
|
||||
if use_layer {
|
||||
let mut cloned_element = element.clone();
|
||||
cloned_element.style.clear_stroke();
|
||||
cloned_element.stroke = None;
|
||||
|
||||
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
|
||||
// The outer opacity/blend layer (above) handles the user-set opacity.
|
||||
@@ -1484,13 +1482,13 @@ impl Render for List<Vector> {
|
||||
|
||||
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
|
||||
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
|
||||
let inflation = element.style.stroke().as_ref().map_or(0., |stroke| stroke.max_aabb_inflation(true));
|
||||
let inflation = stroke.map_or(0., |stroke| stroke.max_aabb_inflation(true));
|
||||
let (largest_scale, _) = singular_values(applied_stroke_transform);
|
||||
let quad = Quad::from_box(bounds).inflate(inflation * largest_scale);
|
||||
let bounds = quad.bounding_box();
|
||||
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||
|
||||
let compose = if element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) {
|
||||
let compose = if stroke.is_some_and(|x| x.align == StrokeAlign::Outside) {
|
||||
peniko::Compose::SrcOut
|
||||
} else {
|
||||
peniko::Compose::SrcIn
|
||||
@@ -1527,7 +1525,7 @@ impl Render for List<Vector> {
|
||||
Stroke,
|
||||
}
|
||||
|
||||
let order = match element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) {
|
||||
let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) {
|
||||
true => [Op::Stroke, Op::Fill],
|
||||
false => [Op::Fill, Op::Stroke], // Default
|
||||
};
|
||||
@@ -1596,9 +1594,9 @@ impl Render for List<Vector> {
|
||||
accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new));
|
||||
|
||||
// Source geometry (not the click-target override) so editing tools work on letterforms.
|
||||
// Recorded together with `vector_data` from the same (first) row so `style` stays consistent with the paint.
|
||||
// Recorded together with `vector_data` from the same (first) row so stroke geometry stays consistent with the paint.
|
||||
// Only item 0 is recorded since editing tools can only target a single item currently.
|
||||
// If that row has no paint attribute, none is recorded and consumers fall back to `style`.
|
||||
// If that row has no paint attribute, none is recorded.
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
|
||||
e.insert(Arc::new(source.clone()));
|
||||
|
||||
@@ -1668,14 +1666,14 @@ impl Render for List<Vector> {
|
||||
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
|
||||
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
|
||||
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) {
|
||||
let filled = has_paint_at(vector_list, index, ATTR_FILL) || vector_list.element(index).is_some_and(|vector| !matches!(vector.style.fill(), Fill::None));
|
||||
let filled = has_paint_at(vector_list, index, ATTR_FILL);
|
||||
|
||||
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
|
||||
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
|
||||
|
||||
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
|
||||
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
|
||||
let stroke_width = geometry.style.stroke().map_or(0., |stroke| {
|
||||
let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| {
|
||||
if stroke.align.is_not_centered() && all_subpaths_closed {
|
||||
stroke.weight * 2.
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@ use core_types::Color;
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||
@@ -491,163 +491,6 @@ impl GradientSpreadMethod {
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient fill.
|
||||
///
|
||||
/// Contains the start and end points, along with the colors at varying points along the length.
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Gradient {
|
||||
pub stops: GradientStops,
|
||||
pub gradient_type: GradientType,
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub spread_method: GradientSpreadMethod,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Whether `start`/`end` are absolute (layer-space) rather than in the legacy [0,1] bounding-box space.
|
||||
/// Documents predating the gradient migration deserialize this as `false`; the deferred migration converts
|
||||
/// them and sets it `true`. Once all documents are migrated, the legacy rendering path can be removed.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub absolute: bool,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// An extra frame adjustment composed onto the `start`/`end` frame (`transform * to_transform()`), letting the gradient
|
||||
/// describe shapes the endpoint pair cannot, such as an elliptical radial. It defaults to identity, so existing documents
|
||||
/// (which only have `start`/`end`) are unaffected; the migration stores a non-identity value only where it's needed.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Default for Gradient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stops: GradientStops::default(),
|
||||
gradient_type: GradientType::Linear,
|
||||
start: DVec2::new(0., 0.5),
|
||||
end: DVec2::new(1., 0.5),
|
||||
spread_method: GradientSpreadMethod::Pad,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Gradient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
let stops = self
|
||||
.stops
|
||||
.iter()
|
||||
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), SRGBA8::from(stop.color).to_rgba_hex()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
|
||||
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
|
||||
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
|
||||
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
|
||||
let start = bounding_box.transform_point2(self.start);
|
||||
let end = bounding_box.transform_point2(self.end);
|
||||
let direction = end - start;
|
||||
|
||||
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
|
||||
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
|
||||
let radial_invertible =
|
||||
self.gradient_type == GradientType::Radial && layer_transform.is_finite() && layer_transform.matrix2.determinant().recip().is_finite() && direction.length_squared() > 1e-20;
|
||||
let transform = if radial_invertible {
|
||||
let radius = (layer_transform.matrix2 * direction).length();
|
||||
let circle = DAffine2 {
|
||||
matrix2: DMat2::from_diagonal(DVec2::splat(radius)),
|
||||
translation: layer_transform.transform_point2(start),
|
||||
};
|
||||
let base = DAffine2::from_cols(direction, direction.perp(), start);
|
||||
(layer_transform.inverse() * circle) * base.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY
|
||||
};
|
||||
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self {
|
||||
let stops = GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: start_color,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: end_color,
|
||||
},
|
||||
]);
|
||||
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
stops,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a stop into the gradient, the index if successful
|
||||
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
|
||||
// Transform the start and end positions to the same coordinate space as the mouse.
|
||||
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
|
||||
|
||||
// Calculate the new position by finding the closest point on the line
|
||||
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||
|
||||
// Don't insert point past end of line
|
||||
if !(0. ..=1.).contains(&new_position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute the color of the inserted stop using evaluate (which respects midpoints)
|
||||
let new_color = self.stops.evaluate(new_position);
|
||||
|
||||
// Compute the correct index to keep the positions in order
|
||||
let mut index = 0;
|
||||
while self.stops.len() > index && self.stops.position[index] <= new_position {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// Insert the new stop, duplicating the midpoint ratio of the interval being split
|
||||
let inherited_midpoint = if index > 0 { self.stops.midpoint[index - 1] } else { 0.5 };
|
||||
self.stops.position.insert(index, new_position);
|
||||
self.stops.midpoint.insert(index, inherited_midpoint);
|
||||
self.stops.color.insert(index, new_color);
|
||||
|
||||
Some(index)
|
||||
}
|
||||
|
||||
/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0, 0) -> (1, 0).
|
||||
pub fn to_transform(&self) -> DAffine2 {
|
||||
let direction = self.end - self.start;
|
||||
DAffine2::from_cols(direction, direction.perp(), self.start)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
|
||||
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
|
||||
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
|
||||
@@ -732,27 +575,3 @@ impl core_types::bounds::BoundingBox for GradientStops {
|
||||
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use glam::DVec2;
|
||||
|
||||
fn linear_gradient(start: DVec2, end: DVec2) -> Gradient {
|
||||
Gradient { start, end, ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_transform_roundtrip() {
|
||||
let cases = [(DVec2::ZERO, DVec2::X), (DVec2::new(10., 20.), DVec2::new(50., 30.)), (DVec2::new(-5., -5.), DVec2::new(5., 3.))];
|
||||
|
||||
for (start, end) in cases {
|
||||
let transform = linear_gradient(start, end).to_transform();
|
||||
let recovered_start = transform.transform_point2(DVec2::ZERO);
|
||||
let recovered_end = transform.transform_point2(DVec2::X);
|
||||
|
||||
assert!((recovered_start - start).length() < 1e-10);
|
||||
assert!((recovered_end - end).length() < 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ mod vector_modification;
|
||||
mod vector_types;
|
||||
|
||||
pub use reference_point::*;
|
||||
pub use style::PathStyle;
|
||||
pub use vector_attributes::*;
|
||||
pub use vector_modification::*;
|
||||
pub use vector_types::*;
|
||||
|
||||
@@ -1,150 +1,16 @@
|
||||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
pub use crate::gradient::*;
|
||||
use core_types::color::{Alpha, SRGBA8};
|
||||
use core_types::list::List;
|
||||
use core_types::Color;
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::transform::Transform;
|
||||
use core_types::{ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
|
||||
use dyn_any::DynAny;
|
||||
use glam::DAffine2;
|
||||
use glam::DVec2;
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
/// Describes the fill of a layer.
|
||||
/// Describes an editable fill choice, storing color or gradient stops without gradient placement metadata.
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Fill {
|
||||
#[default]
|
||||
None,
|
||||
Solid(Color),
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Fill {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "None"),
|
||||
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", SRGBA8::from(*color).to_rgb_hex(), color.a() * 100.),
|
||||
Self::Gradient(gradient) => write!(f, "{gradient}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Construct a new [Fill::Solid] from a [Color].
|
||||
pub fn solid(color: Color) -> Self {
|
||||
Self::Solid(color)
|
||||
}
|
||||
|
||||
/// Construct a new [Fill::Solid] or [Fill::None] from an optional [Color].
|
||||
pub fn solid_or_none(color: Option<Color>) -> Self {
|
||||
match color {
|
||||
Some(color) => Self::Solid(color),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||
Self::Gradient(Gradient { stops, .. }) => {
|
||||
if stops.is_empty() {
|
||||
Color::BLACK
|
||||
} else {
|
||||
stops.color[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a gradient from the fill
|
||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||
match self {
|
||||
Self::Gradient(gradient) => Some(gradient),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a solid color from the fill
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
match self {
|
||||
Self::Solid(color) => Some(*color),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find if fill can be represented with only opaque colors
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.color.iter().all(|color| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if fill is none
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
fn from(color: Color) -> Fill {
|
||||
Fill::Solid(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Color>> for Fill {
|
||||
fn from(color: Option<Color>) -> Fill {
|
||||
Fill::solid_or_none(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<List<Color>> for Fill {
|
||||
fn from(color: List<Color>) -> Fill {
|
||||
let alpha: f64 = color.attribute_cloned_or(ATTR_OPACITY, 0, 1.);
|
||||
let color = color.element(0).copied();
|
||||
Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha as f32)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<List<GradientStops>> for Fill {
|
||||
fn from(gradient: List<GradientStops>) -> Fill {
|
||||
let gradient_type = gradient.attribute_cloned_or_default::<GradientType>(ATTR_GRADIENT_TYPE, 0);
|
||||
let spread_method = gradient.attribute_cloned_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD, 0);
|
||||
let transform = gradient.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
|
||||
|
||||
Fill::Gradient(Gradient {
|
||||
stops: gradient.element(0).cloned().unwrap_or_default(),
|
||||
gradient_type,
|
||||
spread_method,
|
||||
start: transform.transform_point2(DVec2::ZERO),
|
||||
end: transform.transform_point2(DVec2::X),
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
absolute: true,
|
||||
transform: DAffine2::IDENTITY,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Fill {
|
||||
fn from(gradient: Gradient) -> Fill {
|
||||
Fill::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
/// Can be None, a solid [Color], or a linear/radial [GradientStops].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill.
|
||||
///
|
||||
@@ -238,30 +104,6 @@ impl FillChoice {
|
||||
Self::Gradient(stops) => Some(stops.to_css_linear_gradient()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
|
||||
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
|
||||
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
|
||||
match self {
|
||||
Self::None => Fill::None,
|
||||
Self::Solid(color) => Fill::Solid(*color),
|
||||
Self::Gradient(stops) => {
|
||||
let mut fill = existing_gradient.cloned().unwrap_or_default();
|
||||
fill.stops = stops.clone();
|
||||
Fill::Gradient(fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fill> for FillChoice {
|
||||
fn from(fill: Fill) -> Self {
|
||||
match fill {
|
||||
Fill::None => FillChoice::None,
|
||||
Fill::Solid(color) => FillChoice::Solid(color),
|
||||
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
@@ -365,9 +207,6 @@ fn daffine2_identity() -> DAffine2 {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Stroke {
|
||||
/// Deprecated, use `ATTR_STROKE` instead.
|
||||
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
|
||||
pub color: Option<Color>,
|
||||
/// Line thickness
|
||||
pub weight: f64,
|
||||
pub dash_lengths: Vec<f64>,
|
||||
@@ -387,9 +226,8 @@ pub struct Stroke {
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub const fn new(color: Option<Color>, weight: f64) -> Self {
|
||||
pub const fn new(weight: f64) -> Self {
|
||||
Self {
|
||||
color,
|
||||
weight,
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
@@ -404,7 +242,6 @@ impl Stroke {
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
Self {
|
||||
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),
|
||||
weight: self.weight + (other.weight - self.weight) * time,
|
||||
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
|
||||
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
|
||||
@@ -440,11 +277,6 @@ impl Stroke {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current stroke color.
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.color
|
||||
}
|
||||
|
||||
/// Get the current stroke weight.
|
||||
pub fn weight(&self) -> f64 {
|
||||
self.weight
|
||||
@@ -510,12 +342,6 @@ impl Stroke {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
self.color = *color;
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
@@ -564,12 +390,10 @@ impl Stroke {
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
impl Default for Stroke {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
weight: 0.,
|
||||
color: Some(Color::BLACK),
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
cap: StrokeCap::Butt,
|
||||
@@ -582,129 +406,6 @@ impl Default for Stroke {
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct PathStyle {
|
||||
pub stroke: Option<Stroke>,
|
||||
pub fill: Fill,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let fill = &self.fill;
|
||||
|
||||
let stroke = match &self.stroke {
|
||||
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| SRGBA8::from(c).to_rgba_hex()), stroke.weight),
|
||||
None => "None".to_string(),
|
||||
};
|
||||
|
||||
write!(f, "Fill: {fill}\nStroke: {stroke}")
|
||||
}
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
|
||||
Self { stroke, fill }
|
||||
}
|
||||
|
||||
/// Get the current path's [Fill].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// let style = PathStyle::new(None, fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn fill(&self) -> &Fill {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
pub fn fill_mut(&mut self) -> &mut Fill {
|
||||
&mut self.fill
|
||||
}
|
||||
|
||||
/// Get the current path's [Stroke].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke.clone()
|
||||
}
|
||||
|
||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||
if let Some(stroke) = &mut self.stroke {
|
||||
stroke.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the path's [Stroke] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(style.stroke(), None);
|
||||
///
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// style.set_stroke(stroke.clone());
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn set_stroke(&mut self, stroke: Stroke) {
|
||||
self.stroke = Some(stroke);
|
||||
}
|
||||
|
||||
/// Set the path's fill to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
|
||||
///
|
||||
/// assert_ne!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// style.clear_fill();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
/// ```
|
||||
pub fn clear_fill(&mut self) {
|
||||
self.fill = Fill::None;
|
||||
}
|
||||
|
||||
/// Set the path's stroke to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
|
||||
///
|
||||
/// assert!(style.stroke().is_some());
|
||||
///
|
||||
/// style.clear_stroke();
|
||||
///
|
||||
/// assert!(!style.stroke().is_some());
|
||||
/// ```
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ways the user can choose to view the artwork in the viewport.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny)]
|
||||
@@ -720,39 +421,3 @@ pub enum RenderMode {
|
||||
/// Render a preview of how the object would be exported as an SVG.
|
||||
SvgPreview,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fill_from_gradient_list_preserves_attributes() {
|
||||
let mut list = List::new_from_element(GradientStops::default());
|
||||
list.set_attribute(ATTR_GRADIENT_TYPE, 0, GradientType::Radial);
|
||||
list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Reflect);
|
||||
list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 7.)));
|
||||
|
||||
let Fill::Gradient(gradient) = Fill::from(list) else {
|
||||
panic!("expected Fill::Gradient");
|
||||
};
|
||||
|
||||
assert_eq!(gradient.gradient_type, GradientType::Radial);
|
||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
||||
assert_eq!(gradient.start, DVec2::new(5., 7.));
|
||||
assert_eq!(gradient.end, DVec2::new(6., 7.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_from_empty_gradient_list_uses_defaults() {
|
||||
let list = List::new_from_element(GradientStops::default());
|
||||
|
||||
let Fill::Gradient(gradient) = Fill::from(list) else {
|
||||
panic!("expected Fill::Gradient");
|
||||
};
|
||||
|
||||
assert_eq!(gradient.gradient_type, GradientType::default());
|
||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::default());
|
||||
assert_eq!(gradient.start, DVec2::ZERO);
|
||||
assert_eq!(gradient.end, DVec2::X);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use super::misc::dvec2_to_point;
|
||||
use super::style::{PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
pub use super::vector_attributes::*;
|
||||
use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use crate::vector::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::vector::misc::{HandleId, ManipulatorPointId};
|
||||
use crate::vector::vector_modification::VectorExt;
|
||||
use core::borrow::Borrow;
|
||||
use core_types::Color;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::transform::Transform;
|
||||
@@ -19,7 +18,7 @@ use std::collections::HashMap;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Vector {
|
||||
pub style: PathStyle,
|
||||
pub stroke: Option<Stroke>,
|
||||
|
||||
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another).
|
||||
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it).
|
||||
@@ -36,7 +35,7 @@ unsafe impl StaticType for Vector {
|
||||
impl Default for Vector {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
|
||||
stroke: Some(Stroke::new(0.)),
|
||||
colinear_manipulators: Vec::new(),
|
||||
point_domain: PointDomain::new(),
|
||||
segment_domain: SegmentDomain::new(),
|
||||
@@ -50,7 +49,7 @@ impl graphene_hash::CacheHash for Vector {
|
||||
self.point_domain.cache_hash(state);
|
||||
self.segment_domain.cache_hash(state);
|
||||
self.region_domain.cache_hash(state);
|
||||
self.style.cache_hash(state);
|
||||
self.stroke.cache_hash(state);
|
||||
self.colinear_manipulators.cache_hash(state);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +242,7 @@ impl Vector {
|
||||
pub fn stroke_inclusive_bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
let path_bounds = self.bounding_box_with_transform(transform);
|
||||
|
||||
let Some(stroke) = self.style.stroke() else { return path_bounds };
|
||||
let Some(stroke) = self.stroke.as_ref() else { return path_bounds };
|
||||
// Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall
|
||||
// back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync.
|
||||
let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezier_paths().all(|p| p.closed());
|
||||
@@ -532,10 +531,16 @@ impl Vector {
|
||||
self.region_domain.concat(&additional.region_domain, transform_of_additional, &id_map);
|
||||
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = additional.style.clone();
|
||||
self.stroke = additional.stroke.clone();
|
||||
|
||||
self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied());
|
||||
}
|
||||
|
||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||
if let Some(stroke) = &mut self.stroke {
|
||||
stroke.transform = transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Vector {
|
||||
@@ -549,8 +554,9 @@ impl BoundingBox for Vector {
|
||||
}
|
||||
|
||||
// Include stroke by adding offset based on stroke width
|
||||
let stroke_width = self.style.stroke().map(|s| s.weight()).unwrap_or_default();
|
||||
let miter_limit = self.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
let stroke = self.stroke.clone();
|
||||
let stroke_width = stroke.as_ref().map(|s| s.weight()).unwrap_or_default();
|
||||
let miter_limit = stroke.as_ref().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
let scale = transform.scale_magnitudes();
|
||||
|
||||
// Use the full line width to account for different styles of stroke caps
|
||||
|
||||
@@ -16,7 +16,6 @@ use linesweeper::{BinaryOp, FillRule, binary_op};
|
||||
use smallvec::SmallVec;
|
||||
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
|
||||
pub use vector_types::vector::misc::BooleanOperation;
|
||||
use vector_types::vector::style::Fill;
|
||||
|
||||
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
|
||||
// TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
|
||||
@@ -50,7 +49,7 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
||||
|
||||
let result_vector = result_vector_list.element_mut(0).unwrap();
|
||||
Vector::transform(result_vector, transform);
|
||||
result_vector.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result_vector.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
|
||||
// for editor click-target preservation.
|
||||
@@ -148,16 +147,10 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
|
||||
bake_paint_transforms(&mut attributes, copy_from_transform);
|
||||
|
||||
let copy_from = vector.element(index).unwrap();
|
||||
let mut element = Vector {
|
||||
style: copy_from.style.clone(),
|
||||
let element = Vector {
|
||||
stroke: copy_from.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
// Legacy Fill fallback: An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points
|
||||
if let Fill::Gradient(gradient) = element.style.fill_mut()
|
||||
&& gradient.absolute
|
||||
{
|
||||
gradient.transform = copy_from_transform * gradient.transform;
|
||||
}
|
||||
Item::from_parts(element, attributes)
|
||||
} else {
|
||||
Item::<Vector>::default()
|
||||
@@ -288,7 +281,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
||||
set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color));
|
||||
|
||||
let mut element = Vector::default();
|
||||
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
Item::from_parts(element, attributes)
|
||||
})
|
||||
@@ -311,7 +304,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
||||
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
|
||||
|
||||
let mut element = Vector::default();
|
||||
element.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
Item::from_parts(element, attributes)
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ use core_types::{
|
||||
};
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphic_types::Vector;
|
||||
use graphic_types::graphic::{bake_paint_transforms, fill_graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, set_paint_attribute_at, stroke_graphic_list_at};
|
||||
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
|
||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||
use graphic_types::{Graphic, IntoGraphicList};
|
||||
use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
|
||||
@@ -143,7 +143,7 @@ where
|
||||
if fill {
|
||||
set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
|
||||
}
|
||||
if stroke && vector_list.element(index).and_then(|vector| vector.style.stroke()).is_some() {
|
||||
if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) {
|
||||
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
|
||||
}
|
||||
|
||||
@@ -328,8 +328,6 @@ where
|
||||
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
|
||||
|
||||
let stroke = Stroke {
|
||||
// TODO: Remove once the deprecated `Stroke.color` field is deleted in favor of the `ATTR_STROKE` attribute
|
||||
color: None,
|
||||
weight,
|
||||
dash_lengths,
|
||||
dash_offset,
|
||||
@@ -344,7 +342,7 @@ where
|
||||
content.for_each_vector_mut(|vector, transform| {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= transform;
|
||||
vector.style.set_stroke(stroke);
|
||||
vector.stroke = Some(stroke);
|
||||
});
|
||||
|
||||
let paint = paint.into_graphic_list();
|
||||
@@ -471,7 +469,7 @@ async fn round_corners(
|
||||
let edge_length_limit = edge_length_limit * 0.005;
|
||||
|
||||
let mut result = Vector {
|
||||
style: source.style.clone(),
|
||||
stroke: source.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -850,7 +848,7 @@ async fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<
|
||||
});
|
||||
}
|
||||
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Add this to the `List` and reset the transform since we've applied it directly to the points
|
||||
*row.element_mut() = result;
|
||||
@@ -1012,7 +1010,7 @@ async fn auto_tangents(
|
||||
let source = source.element(index).unwrap();
|
||||
|
||||
let mut result = Vector {
|
||||
style: source.style.clone(),
|
||||
stroke: source.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1162,8 +1160,8 @@ async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
result.style = vector.style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result.stroke = vector.stroke.clone();
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
*row.element_mut() = result;
|
||||
row
|
||||
@@ -1226,10 +1224,10 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
|
||||
|
||||
let bezpaths = vector.stroke_bezpath_iter();
|
||||
let mut result = Vector {
|
||||
style: vector.style.clone(),
|
||||
stroke: vector.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Perform operation on all subpaths in this shape.
|
||||
for mut bezpath in bezpaths {
|
||||
@@ -1266,10 +1264,8 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
|
||||
let graphic_list = content.into_graphic_list();
|
||||
let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
|
||||
|
||||
// A fill exists when the canonical attribute carries paint or, matching the renderer's fallback, when the legacy `style.fill` does
|
||||
let has_fills: Vec<bool> = (0..flattened.len())
|
||||
.map(|index| has_paint_at(&flattened, index, ATTR_FILL) || flattened.element(index).is_some_and(|vector| !vector.style.fill().is_none()))
|
||||
.collect();
|
||||
// A fill exists when the canonical attribute carries paint
|
||||
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint_at(&flattened, index, ATTR_FILL)).collect();
|
||||
|
||||
let mut output: List<Vector> = flattened
|
||||
.into_iter()
|
||||
@@ -1277,7 +1273,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
|
||||
.flat_map(|(row, has_fill)| {
|
||||
let (mut vector, attributes) = row.into_parts();
|
||||
|
||||
let stroke = vector.style.stroke().clone().unwrap_or_default();
|
||||
let stroke = vector.stroke.clone().unwrap_or_default();
|
||||
let bezpaths = vector.stroke_bezpath_iter();
|
||||
let mut solidified_stroke = Vector::default();
|
||||
|
||||
@@ -1326,7 +1322,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
|
||||
|
||||
// If the original vector has a fill, preserve it as a separate item with the stroke cleared.
|
||||
let fill_row = has_fill.then(|| {
|
||||
vector.style.clear_stroke();
|
||||
vector.stroke = None;
|
||||
let mut fill_attributes = attributes.clone();
|
||||
// No stroke remains on the fill row
|
||||
fill_attributes.remove::<List<Graphic>>(ATTR_STROKE);
|
||||
@@ -1338,13 +1334,6 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
|
||||
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
|
||||
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
|
||||
|
||||
// Fall back to the legacy stroke color when no canonical stroke paint attribute was carried over
|
||||
if !stroke_attributes.get::<List<Graphic>>(ATTR_FILL).is_some_and(is_paint_present)
|
||||
&& let Some(color) = stroke.color
|
||||
{
|
||||
set_paint_attribute(&mut stroke_attributes, ATTR_FILL, List::new_from_element(color));
|
||||
}
|
||||
|
||||
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
|
||||
|
||||
// Ordering based on the paint order. The first item in the `List` is rendered below the second.
|
||||
@@ -1389,7 +1378,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
|
||||
return vec![row];
|
||||
}
|
||||
|
||||
let style = row.element().style.clone();
|
||||
let stroke = row.element().stroke.clone();
|
||||
let (_, attributes) = row.into_parts();
|
||||
|
||||
bezpaths
|
||||
@@ -1397,7 +1386,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
|
||||
.map(|bezpath| {
|
||||
let mut vector = Vector::default();
|
||||
vector.append_bezpath(bezpath);
|
||||
vector.style = style.clone();
|
||||
vector.stroke = stroke.clone();
|
||||
|
||||
Item::from_parts(vector, attributes.clone())
|
||||
})
|
||||
@@ -1464,9 +1453,9 @@ pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(Lis
|
||||
let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
output.concat(element, source_transform, collision_hash_seed);
|
||||
|
||||
// TODO: Make this instead use the first encountered style
|
||||
// Use the last encountered style as the output style
|
||||
output.style = element.style.clone();
|
||||
// TODO: Make this instead use the first encountered stroke
|
||||
// Use the last encountered stroke as the output stroke
|
||||
output.stroke = element.stroke.clone();
|
||||
|
||||
primary_source = Some((index, source_transform));
|
||||
}
|
||||
@@ -1532,10 +1521,10 @@ async fn sample_polyline(
|
||||
segment_domain: Default::default(),
|
||||
region_domain: Default::default(),
|
||||
colinear_manipulators: Default::default(),
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
stroke: std::mem::take(&mut row.element_mut().stroke),
|
||||
};
|
||||
// Transfer the stroke transform from the input vector content to the result.
|
||||
result.style.set_stroke_transform(row.attribute_cloned_or_default(ATTR_TRANSFORM));
|
||||
result.set_stroke_transform(row.attribute_cloned_or_default(ATTR_TRANSFORM));
|
||||
|
||||
for local_bezpath in row.element().stroke_bezpath_iter() {
|
||||
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
|
||||
@@ -1606,7 +1595,7 @@ async fn simplify(
|
||||
let inverse_transform = transform.inverse();
|
||||
|
||||
let mut result = Vector {
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
stroke: std::mem::take(&mut row.element_mut().stroke),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1702,7 +1691,7 @@ async fn decimate(
|
||||
let inverse_transform = transform.inverse();
|
||||
|
||||
let mut result = Vector {
|
||||
style: std::mem::take(&mut row.element_mut().style),
|
||||
stroke: std::mem::take(&mut row.element_mut().stroke),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1781,7 +1770,7 @@ async fn cut_path(
|
||||
|
||||
if let Some((row_index, bezpath)) = bezpaths.get(index).cloned() {
|
||||
let mut result_vector = Vector {
|
||||
style: content.element(row_index).unwrap().style.clone(),
|
||||
stroke: content.element(row_index).unwrap().stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1991,8 +1980,8 @@ async fn scatter_points(
|
||||
}
|
||||
|
||||
// Transfer the style from the input vector content to the result.
|
||||
result.style = row.element().style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
result.stroke = row.element().stroke.clone();
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
*row.element_mut() = result;
|
||||
row
|
||||
@@ -2632,8 +2621,7 @@ async fn morph<I: IntoGraphicList>(
|
||||
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
|
||||
}
|
||||
|
||||
let mut vector = Vector::default();
|
||||
vector.style.stroke = match (source_element.style.stroke.as_ref(), target_element.style.stroke.as_ref()) {
|
||||
let stroke = match (source_element.stroke.as_ref(), target_element.stroke.as_ref()) {
|
||||
(Some(a), Some(b)) => Some(a.lerp(b, time)),
|
||||
(Some(a), None) => {
|
||||
if time < 0.5 {
|
||||
@@ -2651,15 +2639,16 @@ async fn morph<I: IntoGraphicList>(
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
let mut vector = Vector { stroke, ..Default::default() };
|
||||
|
||||
let fill_paint = {
|
||||
let source = fill_graphic_list_at(&content, source_index);
|
||||
let target = fill_graphic_list_at(&content, target_index);
|
||||
let source = graphic_list_at(&content, source_index, ATTR_FILL);
|
||||
let target = graphic_list_at(&content, target_index, ATTR_FILL);
|
||||
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
||||
};
|
||||
let stroke_paint = {
|
||||
let source = stroke_graphic_list_at(&content, source_index);
|
||||
let target = stroke_graphic_list_at(&content, target_index);
|
||||
let source = graphic_list_at(&content, source_index, ATTR_STROKE);
|
||||
let target = graphic_list_at(&content, target_index, ATTR_STROKE);
|
||||
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
||||
};
|
||||
|
||||
@@ -3457,7 +3446,7 @@ mod test {
|
||||
|
||||
let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await;
|
||||
|
||||
let fill = fill_graphic_list_at(&morphed, 0).expect("Morph should keep the fill paint at the midpoint");
|
||||
let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint");
|
||||
|
||||
// Interpolated color between red and blue should have >0 value on both R and B
|
||||
let Some(Graphic::Color(colors)) = fill.element(0) else {
|
||||
|
||||
Reference in New Issue
Block a user