mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-05 01:08:11 +08:00
Add rank-0 Item leaf variants to the Graphic enum alongside its List variants (#4437)
* Add the rank-0 Item leaf variants to the Graphic enum, rendering leaves and list rows through shared per-row logic * Traverse the rank-0 Graphic leaf variants in the vector node helpers and flattening * Reach rank-0 vector graphics from the styling and gradient-fitting helpers * Two code review fixes * Fix comment
This commit is contained in:
@@ -571,13 +571,33 @@ impl TableItemLayout for BoxCorners {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TableItemLayout for graphene_std::core_types::none::None {
|
||||||
|
fn type_name() -> &'static str {
|
||||||
|
"None"
|
||||||
|
}
|
||||||
|
fn identifier(&self) -> String {
|
||||||
|
"None".to_string()
|
||||||
|
}
|
||||||
|
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
|
label("None")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TableItemLayout for Graphic {
|
impl TableItemLayout for Graphic {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"Graphic"
|
"Graphic"
|
||||||
}
|
}
|
||||||
fn identifier(&self) -> String {
|
fn identifier(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::None => "None".to_string(),
|
Self::None(item) => item.identifier(),
|
||||||
|
Self::Graphic(item) => item.identifier(),
|
||||||
|
Self::Vector(item) => item.identifier(),
|
||||||
|
Self::RasterCPU(item) => item.identifier(),
|
||||||
|
Self::RasterGPU(item) => item.identifier(),
|
||||||
|
Self::Color(item) => item.identifier(),
|
||||||
|
Self::Gradient(item) => item.identifier(),
|
||||||
|
Self::Text(item) => item.identifier(),
|
||||||
|
Self::NoneList(list) => list.identifier(),
|
||||||
Self::GraphicList(list) => list.identifier(),
|
Self::GraphicList(list) => list.identifier(),
|
||||||
Self::VectorList(list) => list.identifier(),
|
Self::VectorList(list) => list.identifier(),
|
||||||
Self::RasterCPUList(list) => list.identifier(),
|
Self::RasterCPUList(list) => list.identifier(),
|
||||||
@@ -593,7 +613,15 @@ impl TableItemLayout for Graphic {
|
|||||||
}
|
}
|
||||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
match self {
|
match self {
|
||||||
Self::None => label("None"),
|
Self::None(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::Graphic(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::Vector(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::RasterCPU(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::RasterGPU(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::Color(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::Gradient(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::Text(item) => item.layout_with_breadcrumb(data),
|
||||||
|
Self::NoneList(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::GraphicList(list) => list.layout_with_breadcrumb(data),
|
Self::GraphicList(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::VectorList(list) => list.layout_with_breadcrumb(data),
|
Self::VectorList(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::RasterCPUList(list) => list.layout_with_breadcrumb(data),
|
Self::RasterCPUList(list) => list.layout_with_breadcrumb(data),
|
||||||
|
|||||||
@@ -2760,7 +2760,7 @@ impl DocumentMessageHandler {
|
|||||||
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
||||||
let has_stroke = appearance.is_some_and(|appearance| {
|
let has_stroke = appearance.is_some_and(|appearance| {
|
||||||
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
||||||
&& appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_fully_transparent())
|
&& appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_guaranteed_fully_transparent())
|
||||||
});
|
});
|
||||||
|
|
||||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
||||||
@@ -4326,7 +4326,7 @@ mod document_message_handler_tests {
|
|||||||
|
|
||||||
let instrumented = editor.eval_graph().await.unwrap();
|
let instrumented = editor.eval_graph().await.unwrap();
|
||||||
|
|
||||||
// The emptiness guards keep these assertions honest: a wrong `Output` type on `grab_all_input` yields no records at all, which would otherwise pass vacuously
|
// The emptiness guards keep these assertions honest: a wrong `Output` type on `grab_all_input` yields no records at all, which would otherwise pass without checking anything
|
||||||
let base_lengths: Vec<usize> = instrumented
|
let base_lengths: Vec<usize> = instrumented
|
||||||
.grab_all_input::<graphene_std::graphic::extend::BaseInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
.grab_all_input::<graphene_std::graphic::extend::BaseInput, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||||
.map(|base| base.len())
|
.map(|base| base.len())
|
||||||
@@ -4341,7 +4341,7 @@ mod document_message_handler_tests {
|
|||||||
let phantom_count = news
|
let phantom_count = news
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|new| new.iter_element_values())
|
.flat_map(|new| new.iter_element_values())
|
||||||
.filter(|graphic| matches!(graphic, graphene_std::Graphic::None))
|
.filter(|graphic| matches!(graphic, graphene_std::Graphic::None(_)))
|
||||||
.count();
|
.count();
|
||||||
assert_eq!(phantom_count, 0, "No stacked element should be a phantom None graphic");
|
assert_eq!(phantom_count, 0, "No stacked element should be a phantom None graphic");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod list;
|
|||||||
pub mod math;
|
pub mod math;
|
||||||
pub mod memo;
|
pub mod memo;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
|
pub mod none;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod render_complexity;
|
pub mod render_complexity;
|
||||||
|
|||||||
@@ -1331,6 +1331,19 @@ pub struct Item<T> {
|
|||||||
attributes: ItemAttributeValues,
|
attributes: ItemAttributeValues,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: BoundingBox> BoundingBox for Item<T> {
|
||||||
|
/// Computes the element's bounding box, composing the item's transform attribute with the given transform.
|
||||||
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
|
self.element().bounding_box(transform * item_transform, include_stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
|
self.element().thumbnail_bounding_box(transform * item_transform, include_stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Default> Default for Item<T> {
|
impl<T: Default> Default for Item<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new_from_element(T::default())
|
Self::new_from_element(T::default())
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use dyn_any::DynAny;
|
||||||
|
use graphene_hash::CacheHash;
|
||||||
|
|
||||||
|
/// An artist's declaration that there is no content here, distinct from the `()` type's "nothing was wired".
|
||||||
|
/// Visually represented as a red slash over a white background. Akin to the CSS `none` keyword.
|
||||||
|
///
|
||||||
|
/// Because its name matches the Rust prelude's `Option::None` variant, we always reference this as `none::None`.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, CacheHash, DynAny)]
|
||||||
|
pub struct None;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Raster types moved to raster-types crate
|
// Raster types moved to raster-types crate
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use crate::list::List;
|
use crate::list::{Item, List};
|
||||||
|
|
||||||
pub trait RenderComplexity {
|
pub trait RenderComplexity {
|
||||||
fn render_complexity(&self) -> usize {
|
fn render_complexity(&self) -> usize {
|
||||||
@@ -8,6 +8,12 @@ pub trait RenderComplexity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: RenderComplexity> RenderComplexity for Item<T> {
|
||||||
|
fn render_complexity(&self) -> usize {
|
||||||
|
self.element().render_complexity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: RenderComplexity> RenderComplexity for List<T> {
|
impl<T: RenderComplexity> RenderComplexity for List<T> {
|
||||||
fn render_complexity(&self) -> usize {
|
fn render_complexity(&self) -> usize {
|
||||||
self.iter_element_values().map(|element| element.render_complexity()).fold(0, usize::saturating_add)
|
self.iter_element_values().map(|element| element.render_complexity()).fold(0, usize::saturating_add)
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn painted_cover_distinguishes_none_paint_from_absence() {
|
fn painted_cover_distinguishes_none_paint_from_absence() {
|
||||||
let mut appearance = Appearance::default();
|
let mut appearance = Appearance::default();
|
||||||
appearance.replace_or_insert(Coverage::new_fill(), Graphic::None, CoverPlacement::Above);
|
appearance.replace_or_insert(Coverage::new_fill(), Graphic::default(), CoverPlacement::Above);
|
||||||
|
|
||||||
assert!(appearance.has_cover(Cover::Fill), "a none-painted coverage still exists");
|
assert!(appearance.has_cover(Cover::Fill), "a none-painted coverage still exists");
|
||||||
assert!(!appearance.has_painted_cover(Cover::Fill), "a none-painted coverage draws nothing");
|
assert!(!appearance.has_painted_cover(Cover::Fill), "a none-painted coverage draws nothing");
|
||||||
|
|||||||
@@ -3,22 +3,30 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
|||||||
use core_types::graphene_hash::CacheHash;
|
use core_types::graphene_hash::CacheHash;
|
||||||
use core_types::list::{ATTR_APPEARANCE, ATTR_PAINT, Item, ItemAttributeValues, List, NodeIdPath};
|
use core_types::list::{ATTR_APPEARANCE, ATTR_PAINT, Item, ItemAttributeValues, List, NodeIdPath};
|
||||||
use core_types::math::quad::Quad;
|
use core_types::math::quad::Quad;
|
||||||
|
use core_types::none;
|
||||||
use core_types::ops::FromAnchorPosition;
|
use core_types::ops::FromAnchorPosition;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::transform::Transform;
|
use core_types::transform::Transform;
|
||||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_SPREAD, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use vector_types::Gradient;
|
|
||||||
pub use vector_types::Vector;
|
pub use vector_types::Vector;
|
||||||
|
use vector_types::{Gradient, GradientSpread};
|
||||||
|
|
||||||
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
/// The possible forms of graphical content that can be rendered by the Render node (to targets like SVG and raster) or another render boundary node.
|
||||||
#[derive(Clone, Debug, Default, CacheHash, PartialEq, DynAny)]
|
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
|
||||||
pub enum Graphic {
|
pub enum Graphic {
|
||||||
/// The absence of graphical content, like CSS's `none` keyword: painting it produces nothing.
|
/// No content, akin to CSS `none`, represented visually by a red slash.
|
||||||
#[default]
|
None(Item<none::None>),
|
||||||
None,
|
Graphic(Box<Item<Graphic>>),
|
||||||
|
Vector(Box<Item<Vector>>),
|
||||||
|
RasterCPU(Box<Item<Raster<CPU>>>),
|
||||||
|
RasterGPU(Item<Raster<GPU>>),
|
||||||
|
Color(Item<Color>),
|
||||||
|
Gradient(Item<Gradient>),
|
||||||
|
Text(Item<String>),
|
||||||
|
NoneList(List<none::None>),
|
||||||
GraphicList(List<Graphic>),
|
GraphicList(List<Graphic>),
|
||||||
VectorList(List<Vector>),
|
VectorList(List<Vector>),
|
||||||
RasterCPUList(List<Raster<CPU>>),
|
RasterCPUList(List<Raster<CPU>>),
|
||||||
@@ -28,6 +36,12 @@ pub enum Graphic {
|
|||||||
TextList(List<String>),
|
TextList(List<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Graphic {
|
||||||
|
fn default() -> Self {
|
||||||
|
Graphic::None(Item::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GraphicList
|
// GraphicList
|
||||||
impl From<List<Graphic>> for Graphic {
|
impl From<List<Graphic>> for Graphic {
|
||||||
fn from(graphic: List<Graphic>) -> Self {
|
fn from(graphic: List<Graphic>) -> Self {
|
||||||
@@ -117,7 +131,7 @@ impl From<List<String>> for Graphic {
|
|||||||
/// collapses no structure and rebuilding or snapshotting the result would be busywork.
|
/// collapses no structure and rebuilding or snapshotting the result would be busywork.
|
||||||
pub fn is_lone_anonymous_leaf(content: &List<Graphic>) -> bool {
|
pub fn is_lone_anonymous_leaf(content: &List<Graphic>) -> bool {
|
||||||
content.len() == 1
|
content.len() == 1
|
||||||
&& !matches!(content.element(0), Some(Graphic::GraphicList(_)))
|
&& !matches!(content.element(0), Some(Graphic::Graphic(_)) | Some(Graphic::GraphicList(_)))
|
||||||
&& content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).is_none()
|
&& content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).is_none()
|
||||||
&& content.attribute::<f64>(ATTR_OPACITY, 0).is_none()
|
&& content.attribute::<f64>(ATTR_OPACITY, 0).is_none()
|
||||||
&& content.attribute::<f64>(ATTR_OPACITY_FILL, 0).is_none()
|
&& content.attribute::<f64>(ATTR_OPACITY_FILL, 0).is_none()
|
||||||
@@ -137,9 +151,9 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
|
|
||||||
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) {
|
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) {
|
||||||
for current_graphic_item in current_graphic_list.into_iter() {
|
for current_graphic_item in current_graphic_list.into_iter() {
|
||||||
// Whether the parent carries each composed attribute: a structural fact (column presence), never a value comparison.
|
// Whether the parent carries each composed attribute: a structural fact (attribute presence), never a value comparison.
|
||||||
// Flattening composes a parent attribute onto its children only when the parent has it,
|
// Flattening composes a parent attribute onto its children only when the parent has it,
|
||||||
// so an absent parent attribute never invents a column the children didn't already have.
|
// so an absent parent attribute never invents an attribute the children didn't already have.
|
||||||
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
||||||
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
||||||
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
||||||
@@ -151,12 +165,18 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
|
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
|
||||||
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
|
||||||
|
|
||||||
match current_graphic_item.into_element() {
|
// A boxed single graphic is the rank-0 spelling of the same nesting, so it flattens through the list path
|
||||||
|
let current_element = match current_graphic_item.into_element() {
|
||||||
|
Graphic::Graphic(item) => Graphic::GraphicList(List::new_from_item(*item)),
|
||||||
|
element => element,
|
||||||
|
};
|
||||||
|
|
||||||
|
match current_element {
|
||||||
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
|
// Compose the parent's transform/opacity/fill onto each child, but only for attributes the parent carries.
|
||||||
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
|
// A child lacking one is padded with the composition identity (`1.` for opacity/fill, identity for transform), so composing through it is a no-op.
|
||||||
Graphic::GraphicList(mut sub_list) => {
|
Graphic::GraphicList(mut sub_list) => {
|
||||||
// A group's first child has no preceding sibling, so its clipping flag is inert until splicing
|
// A group's first child has no preceding sibling, so its clipping flag is inert until splicing
|
||||||
// hands it the group's own predecessor. Clear it (keeping the column) to stay clip-neutral.
|
// hands it the group's own predecessor. Clear it (keeping the attribute) to stay clip-neutral.
|
||||||
if sub_list.attribute::<bool>(ATTR_CLIPPING_MASK, 0).is_some() {
|
if sub_list.attribute::<bool>(ATTR_CLIPPING_MASK, 0).is_some() {
|
||||||
sub_list.set_attribute(ATTR_CLIPPING_MASK, 0, false);
|
sub_list.set_attribute(ATTR_CLIPPING_MASK, 0, false);
|
||||||
}
|
}
|
||||||
@@ -235,6 +255,11 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
|
|||||||
|
|
||||||
/// Bake the provided transform into the per-item transforms of the appearance's paint graphics.
|
/// Bake the provided transform into the per-item transforms of the appearance's paint graphics.
|
||||||
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
|
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
|
||||||
|
fn bake_item_transform<T>(item: &mut Item<T>, transform: DAffine2) {
|
||||||
|
let baked = transform * item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
item.set_attribute(ATTR_TRANSFORM, baked);
|
||||||
|
}
|
||||||
|
|
||||||
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
|
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
|
||||||
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
*item_transform = transform * *item_transform;
|
*item_transform = transform * *item_transform;
|
||||||
@@ -243,14 +268,20 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
|
|||||||
|
|
||||||
fn bake_graphic_transform(graphic: &mut Graphic, transform: DAffine2) {
|
fn bake_graphic_transform(graphic: &mut Graphic, transform: DAffine2) {
|
||||||
match graphic {
|
match graphic {
|
||||||
Graphic::None => {}
|
Graphic::Graphic(item) => bake_item_transform(item, transform),
|
||||||
|
Graphic::Vector(item) => bake_item_transform(item, transform),
|
||||||
|
Graphic::RasterCPU(item) => bake_item_transform(item, transform),
|
||||||
|
Graphic::RasterGPU(item) => bake_item_transform(item, transform),
|
||||||
|
Graphic::Gradient(item) => bake_item_transform(item, transform),
|
||||||
|
Graphic::Text(item) => bake_item_transform(item, transform),
|
||||||
Graphic::GraphicList(list) => bake_list_transform(list, transform),
|
Graphic::GraphicList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::VectorList(list) => bake_list_transform(list, transform),
|
Graphic::VectorList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::RasterCPUList(list) => bake_list_transform(list, transform),
|
Graphic::RasterCPUList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::RasterGPUList(list) => bake_list_transform(list, transform),
|
Graphic::RasterGPUList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::GradientList(list) => bake_list_transform(list, transform),
|
Graphic::GradientList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::TextList(list) => bake_list_transform(list, transform),
|
Graphic::TextList(list) => bake_list_transform(list, transform),
|
||||||
Graphic::ColorList(_) => {}
|
// A color has no spatial extent, so there is no placement for a transform to move
|
||||||
|
Graphic::None(_) | Graphic::NoneList(_) | Graphic::Color(_) | Graphic::ColorList(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,31 +302,51 @@ pub trait TryFromGraphic: Clone + Sized {
|
|||||||
|
|
||||||
impl TryFromGraphic for Vector {
|
impl TryFromGraphic for Vector {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::VectorList(t) = graphic { Some(t) } else { None }
|
match graphic {
|
||||||
|
Graphic::Vector(item) => Some(List::new_from_item(*item)),
|
||||||
|
Graphic::VectorList(list) => Some(list),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for Raster<CPU> {
|
impl TryFromGraphic for Raster<CPU> {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::RasterCPUList(t) = graphic { Some(t) } else { None }
|
match graphic {
|
||||||
|
Graphic::RasterCPU(item) => Some(List::new_from_item(*item)),
|
||||||
|
Graphic::RasterCPUList(list) => Some(list),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for Color {
|
impl TryFromGraphic for Color {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::ColorList(t) = graphic { Some(t) } else { None }
|
match graphic {
|
||||||
|
Graphic::Color(item) => Some(List::new_from_item(item)),
|
||||||
|
Graphic::ColorList(list) => Some(list),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for Gradient {
|
impl TryFromGraphic for Gradient {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::GradientList(t) = graphic { Some(t) } else { None }
|
match graphic {
|
||||||
|
Graphic::Gradient(item) => Some(List::new_from_item(item)),
|
||||||
|
Graphic::GradientList(list) => Some(list),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFromGraphic for String {
|
impl TryFromGraphic for String {
|
||||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||||
if let Graphic::TextList(t) = graphic { Some(t) } else { None }
|
match graphic {
|
||||||
|
Graphic::Text(item) => Some(List::new_from_item(item)),
|
||||||
|
Graphic::TextList(list) => Some(list),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,11 +473,23 @@ impl Graphic {
|
|||||||
|
|
||||||
pub fn had_clip_enabled(&self) -> bool {
|
pub fn had_clip_enabled(&self) -> bool {
|
||||||
fn all_clipped<T>(list: &List<T>) -> bool {
|
fn all_clipped<T>(list: &List<T>) -> bool {
|
||||||
list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
|
!list.is_empty() && list.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_clipped<T>(item: &Item<T>) -> bool {
|
||||||
|
item.attribute_cloned_or_default::<bool>(ATTR_CLIPPING_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => true,
|
Graphic::None(item) => item_clipped(item),
|
||||||
|
Graphic::Graphic(item) => item_clipped(item),
|
||||||
|
Graphic::Vector(item) => item_clipped(item),
|
||||||
|
Graphic::RasterCPU(item) => item_clipped(item),
|
||||||
|
Graphic::RasterGPU(item) => item_clipped(item),
|
||||||
|
Graphic::Color(item) => item_clipped(item),
|
||||||
|
Graphic::Gradient(item) => item_clipped(item),
|
||||||
|
Graphic::Text(item) => item_clipped(item),
|
||||||
|
Graphic::NoneList(list) => all_clipped(list),
|
||||||
Graphic::VectorList(list) => all_clipped(list),
|
Graphic::VectorList(list) => all_clipped(list),
|
||||||
Graphic::GraphicList(list) => all_clipped(list),
|
Graphic::GraphicList(list) => all_clipped(list),
|
||||||
Graphic::RasterCPUList(list) => all_clipped(list),
|
Graphic::RasterCPUList(list) => all_clipped(list),
|
||||||
@@ -439,109 +502,92 @@ impl Graphic {
|
|||||||
|
|
||||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Graphic::VectorList(vector) => (0..vector.len()).all(|index| {
|
Graphic::Vector(item) => vector_can_reduce_to_clip_path(item.attribute_cloned_or(ATTR_OPACITY, 1.), item.attribute::<Appearance>(ATTR_APPEARANCE)),
|
||||||
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
Graphic::VectorList(list) => {
|
||||||
let appearance = vector.attribute::<Appearance>(ATTR_APPEARANCE, index);
|
(0..list.len()).all(|index| vector_can_reduce_to_clip_path(list.attribute_cloned_or(ATTR_OPACITY, index, 1.), list.attribute::<Appearance>(ATTR_APPEARANCE, index)))
|
||||||
|
}
|
||||||
let fills_opaque_or_absent = appearance.is_none_or(|appearance| {
|
|
||||||
appearance
|
|
||||||
.covers_with_paints()
|
|
||||||
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
|
||||||
.all(|(_, paint)| paint.is_none_or(Graphic::is_opaque))
|
|
||||||
});
|
|
||||||
|
|
||||||
let strokes_invisible_or_transparent = appearance.is_none_or(|appearance| {
|
|
||||||
appearance
|
|
||||||
.covers_with_paints()
|
|
||||||
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
|
||||||
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_fully_transparent))
|
|
||||||
});
|
|
||||||
|
|
||||||
opacity > 1. - f64::EPSILON && fills_opaque_or_absent && strokes_invisible_or_transparent
|
|
||||||
}),
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_opaque(&self) -> bool {
|
pub fn is_guaranteed_fully_opaque(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => false,
|
Graphic::None(_) | Graphic::NoneList(_) => false,
|
||||||
Graphic::GraphicList(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
// The group's own opacity scales whatever it wraps, so full alpha there is a precondition
|
||||||
|
Graphic::Graphic(item) => item_opacity_is_full(item) && item.element().is_guaranteed_fully_opaque(),
|
||||||
|
Graphic::GraphicList(list) => !list.is_empty() && every_item_has_full_opacity(list) && list.iter_element_values().all(Graphic::is_guaranteed_fully_opaque),
|
||||||
|
Graphic::Vector(item) => vector_is_guaranteed_fully_opaque(
|
||||||
|
item.attribute_cloned_or(ATTR_OPACITY, 1.),
|
||||||
|
item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.),
|
||||||
|
item.attribute::<Appearance>(ATTR_APPEARANCE),
|
||||||
|
),
|
||||||
Graphic::VectorList(list) => {
|
Graphic::VectorList(list) => {
|
||||||
!list.is_empty()
|
!list.is_empty()
|
||||||
&& (0..list.len()).all(|i| {
|
&& (0..list.len()).all(|index| {
|
||||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
vector_is_guaranteed_fully_opaque(
|
||||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
list.attribute_cloned_or(ATTR_OPACITY, index, 1.),
|
||||||
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, i);
|
list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.),
|
||||||
|
list.attribute::<Appearance>(ATTR_APPEARANCE, index),
|
||||||
let fill_opaque = opacity_fill >= 1. - f64::EPSILON
|
)
|
||||||
&& appearance.is_some_and(|appearance| {
|
|
||||||
appearance
|
|
||||||
.covers_with_paints()
|
|
||||||
.any(|(coverage, paint)| coverage.cover() == Cover::Fill && paint.is_some_and(Graphic::is_opaque))
|
|
||||||
});
|
|
||||||
|
|
||||||
let strokes_opaque_or_invisible = appearance.is_none_or(|appearance| {
|
|
||||||
appearance
|
|
||||||
.covers_with_paints()
|
|
||||||
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
|
||||||
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_some_and(Graphic::is_opaque))
|
|
||||||
});
|
|
||||||
|
|
||||||
opacity >= 1. - f64::EPSILON && fill_opaque && strokes_opaque_or_invisible
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Graphic::RasterCPU(_) | Graphic::RasterCPUList(_) => false,
|
||||||
|
Graphic::RasterGPU(_) | Graphic::RasterGPUList(_) => false,
|
||||||
|
Graphic::Color(item) => item.element().is_opaque(),
|
||||||
Graphic::ColorList(list) => list.element(0).is_some_and(|color| color.is_opaque()),
|
Graphic::ColorList(list) => list.element(0).is_some_and(|color| color.is_opaque()),
|
||||||
Graphic::GradientList(list) => list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque())),
|
// A `Clear` spread cuts off to transparency past the ends, leaving the rest of the region unpainted
|
||||||
Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::TextList(_) => false,
|
Graphic::Gradient(item) => item.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD) != GradientSpread::Clear && item.element().iter().all(|stop| stop.color.is_opaque()),
|
||||||
|
Graphic::GradientList(list) => {
|
||||||
|
list.attribute_cloned_or_default::<GradientSpread>(ATTR_GRADIENT_SPREAD, 0) != GradientSpread::Clear
|
||||||
|
&& list.element(0).is_some_and(|stops| stops.iter().all(|stop| stop.color.is_opaque()))
|
||||||
|
}
|
||||||
|
Graphic::Text(_) | Graphic::TextList(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fully_transparent(&self) -> bool {
|
pub fn is_guaranteed_fully_transparent(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => true,
|
Graphic::None(_) | Graphic::NoneList(_) => true,
|
||||||
Graphic::GraphicList(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
Graphic::Graphic(item) => item_opacity_is_zero(item) || item.element().is_guaranteed_fully_transparent(),
|
||||||
Graphic::VectorList(list) => (0..list.len()).all(|i| {
|
Graphic::GraphicList(list) => every_item_has_zero_opacity(list) || list.iter_element_values().all(Graphic::is_guaranteed_fully_transparent),
|
||||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
Graphic::Vector(item) => vector_is_guaranteed_fully_transparent(
|
||||||
if opacity <= f64::EPSILON {
|
item.attribute_cloned_or(ATTR_OPACITY, 1.),
|
||||||
return true;
|
item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.),
|
||||||
}
|
item.attribute::<Appearance>(ATTR_APPEARANCE),
|
||||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
),
|
||||||
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, i);
|
Graphic::VectorList(list) => (0..list.len()).all(|index| {
|
||||||
|
vector_is_guaranteed_fully_transparent(
|
||||||
let fills_invisible = opacity_fill <= f64::EPSILON
|
list.attribute_cloned_or(ATTR_OPACITY, index, 1.),
|
||||||
|| appearance.is_none_or(|appearance| {
|
list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.),
|
||||||
appearance
|
list.attribute::<Appearance>(ATTR_APPEARANCE, index),
|
||||||
.covers_with_paints()
|
)
|
||||||
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
|
||||||
.all(|(_, paint)| paint.is_none_or(Graphic::is_fully_transparent))
|
|
||||||
});
|
|
||||||
|
|
||||||
let strokes_invisible = appearance.is_none_or(|appearance| {
|
|
||||||
appearance
|
|
||||||
.covers_with_paints()
|
|
||||||
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
|
||||||
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_fully_transparent))
|
|
||||||
});
|
|
||||||
|
|
||||||
fills_invisible && strokes_invisible
|
|
||||||
}),
|
}),
|
||||||
|
Graphic::Color(item) => item.element().a() == 0.,
|
||||||
Graphic::ColorList(list) => list.iter_element_values().all(|color| color.a() == 0.),
|
Graphic::ColorList(list) => list.iter_element_values().all(|color| color.a() == 0.),
|
||||||
Graphic::GradientList(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
|
// A stopless ramp paints as solid black, matching `Gradient::evaluate`, so it counts as transparent only once it has stops
|
||||||
Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::TextList(_) => false,
|
Graphic::Gradient(item) => !item.element().is_empty() && item.element().iter().all(|stop| stop.color.a() == 0.),
|
||||||
|
Graphic::GradientList(list) => list.iter_element_values().all(|stops| !stops.is_empty() && stops.iter().all(|stop| stop.color.a() == 0.)),
|
||||||
|
// Their content is never inspected, so zeroed opacity is the only invisibility these can report
|
||||||
|
Graphic::RasterCPU(item) => item_opacity_is_zero(item),
|
||||||
|
Graphic::RasterGPU(item) => item_opacity_is_zero(item),
|
||||||
|
Graphic::Text(item) => item_opacity_is_zero(item),
|
||||||
|
Graphic::RasterCPUList(list) => every_item_has_zero_opacity(list),
|
||||||
|
Graphic::RasterGPUList(list) => every_item_has_zero_opacity(list),
|
||||||
|
Graphic::TextList(list) => every_item_has_zero_opacity(list),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if this paint opaquely covers the entire fill region.
|
/// True if this paint fully, opaquely covers the entire fill region.
|
||||||
/// Vector, Raster, and a nested Graphic may leave gaps, so they return false.
|
pub fn is_guaranteed_to_cover_opaquely(&self) -> bool {
|
||||||
pub fn covers_opaquely(&self) -> bool {
|
matches!(self, Graphic::Color(_) | Graphic::Gradient(_) | Graphic::ColorList(_) | Graphic::GradientList(_)) && self.is_guaranteed_fully_opaque()
|
||||||
matches!(self, Graphic::ColorList(_) | Graphic::GradientList(_)) && self.is_opaque()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this graphic contains no content.
|
/// Returns true if this graphic contains no content.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => true,
|
// A leaf always holds exactly one element, so only the none-typed content is truly empty
|
||||||
|
Graphic::None(_) | Graphic::NoneList(_) => true,
|
||||||
|
Graphic::Graphic(_) | Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Color(_) | Graphic::Gradient(_) | Graphic::Text(_) => false,
|
||||||
Graphic::GraphicList(list) => list.is_empty(),
|
Graphic::GraphicList(list) => list.is_empty(),
|
||||||
Graphic::VectorList(list) => list.is_empty(),
|
Graphic::VectorList(list) => list.is_empty(),
|
||||||
Graphic::ColorList(list) => list.is_empty(),
|
Graphic::ColorList(list) => list.is_empty(),
|
||||||
@@ -553,29 +599,125 @@ impl Graphic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combined bounding box of a vector list's rows, inflating each row by its appearance's stroke when `include_stroke`.
|
/// Whether a vector object's own opacity and paint let a clipper reduce to an SVG `<clipPath>` instead of a `<mask>`.
|
||||||
/// Stroke parameters live on the row attribute, out of reach of the element-level impl.
|
fn vector_can_reduce_to_clip_path(opacity: f64, appearance: Option<&Appearance>) -> bool {
|
||||||
|
let fills_opaque_or_absent = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
||||||
|
.all(|(_, paint)| paint.is_none_or(Graphic::is_guaranteed_fully_opaque))
|
||||||
|
});
|
||||||
|
|
||||||
|
let strokes_invisible_or_transparent = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_guaranteed_fully_transparent))
|
||||||
|
});
|
||||||
|
|
||||||
|
opacity > 1. - f64::EPSILON && fills_opaque_or_absent && strokes_invisible_or_transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a vector object paints its whole interior at full alpha, so nothing behind it can show through.
|
||||||
|
fn vector_is_guaranteed_fully_opaque(opacity: f64, opacity_fill: f64, appearance: Option<&Appearance>) -> bool {
|
||||||
|
let fill_opaque = opacity_fill >= 1. - f64::EPSILON
|
||||||
|
&& appearance.is_some_and(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.any(|(coverage, paint)| coverage.cover() == Cover::Fill && paint.is_some_and(Graphic::is_guaranteed_fully_opaque))
|
||||||
|
});
|
||||||
|
|
||||||
|
let strokes_opaque_or_invisible = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_some_and(Graphic::is_guaranteed_fully_opaque))
|
||||||
|
});
|
||||||
|
|
||||||
|
opacity >= 1. - f64::EPSILON && fill_opaque && strokes_opaque_or_invisible
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a vector object draws nothing visible, either through its opacity or through its paint.
|
||||||
|
fn vector_is_guaranteed_fully_transparent(opacity: f64, opacity_fill: f64, appearance: Option<&Appearance>) -> bool {
|
||||||
|
if opacity <= f64::EPSILON {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fills_invisible = opacity_fill <= f64::EPSILON
|
||||||
|
|| appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
||||||
|
.all(|(_, paint)| paint.is_none_or(Graphic::is_guaranteed_fully_transparent))
|
||||||
|
});
|
||||||
|
|
||||||
|
let strokes_invisible = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(Graphic::is_guaranteed_fully_transparent))
|
||||||
|
});
|
||||||
|
|
||||||
|
fills_invisible && strokes_invisible
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a lone item's opacity zeroes it out, independent of what its element holds.
|
||||||
|
fn item_opacity_is_zero<T>(item: &Item<T>) -> bool {
|
||||||
|
item.attribute_cloned_or::<f64>(ATTR_OPACITY, 1.) <= f64::EPSILON
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every item of a list is zeroed out by its opacity, which an empty list satisfies with nothing to draw.
|
||||||
|
fn every_item_has_zero_opacity<T>(list: &List<T>) -> bool {
|
||||||
|
(0..list.len()).all(|index| list.attribute_cloned_or::<f64>(ATTR_OPACITY, index, 1.) <= f64::EPSILON)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a lone item passes its content through at full opacity, covering both factors the renderer multiplies together.
|
||||||
|
fn item_opacity_is_full<T>(item: &Item<T>) -> bool {
|
||||||
|
item.attribute_cloned_or::<f64>(ATTR_OPACITY, 1.) >= 1. - f64::EPSILON && item.attribute_cloned_or::<f64>(ATTR_OPACITY_FILL, 1.) >= 1. - f64::EPSILON
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether every item of a list passes its content through with full opacity.
|
||||||
|
fn every_item_has_full_opacity<T>(list: &List<T>) -> bool {
|
||||||
|
(0..list.len()).all(|index| list.attribute_cloned_or::<f64>(ATTR_OPACITY, index, 1.) >= 1. - f64::EPSILON && list.attribute_cloned_or::<f64>(ATTR_OPACITY_FILL, index, 1.) >= 1. - f64::EPSILON)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounding box of one vector, inflated by its appearance's stroke when `include_stroke` is true.
|
||||||
|
/// Stroke parameters live on the item attribute, out of reach of the element-level impl.
|
||||||
|
fn vector_bounding_box(vector: &Vector, composed_transform: DAffine2, appearance: Option<&Appearance>, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||||
|
let mut bounds = vector.bounding_box_with_transform(composed_transform)?;
|
||||||
|
|
||||||
|
// The full line width (not half) accounts for different styles of stroke caps
|
||||||
|
if include_stroke && let Some(stroke) = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map(Coverage::stroke_params) {
|
||||||
|
let scale = composed_transform.scale_magnitudes();
|
||||||
|
let offset = DVec2::splat(stroke.weight() * scale.x.max(scale.y) * stroke.join_miter_limit);
|
||||||
|
bounds = [bounds[0] - offset, bounds[1] + offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounding box of a lone vector, inflating it by its appearance's stroke when `include_stroke`.
|
||||||
|
pub fn vector_item_bounding_box(item: &Item<Vector>, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
let composed_transform = transform * item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
|
||||||
|
match vector_bounding_box(item.element(), composed_transform, item.attribute::<Appearance>(ATTR_APPEARANCE), include_stroke) {
|
||||||
|
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||||
|
None => RenderBoundingBox::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combined bounding box of a vector list's items, inflating each item by its appearance's stroke when `include_stroke`.
|
||||||
pub fn vector_list_bounding_box(list: &List<Vector>, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
pub fn vector_list_bounding_box(list: &List<Vector>, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
let mut combined_bounds: Option<[DVec2; 2]> = None;
|
let mut combined_bounds: Option<[DVec2; 2]> = None;
|
||||||
|
|
||||||
for index in 0..list.len() {
|
for index in 0..list.len() {
|
||||||
let Some(element) = list.element(index) else { continue };
|
let Some(element) = list.element(index) else { continue };
|
||||||
let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let row_transform = transform * item_transform;
|
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, index);
|
||||||
|
|
||||||
let Some(mut bounds) = element.bounding_box_with_transform(row_transform) else { continue };
|
let Some(bounds) = vector_bounding_box(element, transform * item_transform, appearance, include_stroke) else {
|
||||||
|
continue;
|
||||||
// The full line width (not half) accounts for different styles of stroke caps
|
};
|
||||||
if include_stroke
|
|
||||||
&& let Some(stroke) = list
|
|
||||||
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
|
||||||
.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke))
|
|
||||||
.map(Coverage::stroke_params)
|
|
||||||
{
|
|
||||||
let scale = row_transform.scale_magnitudes();
|
|
||||||
let offset = DVec2::splat(stroke.weight() * scale.x.max(scale.y) * stroke.join_miter_limit);
|
|
||||||
bounds = [bounds[0] - offset, bounds[1] + offset];
|
|
||||||
}
|
|
||||||
|
|
||||||
combined_bounds = Some(match combined_bounds {
|
combined_bounds = Some(match combined_bounds {
|
||||||
Some(existing) => Quad::combine_bounds(existing, bounds),
|
Some(existing) => Quad::combine_bounds(existing, bounds),
|
||||||
@@ -592,7 +734,14 @@ pub fn vector_list_bounding_box(list: &List<Vector>, transform: DAffine2, includ
|
|||||||
impl BoundingBox for Graphic {
|
impl BoundingBox for Graphic {
|
||||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => RenderBoundingBox::None,
|
Graphic::None(_) | Graphic::NoneList(_) => RenderBoundingBox::None,
|
||||||
|
Graphic::Graphic(item) => item.bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Vector(item) => vector_item_bounding_box(item, transform, include_stroke),
|
||||||
|
Graphic::RasterCPU(item) => item.bounding_box(transform, include_stroke),
|
||||||
|
Graphic::RasterGPU(item) => item.bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Color(item) => item.bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Gradient(item) => item.bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Text(item) => item.bounding_box(transform, include_stroke),
|
||||||
Graphic::VectorList(list) => vector_list_bounding_box(list, transform, include_stroke),
|
Graphic::VectorList(list) => vector_list_bounding_box(list, transform, include_stroke),
|
||||||
Graphic::RasterCPUList(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterCPUList(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPUList(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterGPUList(list) => list.bounding_box(transform, include_stroke),
|
||||||
@@ -605,7 +754,14 @@ impl BoundingBox for Graphic {
|
|||||||
|
|
||||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => RenderBoundingBox::None,
|
Graphic::None(_) | Graphic::NoneList(_) => RenderBoundingBox::None,
|
||||||
|
Graphic::Graphic(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Vector(item) => vector_item_bounding_box(item, transform, include_stroke),
|
||||||
|
Graphic::RasterCPU(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::RasterGPU(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Color(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Gradient(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
|
Graphic::Text(item) => item.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::VectorList(vector) => vector_list_bounding_box(vector, transform, include_stroke),
|
Graphic::VectorList(vector) => vector_list_bounding_box(vector, transform, include_stroke),
|
||||||
Graphic::RasterCPUList(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterCPUList(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPUList(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterGPUList(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
@@ -620,7 +776,14 @@ impl BoundingBox for Graphic {
|
|||||||
impl RenderComplexity for Graphic {
|
impl RenderComplexity for Graphic {
|
||||||
fn render_complexity(&self) -> usize {
|
fn render_complexity(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::None => 0,
|
Self::None(_) | Self::NoneList(_) => 0,
|
||||||
|
Self::Graphic(item) => item.render_complexity(),
|
||||||
|
Self::Vector(item) => item.render_complexity(),
|
||||||
|
Self::RasterCPU(item) => item.render_complexity(),
|
||||||
|
Self::RasterGPU(item) => item.render_complexity(),
|
||||||
|
Self::Color(item) => item.render_complexity(),
|
||||||
|
Self::Gradient(item) => item.render_complexity(),
|
||||||
|
Self::Text(item) => item.render_complexity(),
|
||||||
Self::GraphicList(list) => list.render_complexity(),
|
Self::GraphicList(list) => list.render_complexity(),
|
||||||
Self::VectorList(list) => list.render_complexity(),
|
Self::VectorList(list) => list.render_complexity(),
|
||||||
Self::RasterCPUList(list) => list.render_complexity(),
|
Self::RasterCPUList(list) => list.render_complexity(),
|
||||||
@@ -753,7 +916,7 @@ mod tests {
|
|||||||
assert_eq!(layers, [Some(NodeId(7)), Some(NodeId(9))]);
|
assert_eq!(layers, [Some(NodeId(7)), Some(NodeId(9))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flattening must not invent attribute columns that neither the parent graphic nor the child carried
|
// Flattening must not invent attributes that neither the parent graphic nor the child carried
|
||||||
#[test]
|
#[test]
|
||||||
fn flatten_does_not_invent_attributes() {
|
fn flatten_does_not_invent_attributes() {
|
||||||
let graphics = List::new_from_element(vector_graphic());
|
let graphics = List::new_from_element(vector_graphic());
|
||||||
@@ -779,12 +942,12 @@ mod tests {
|
|||||||
|
|
||||||
// A padded (empty) appearance cell is undeclared, so the parent's appearance cascades into it while a declared sibling keeps its own
|
// A padded (empty) appearance cell is undeclared, so the parent's appearance cascades into it while a declared sibling keeps its own
|
||||||
#[test]
|
#[test]
|
||||||
fn flatten_cascades_into_padded_empty_appearance_rows() {
|
fn flatten_cascades_into_padded_empty_appearance_items() {
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
|
|
||||||
let solid = |color: Color| Graphic::ColorList(List::new_from_element(color));
|
let solid = |color: Color| Graphic::ColorList(List::new_from_element(color));
|
||||||
|
|
||||||
// Declaring an appearance on row 0 forces the column, padding row 1 with the empty appearance
|
// Declaring an appearance on item 0 forces the attribute, padding item 1 with the empty appearance
|
||||||
let mut inner = List::new();
|
let mut inner = List::new();
|
||||||
inner.push(Item::new_from_element(Vector::default()));
|
inner.push(Item::new_from_element(Vector::default()));
|
||||||
inner.push(Item::new_from_element(Vector::default()));
|
inner.push(Item::new_from_element(Vector::default()));
|
||||||
@@ -800,8 +963,8 @@ mod tests {
|
|||||||
colors.element(0).copied()
|
colors.element(0).copied()
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(color_of(0), Some(Color::BLACK), "a declared row should keep its own appearance");
|
assert_eq!(color_of(0), Some(Color::BLACK), "a declared item should keep its own appearance");
|
||||||
assert_eq!(color_of(1), Some(Color::WHITE), "a padded row should inherit the parent appearance");
|
assert_eq!(color_of(1), Some(Color::WHITE), "a padded item should inherit the parent appearance");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,19 +989,19 @@ mod graphic_is_opaque_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn opaque_color_is_opaque() {
|
fn opaque_color_is_opaque() {
|
||||||
let g = color_graphic(1.);
|
let g = color_graphic(1.);
|
||||||
assert!(g.is_opaque());
|
assert!(g.is_guaranteed_fully_opaque());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transparent_color_is_not_opaque() {
|
fn transparent_color_is_not_opaque() {
|
||||||
let g = color_graphic(0.5);
|
let g = color_graphic(0.5);
|
||||||
assert!(!g.is_opaque());
|
assert!(!g.is_guaranteed_fully_opaque());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vector_is_not_opaque() {
|
fn vector_is_not_opaque() {
|
||||||
let g = Graphic::VectorList(List::default());
|
let g = Graphic::VectorList(List::default());
|
||||||
assert!(!g.is_opaque());
|
assert!(!g.is_guaranteed_fully_opaque());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -858,7 +1021,7 @@ mod graphic_is_opaque_tests {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
let g = gradient_graphic(gradient);
|
let g = gradient_graphic(gradient);
|
||||||
assert!(g.is_opaque());
|
assert!(g.is_guaranteed_fully_opaque());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -878,6 +1041,52 @@ mod graphic_is_opaque_tests {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
let g = gradient_graphic(gradient);
|
let g = gradient_graphic(gradient);
|
||||||
assert!(!g.is_opaque());
|
assert!(!g.is_guaranteed_fully_opaque());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_with_clear_spread_is_not_opaque() {
|
||||||
|
let opaque = Color::from_rgbaf32(1., 0., 0., 1.).unwrap();
|
||||||
|
let gradient = Gradient::new(vec![
|
||||||
|
GradientStop {
|
||||||
|
position: 0.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: opaque,
|
||||||
|
},
|
||||||
|
GradientStop {
|
||||||
|
position: 1.,
|
||||||
|
midpoint: 0.5,
|
||||||
|
color: opaque,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut gradient_list = List::new_from_element(gradient);
|
||||||
|
gradient_list.set_attribute(ATTR_GRADIENT_SPREAD, 0, GradientSpread::Clear);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!Graphic::GradientList(gradient_list).is_guaranteed_fully_opaque(),
|
||||||
|
"a clear spread leaves the region past the ends unpainted"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_group_opacity_is_not_opaque() {
|
||||||
|
let mut list = List::new_from_element(color_graphic(1.));
|
||||||
|
assert!(Graphic::GraphicList(list.clone()).is_guaranteed_fully_opaque());
|
||||||
|
|
||||||
|
list.set_attribute(ATTR_OPACITY, 0, 0.5);
|
||||||
|
assert!(!Graphic::GraphicList(list.clone()).is_guaranteed_fully_opaque());
|
||||||
|
|
||||||
|
list.set_attribute(ATTR_OPACITY, 0, 0.);
|
||||||
|
assert!(Graphic::GraphicList(list).is_guaranteed_fully_transparent());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_leaf_group_opacity_is_not_opaque() {
|
||||||
|
let item = Item::new_from_element(color_graphic(1.));
|
||||||
|
assert!(Graphic::Graphic(Box::new(item.clone())).is_guaranteed_fully_opaque());
|
||||||
|
|
||||||
|
let reduced = item.with_attribute(ATTR_OPACITY, 0.5);
|
||||||
|
assert!(!Graphic::Graphic(Box::new(reduced)).is_guaranteed_fully_opaque());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, spread_adjusted_samples, transform_is_invertible};
|
use crate::renderer::{ClearGuardPlacement, ItemRef, RenderParams, format_transform_matrix, gradient_placement, gradient_settings_from_item, spread_adjusted_samples, transform_is_invertible};
|
||||||
use crate::{Render, RenderSvgSegmentList, SvgRender};
|
use crate::{Render, RenderSvgSegmentList, SvgRender};
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
@@ -10,7 +10,7 @@ use graphic_types::vector_types::gradient::GradientForm;
|
|||||||
use graphic_types::vector_types::vector::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
use vector_types::gradient::{GradientSettings, GradientSpread};
|
use vector_types::gradient::GradientSpread;
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
pub enum PaintTarget {
|
pub enum PaintTarget {
|
||||||
@@ -50,6 +50,20 @@ pub trait RenderExt {
|
|||||||
) -> Self::Output;
|
) -> Self::Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The paint attribute for a solid color, or `none` when the color is absent.
|
||||||
|
fn render_color_paint(color: Option<&Color>, target: PaintTarget) -> String {
|
||||||
|
let Some(color) = color else {
|
||||||
|
return format!(r#" {}="none""#, target.paint_attr());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex());
|
||||||
|
if color.a() < 1. {
|
||||||
|
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderExt for List<Color> {
|
impl RenderExt for List<Color> {
|
||||||
type Output = String;
|
type Output = String;
|
||||||
|
|
||||||
@@ -63,23 +77,91 @@ impl RenderExt for List<Color> {
|
|||||||
_render_params: &RenderParams,
|
_render_params: &RenderParams,
|
||||||
target: PaintTarget,
|
target: PaintTarget,
|
||||||
) -> Self::Output {
|
) -> Self::Output {
|
||||||
let Some(color) = self.element(0) else {
|
render_color_paint(self.element(0), target)
|
||||||
return format!(r#" {}="none""#, target.paint_attr());
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex());
|
|
||||||
if color.a() < 1. {
|
|
||||||
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderExt for List<Gradient> {
|
/// Adds one gradient item's def into `svg_defs` and returns the gradient ID, or `None` when the item is absent.
|
||||||
type Output = u64;
|
fn render_gradient_paint(item: Option<ItemRef<'_, Gradient>>, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> Option<u64> {
|
||||||
|
let mut stop = String::new();
|
||||||
|
|
||||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
let item = item?;
|
||||||
|
let stops = item.element()?;
|
||||||
|
let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM);
|
||||||
|
let local_gradient_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
|
let settings = gradient_settings_from_item(item);
|
||||||
|
|
||||||
|
let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
||||||
|
|
||||||
|
for (position, color, original_midpoint) in samples {
|
||||||
|
stop.push_str("<stop");
|
||||||
|
if position != 0. {
|
||||||
|
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||||
|
}
|
||||||
|
let _ = write!(stop, r##" stop-color="#{}""##, SRGBA8::from(color).to_rgb_hex());
|
||||||
|
if color.a() < 1. {
|
||||||
|
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||||
|
}
|
||||||
|
if let Some(midpoint) = original_midpoint {
|
||||||
|
let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
||||||
|
}
|
||||||
|
stop.push_str(" />")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A gradient with no stops paints as solid black, matching `Gradient::evaluate` (a stopless def would otherwise render as no paint per the SVG spec)
|
||||||
|
if stop.is_empty() {
|
||||||
|
stop.push_str(r##"<stop stop-color="#000000" />"##);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to cancel out the element's transform as it is already applied to the path itself.
|
||||||
|
let element_transform_inverse = if transform_is_invertible(element_transform) {
|
||||||
|
element_transform.inverse()
|
||||||
|
} else {
|
||||||
|
DAffine2::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let document_transform = item_transform * local_gradient_transform;
|
||||||
|
|
||||||
|
let placement = gradient_placement(document_transform, gradient_form);
|
||||||
|
let gradient_transform = format_transform_matrix(element_transform_inverse * placement);
|
||||||
|
let gradient_transform = if gradient_transform.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(r#" gradientTransform="{gradient_transform}""#)
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient_spread = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(r#" spreadMethod="{}""#, settings.spread.svg_name())
|
||||||
|
};
|
||||||
|
|
||||||
|
let gradient_id = generate_uuid();
|
||||||
|
|
||||||
|
match gradient_form {
|
||||||
|
GradientForm::Linear => {
|
||||||
|
let _ = write!(
|
||||||
|
svg_defs,
|
||||||
|
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{gradient_spread}{gradient_transform}>{}</linearGradient>"#,
|
||||||
|
gradient_id, stop
|
||||||
|
);
|
||||||
|
}
|
||||||
|
GradientForm::Radial => {
|
||||||
|
let _ = write!(
|
||||||
|
svg_defs,
|
||||||
|
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{gradient_spread}{gradient_transform}>{}</radialGradient>"#,
|
||||||
|
gradient_id, stop
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(gradient_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderExt for List<Gradient> {
|
||||||
|
type Output = Option<u64>;
|
||||||
|
|
||||||
|
/// Adds the gradient def through mutating the first argument, returning the gradient ID, or `None` when the list is empty.
|
||||||
fn render(
|
fn render(
|
||||||
&self,
|
&self,
|
||||||
svg_defs: &mut String,
|
svg_defs: &mut String,
|
||||||
@@ -90,78 +172,7 @@ impl RenderExt for List<Gradient> {
|
|||||||
_render_params: &RenderParams,
|
_render_params: &RenderParams,
|
||||||
_target: PaintTarget,
|
_target: PaintTarget,
|
||||||
) -> Self::Output {
|
) -> Self::Output {
|
||||||
let mut stop = String::new();
|
render_gradient_paint((!self.is_empty()).then_some(ItemRef::ListItem(self, 0)), svg_defs, item_transform, element_transform)
|
||||||
|
|
||||||
let Some(stops) = self.element(0) else { return 0 };
|
|
||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, 0);
|
|
||||||
let local_gradient_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
|
||||||
let settings = GradientSettings::from_list_row_attributes(self, 0);
|
|
||||||
|
|
||||||
let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
|
|
||||||
|
|
||||||
for (position, color, original_midpoint) in samples {
|
|
||||||
stop.push_str("<stop");
|
|
||||||
if position != 0. {
|
|
||||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
|
||||||
}
|
|
||||||
let _ = write!(stop, r##" stop-color="#{}""##, SRGBA8::from(color).to_rgb_hex());
|
|
||||||
if color.a() < 1. {
|
|
||||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
|
||||||
}
|
|
||||||
if let Some(midpoint) = original_midpoint {
|
|
||||||
let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
|
||||||
}
|
|
||||||
stop.push_str(" />")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A gradient with no stops paints as solid black, matching `Gradient::evaluate` (a stopless def would otherwise render as no paint per the SVG spec)
|
|
||||||
if stop.is_empty() {
|
|
||||||
stop.push_str(r##"<stop stop-color="#000000" />"##);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to cancel out the element's transform as it is already applied to the path itself.
|
|
||||||
let element_transform_inverse = if transform_is_invertible(element_transform) {
|
|
||||||
element_transform.inverse()
|
|
||||||
} else {
|
|
||||||
DAffine2::IDENTITY
|
|
||||||
};
|
|
||||||
|
|
||||||
let document_transform = item_transform * local_gradient_transform;
|
|
||||||
|
|
||||||
let placement = gradient_placement(document_transform, gradient_form);
|
|
||||||
let gradient_transform = format_transform_matrix(element_transform_inverse * placement);
|
|
||||||
let gradient_transform = if gradient_transform.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(r#" gradientTransform="{gradient_transform}""#)
|
|
||||||
};
|
|
||||||
|
|
||||||
let gradient_spread = if matches!(settings.spread, GradientSpread::Pad | GradientSpread::Clear) {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(r#" spreadMethod="{}""#, settings.spread.svg_name())
|
|
||||||
};
|
|
||||||
|
|
||||||
let gradient_id = generate_uuid();
|
|
||||||
|
|
||||||
match gradient_form {
|
|
||||||
GradientForm::Linear => {
|
|
||||||
let _ = write!(
|
|
||||||
svg_defs,
|
|
||||||
r#"<linearGradient id="{}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{gradient_spread}{gradient_transform}>{}</linearGradient>"#,
|
|
||||||
gradient_id, stop
|
|
||||||
);
|
|
||||||
}
|
|
||||||
GradientForm::Radial => {
|
|
||||||
let _ = write!(
|
|
||||||
svg_defs,
|
|
||||||
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{gradient_spread}{gradient_transform}>{}</radialGradient>"#,
|
|
||||||
gradient_id, stop
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gradient_id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,13 +253,26 @@ impl RenderExt for List<Graphic> {
|
|||||||
let paint_attr = target.paint_attr();
|
let paint_attr = target.paint_attr();
|
||||||
|
|
||||||
match fill_graphic {
|
match fill_graphic {
|
||||||
|
Some(Graphic::Color(item)) => render_color_paint(Some(item.element()), target),
|
||||||
Some(Graphic::ColorList(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target),
|
Some(Graphic::ColorList(color_list)) => color_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target),
|
||||||
Some(Graphic::GradientList(gradient_list)) => {
|
Some(Graphic::Gradient(item)) => render_gradient_paint(Some(ItemRef::Item(item)), svg_defs, item_transform, element_transform)
|
||||||
let gradient_id = gradient_list.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target);
|
.map(|gradient_id| format!(r##" {paint_attr}="url(#{gradient_id})""##))
|
||||||
format!(r##" {paint_attr}="url(#{gradient_id})""##)
|
.unwrap_or_else(|| format!(r#" {paint_attr}="none""#)),
|
||||||
}
|
Some(Graphic::GradientList(gradient_list)) => gradient_list
|
||||||
Some(Graphic::None) => format!(r#" {paint_attr}="none""#),
|
.render(svg_defs, item_transform, element_transform, stroke_transform, bounds, render_params, target)
|
||||||
Some(Graphic::VectorList(_)) | Some(Graphic::RasterCPUList(_)) | Some(Graphic::RasterGPUList(_)) | Some(Graphic::GraphicList(_)) | Some(Graphic::TextList(_)) => {
|
.map(|gradient_id| format!(r##" {paint_attr}="url(#{gradient_id})""##))
|
||||||
|
.unwrap_or_else(|| format!(r#" {paint_attr}="none""#)),
|
||||||
|
Some(Graphic::None(_)) | Some(Graphic::NoneList(_)) => format!(r#" {paint_attr}="none""#),
|
||||||
|
Some(Graphic::Graphic(_))
|
||||||
|
| Some(Graphic::Vector(_))
|
||||||
|
| Some(Graphic::RasterCPU(_))
|
||||||
|
| Some(Graphic::RasterGPU(_))
|
||||||
|
| Some(Graphic::Text(_))
|
||||||
|
| Some(Graphic::VectorList(_))
|
||||||
|
| Some(Graphic::RasterCPUList(_))
|
||||||
|
| Some(Graphic::RasterGPUList(_))
|
||||||
|
| Some(Graphic::GraphicList(_))
|
||||||
|
| Some(Graphic::TextList(_)) => {
|
||||||
let bounds = if target == PaintTarget::Stroke {
|
let bounds = if target == PaintTarget::Stroke {
|
||||||
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
|
// To prevent a wraparound artefact occurring when the tile boundary and the stroke region are perfectly aligned, the local coordinate is expanded slightly.
|
||||||
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };
|
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -934,10 +934,10 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
|
|||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
#[implementations(
|
#[implementations(
|
||||||
List<Graphic>,
|
List<Graphic>,
|
||||||
List<Vector>,
|
List<Vector>,
|
||||||
List<Raster<CPU>>,
|
List<Raster<CPU>>,
|
||||||
List<Raster<GPU>>,
|
List<Raster<GPU>>,
|
||||||
List<Color>,
|
List<Color>,
|
||||||
List<Gradient>,
|
List<Gradient>,
|
||||||
List<String>,
|
List<String>,
|
||||||
Item<DAffine2>,
|
Item<DAffine2>,
|
||||||
@@ -981,6 +981,12 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
|
|||||||
|
|
||||||
let recurse = fully_flatten || recursion_depth == 0;
|
let recurse = fully_flatten || recursion_depth == 0;
|
||||||
|
|
||||||
|
// A boxed single graphic is the rank-0 spelling of the same nesting, so it flattens through the list path
|
||||||
|
let current_element = match current_element {
|
||||||
|
Graphic::Graphic(item) if recurse => Graphic::GraphicList(List::new_from_item(*item)),
|
||||||
|
element => element,
|
||||||
|
};
|
||||||
|
|
||||||
match current_element {
|
match current_element {
|
||||||
// If we're allowed to recurse, flatten any graphics we encounter
|
// If we're allowed to recurse, flatten any graphics we encounter
|
||||||
Graphic::GraphicList(mut current_element) if recurse => {
|
Graphic::GraphicList(mut current_element) if recurse => {
|
||||||
|
|||||||
@@ -223,8 +223,16 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
item
|
item
|
||||||
};
|
};
|
||||||
|
|
||||||
match graphic.clone() {
|
// A boxed single graphic is the rank-0 version of the same nesting, so it flattens through the group path
|
||||||
|
let graphic = match graphic.clone() {
|
||||||
|
Graphic::Graphic(item) => Graphic::GraphicList(List::new_from_item(*item)),
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
|
||||||
|
match graphic {
|
||||||
|
Graphic::Vector(item) => vec![compose_parent(*item)],
|
||||||
Graphic::VectorList(vector) => vector.into_iter().map(compose_parent).collect::<Vec<_>>(),
|
Graphic::VectorList(vector) => vector.into_iter().map(compose_parent).collect::<Vec<_>>(),
|
||||||
|
Graphic::Text(item) => text_nodes::shape_text_list(&List::new_from_item(item), false).into_iter().map(compose_parent).collect::<Vec<_>>(),
|
||||||
Graphic::TextList(text) => text_nodes::shape_text_list(&text, false).into_iter().map(compose_parent).collect::<Vec<_>>(),
|
Graphic::TextList(text) => text_nodes::shape_text_list(&text, false).into_iter().map(compose_parent).collect::<Vec<_>>(),
|
||||||
Graphic::GraphicList(mut graphic) => {
|
Graphic::GraphicList(mut graphic) => {
|
||||||
if parent_has_transform {
|
if parent_has_transform {
|
||||||
@@ -261,7 +269,10 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Rasters, colors, and gradients bound no region, so they contribute no operand
|
// Rasters, colors, and gradients bound no region, so they contribute no operand
|
||||||
Graphic::None | Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::ColorList(_) | Graphic::GradientList(_) => Vec::new(),
|
Graphic::None(_) | Graphic::NoneList(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Color(_) | Graphic::Gradient(_) => Vec::new(),
|
||||||
|
Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::ColorList(_) | Graphic::GradientList(_) => Vec::new(),
|
||||||
|
// Normalized to GraphicList above
|
||||||
|
Graphic::Graphic(_) => Vec::new(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -47,14 +47,31 @@ trait VectorListIterMut {
|
|||||||
impl VectorListIterMut for List<Graphic> {
|
impl VectorListIterMut for List<Graphic> {
|
||||||
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
|
fn for_each_vector_list_mut(&mut self, mut f: impl FnMut(&mut List<Vector>)) {
|
||||||
for graphic in self.iter_element_values_mut() {
|
for graphic in self.iter_element_values_mut() {
|
||||||
if let Some(vector_list) = graphic.as_vector_list_mut() {
|
match graphic {
|
||||||
f(vector_list);
|
// A lone vector is lifted into a one-item list for the duration of the call, so the shared per-list logic reaches it
|
||||||
};
|
Graphic::Vector(item) => {
|
||||||
|
let mut lifted = List::new_from_item(std::mem::take(&mut **item));
|
||||||
|
f(&mut lifted);
|
||||||
|
if let Some(updated) = lifted.into_iter().next() {
|
||||||
|
**item = updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graphic => {
|
||||||
|
if let Some(vector_list) = graphic.as_vector_list_mut() {
|
||||||
|
f(vector_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vector_count(&self) -> usize {
|
fn vector_count(&self) -> usize {
|
||||||
self.iter_element_values().filter_map(|element| element.as_vector_list()).map(|list| list.len()).sum()
|
self.iter_element_values()
|
||||||
|
.map(|element| match element {
|
||||||
|
Graphic::Vector(_) => 1,
|
||||||
|
element => element.as_vector_list().map_or(0, List::len),
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +100,18 @@ impl VectorItemMut for Item<Vector> {
|
|||||||
|
|
||||||
impl VectorItemMut for Item<Graphic> {
|
impl VectorItemMut for Item<Graphic> {
|
||||||
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) {
|
fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) {
|
||||||
let Some(vector_list) = self.element_mut().as_vector_list_mut() else { return };
|
match self.element_mut() {
|
||||||
let (elements, transforms) = vector_list.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
|
Graphic::Vector(item) => {
|
||||||
for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
|
let transform = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
f(vector, *transform);
|
f(item.element_mut(), transform);
|
||||||
|
}
|
||||||
|
element => {
|
||||||
|
let Some(vector_list) = element.as_vector_list_mut() else { return };
|
||||||
|
let (elements, transforms) = vector_list.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
for (vector, transform) in elements.iter_mut().zip(transforms.iter()) {
|
||||||
|
f(vector, *transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,6 +142,8 @@ impl MapVectorItems for Graphic {
|
|||||||
// Collecting from zero items would drop the attribute columns, so an empty list is left alone
|
// Collecting from zero items would drop the attribute columns, so an empty list is left alone
|
||||||
Graphic::VectorList(list) if !list.is_empty() => *list = std::mem::take(list).into_iter().map(&mut *f).collect(),
|
Graphic::VectorList(list) if !list.is_empty() => *list = std::mem::take(list).into_iter().map(&mut *f).collect(),
|
||||||
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| map_nested(nested, f)),
|
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| map_nested(nested, f)),
|
||||||
|
Graphic::Vector(item) => **item = f(std::mem::take(&mut **item)),
|
||||||
|
Graphic::Graphic(item) => map_nested(item.element_mut(), f),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +159,8 @@ impl MapVectorItems for Graphic {
|
|||||||
match graphic {
|
match graphic {
|
||||||
Graphic::VectorList(list) => elements.extend(list.iter_element_values_mut()),
|
Graphic::VectorList(list) => elements.extend(list.iter_element_values_mut()),
|
||||||
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| collect(nested, elements)),
|
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| collect(nested, elements)),
|
||||||
|
Graphic::Vector(item) => elements.push(item.element_mut()),
|
||||||
|
Graphic::Graphic(item) => collect(item.element_mut(), elements),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +196,12 @@ impl ExpandVectorItems for Graphic {
|
|||||||
*list = expanded;
|
*list = expanded;
|
||||||
}
|
}
|
||||||
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| expand_nested(nested, f)),
|
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| expand_nested(nested, f)),
|
||||||
|
// One item expanding into many is a rank raise, so the leaf becomes the list it grew into
|
||||||
|
Graphic::Vector(item) => {
|
||||||
|
let expanded = f(std::mem::take(&mut **item));
|
||||||
|
*graphic = Graphic::VectorList(expanded);
|
||||||
|
}
|
||||||
|
Graphic::Graphic(item) => expand_nested(item.element_mut(), f),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1510,6 +1545,12 @@ impl SolidifyStroke for Graphic {
|
|||||||
match graphic {
|
match graphic {
|
||||||
Graphic::VectorList(list) if !list.is_empty() => *list = solidify_stroke_list_with_snapshot(std::mem::take(list)),
|
Graphic::VectorList(list) if !list.is_empty() => *list = solidify_stroke_list_with_snapshot(std::mem::take(list)),
|
||||||
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(solidify_nested),
|
Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(solidify_nested),
|
||||||
|
// Solidifying can split one path into separate fill and stroke items, so the leaf becomes a list
|
||||||
|
Graphic::Vector(item) => {
|
||||||
|
let solidified = solidify_stroke_list_with_snapshot(List::new_from_item(std::mem::take(&mut **item)));
|
||||||
|
*graphic = Graphic::VectorList(solidified);
|
||||||
|
}
|
||||||
|
Graphic::Graphic(item) => solidify_nested(item.element_mut()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2571,6 +2612,25 @@ async fn morph<I: IntoGraphicList>(
|
|||||||
build_transform_with_y_preservation(metadata_source_transform, start, end)
|
build_transform_with_y_preservation(metadata_source_transform, start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The two paint kinds that can interpolate, read from either rank so the pairings below stay at four cases.
|
||||||
|
/// A gradient normalizes to the list form because the interpolation carries its placement attributes along.
|
||||||
|
enum InterpolablePaint<'a> {
|
||||||
|
Color(&'a Color),
|
||||||
|
Gradient(List<Gradient>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> InterpolablePaint<'a> {
|
||||||
|
fn from_graphic(graphic: &'a Graphic) -> Option<Self> {
|
||||||
|
match graphic {
|
||||||
|
Graphic::Color(item) => Some(InterpolablePaint::Color(item.element())),
|
||||||
|
Graphic::ColorList(list) => list.element(0).map(InterpolablePaint::Color),
|
||||||
|
Graphic::Gradient(item) => Some(InterpolablePaint::Gradient(List::new_from_item(item.clone()))),
|
||||||
|
Graphic::GradientList(list) => list.element(0).is_some().then(|| InterpolablePaint::Gradient(list.clone())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lerp between two graphics. Solid color and gradient pairings interpolate; all other pairings step at the midpoint.
|
// Lerp between two graphics. Solid color and gradient pairings interpolate; all other pairings step at the midpoint.
|
||||||
fn lerp_graphic(a: Option<&List<Graphic>>, b: Option<&List<Graphic>>, time: f64) -> Option<List<Graphic>> {
|
fn lerp_graphic(a: Option<&List<Graphic>>, b: Option<&List<Graphic>>, time: f64) -> Option<List<Graphic>> {
|
||||||
let transparent = List::new_from_element(Color::TRANSPARENT).into_graphic_list();
|
let transparent = List::new_from_element(Color::TRANSPARENT).into_graphic_list();
|
||||||
@@ -2595,30 +2655,30 @@ async fn morph<I: IntoGraphicList>(
|
|||||||
Graphic::GradientList(gradient_list)
|
Graphic::GradientList(gradient_list)
|
||||||
};
|
};
|
||||||
|
|
||||||
let graphic = match (a.element(0), b.element(0)) {
|
let graphic = match (a.element(0).and_then(InterpolablePaint::from_graphic), b.element(0).and_then(InterpolablePaint::from_graphic)) {
|
||||||
(Some(Graphic::ColorList(color_list_a)), Some(Graphic::ColorList(color_list_b))) => color_list_a
|
(Some(InterpolablePaint::Color(color_a)), Some(InterpolablePaint::Color(color_b))) => Some(Graphic::from(color_a.lerp(color_b, time as f32))),
|
||||||
.element(0)
|
(Some(InterpolablePaint::Color(color_a)), Some(InterpolablePaint::Gradient(gradient_list_b))) => gradient_list_b.element(0).cloned().map(|stops_b| {
|
||||||
.zip(color_list_b.element(0))
|
|
||||||
.map(|(color_a, color_b)| Graphic::from(color_a.lerp(color_b, time as f32))),
|
|
||||||
(Some(Graphic::ColorList(color_list_a)), Some(Graphic::GradientList(gradient_list_b))) => color_list_a.element(0).zip(gradient_list_b.element(0)).map(|(color_a, stops_b)| {
|
|
||||||
let solid_to_gradient = stops_b.map_colors(|_| *color_a);
|
let solid_to_gradient = stops_b.map_colors(|_| *color_a);
|
||||||
let stops = solid_to_gradient.lerp(stops_b, time);
|
let stops = solid_to_gradient.lerp(&stops_b, time);
|
||||||
gradient_with_stops(gradient_list_b.clone(), stops)
|
gradient_with_stops(gradient_list_b, stops)
|
||||||
}),
|
}),
|
||||||
(Some(Graphic::GradientList(gradient_list_a)), Some(Graphic::ColorList(color_list_b))) => gradient_list_a.element(0).zip(color_list_b.element(0)).map(|(stops_a, color_b)| {
|
(Some(InterpolablePaint::Gradient(gradient_list_a)), Some(InterpolablePaint::Color(color_b))) => gradient_list_a.element(0).cloned().map(|stops_a| {
|
||||||
let gradient_to_solid = stops_a.map_colors(|_| *color_b);
|
let gradient_to_solid = stops_a.map_colors(|_| *color_b);
|
||||||
let stops = stops_a.lerp(&gradient_to_solid, time);
|
let stops = stops_a.lerp(&gradient_to_solid, time);
|
||||||
gradient_with_stops(gradient_list_a.clone(), stops)
|
gradient_with_stops(gradient_list_a, stops)
|
||||||
}),
|
}),
|
||||||
(Some(Graphic::GradientList(gradient_list_a)), Some(Graphic::GradientList(gradient_list_b))) => gradient_list_a.element(0).zip(gradient_list_b.element(0)).map(|(stops_a, stops_b)| {
|
(Some(InterpolablePaint::Gradient(gradient_list_a)), Some(InterpolablePaint::Gradient(gradient_list_b))) => gradient_list_a
|
||||||
let stops = stops_a.lerp(stops_b, time);
|
.element(0)
|
||||||
let metadata_source = if time < 0.5 { gradient_list_a } else { gradient_list_b };
|
.zip(gradient_list_b.element(0))
|
||||||
|
.map(|(stops_a, stops_b)| stops_a.lerp(stops_b, time))
|
||||||
|
.map(|stops| {
|
||||||
|
let transform = lerp_gradient_transform(&gradient_list_a, &gradient_list_b, time);
|
||||||
|
|
||||||
let mut gradient_list = metadata_source.clone();
|
let mut gradient_list = if time < 0.5 { gradient_list_a } else { gradient_list_b };
|
||||||
gradient_list.set_attribute(ATTR_TRANSFORM, 0, lerp_gradient_transform(gradient_list_a, gradient_list_b, time));
|
gradient_list.set_attribute(ATTR_TRANSFORM, 0, transform);
|
||||||
|
|
||||||
gradient_with_stops(gradient_list, stops)
|
gradient_with_stops(gradient_list, stops)
|
||||||
}),
|
}),
|
||||||
// Pairings beyond solid colors and gradients (raster, vector, or mixed) can't be interpolated, so step at the midpoint
|
// Pairings beyond solid colors and gradients (raster, vector, or mixed) can't be interpolated, so step at the midpoint
|
||||||
_ => return Some(if time < 0.5 { a.clone() } else { b.clone() }),
|
_ => return Some(if time < 0.5 { a.clone() } else { b.clone() }),
|
||||||
};
|
};
|
||||||
@@ -4106,4 +4166,45 @@ mod test {
|
|||||||
assert_eq!(beveled.point_domain.positions().len(), 6);
|
assert_eq!(beveled.point_domain.positions().len(), 6);
|
||||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A rank-0 vector reaches the same per-list styling path as a vector list, rather than being skipped
|
||||||
|
#[tokio::test]
|
||||||
|
async fn assign_colors_reaches_rank_0_vector_graphics() {
|
||||||
|
let leaf = Graphic::Vector(Box::new(vector_item_from_bezpath(Rect::new(0., 0., 10., 10.).to_path(DEFAULT_ACCURACY))));
|
||||||
|
let content = List::new_from_element(leaf);
|
||||||
|
|
||||||
|
let styled = super::assign_colors(
|
||||||
|
Footprint::default(),
|
||||||
|
content,
|
||||||
|
Item::new_from_element(true),
|
||||||
|
Item::new_from_element(false),
|
||||||
|
Item::new_from_element(Gradient::from(vec![Color::BLACK, Color::WHITE])),
|
||||||
|
Item::new_from_element(false),
|
||||||
|
Item::new_from_element(false),
|
||||||
|
Item::new_from_element(SeedValue::default()),
|
||||||
|
Item::new_from_element(0_u32),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let Some(Graphic::Vector(item)) = styled.element(0) else {
|
||||||
|
panic!("the leaf should stay a rank-0 vector")
|
||||||
|
};
|
||||||
|
let appearance = item.attribute::<Appearance>(ATTR_APPEARANCE).expect("the leaf should have gained an appearance");
|
||||||
|
assert!(appearance.has_painted_cover(Cover::Fill), "the fill of a rank-0 vector should be styled like a list element");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill's automatic gradient placement measures rank-0 vector content instead of falling back to the unit box
|
||||||
|
#[test]
|
||||||
|
fn vector_item_mut_reaches_a_rank_0_vector_graphic() {
|
||||||
|
let transform = DAffine2::from_translation(DVec2::new(7., 3.));
|
||||||
|
let item = create_vector_item(Rect::new(0., 0., 10., 10.).to_path(DEFAULT_ACCURACY), transform);
|
||||||
|
let mut content = Item::new_from_element(Graphic::Vector(Box::new(item)));
|
||||||
|
|
||||||
|
let mut visited = Vec::new();
|
||||||
|
content.for_each_vector_mut(|vector, vector_transform| visited.push((vector.bounding_box(), vector_transform)));
|
||||||
|
|
||||||
|
assert_eq!(visited.len(), 1, "the lone vector should be visited exactly once");
|
||||||
|
assert_eq!(visited[0].1, transform, "its own transform attribute should come along for placement");
|
||||||
|
assert!(visited[0].0.is_some(), "its geometry should be measurable for the automatic gradient bounds");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user