mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Introduce the new brush stroke data types (#4467)
* Add the brush stroke format types * Route brush strokes through the node graph * Store brush style as per-item attributes instead of a struct
This commit is contained in:
@@ -2,6 +2,7 @@ use super::DocumentNode;
|
||||
use crate::application_io::PlatformEditorApi;
|
||||
use crate::application_io::resource::Resource;
|
||||
use crate::proto::{Any as DAny, FutureAny};
|
||||
use brush_nodes::Stroke;
|
||||
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
|
||||
use core_types::color::SRGBA8;
|
||||
use core_types::list::{Item, List, NodeIdPath};
|
||||
@@ -97,6 +98,7 @@ macro_rules! tagged_value {
|
||||
#[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this document upgrade code
|
||||
#[serde(alias = "BrushStrokeTable")]
|
||||
BrushStrokes(Vec<BrushStroke>),
|
||||
Strokes(Vec<Stroke>),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -140,6 +142,7 @@ macro_rules! tagged_value {
|
||||
Self::BoxCorners(values) => values.cache_hash(state),
|
||||
Self::GradientRamp(ramp) => ramp.cache_hash(state),
|
||||
Self::BrushStrokes(strokes) => strokes.cache_hash(state),
|
||||
Self::Strokes(strokes) => strokes.cache_hash(state),
|
||||
// =======================
|
||||
// NON-SERIALIZED VARIANTS
|
||||
// =======================
|
||||
@@ -203,6 +206,10 @@ macro_rules! tagged_value {
|
||||
Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))),
|
||||
Self::GradientRamp(ramp) => Box::new(Item::<Gradient>::from(ramp)),
|
||||
Self::BrushStrokes(strokes) => Box::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
|
||||
Self::Strokes(strokes) => {
|
||||
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||
Box::new(list)
|
||||
}
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -266,6 +273,10 @@ macro_rules! tagged_value {
|
||||
Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))),
|
||||
Self::GradientRamp(ramp) => Arc::new(Item::<Gradient>::from(ramp)),
|
||||
Self::BrushStrokes(strokes) => Arc::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
|
||||
Self::Strokes(strokes) => {
|
||||
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||
Arc::new(list)
|
||||
}
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -295,6 +306,7 @@ macro_rules! tagged_value {
|
||||
Self::BoxCorners(_) => item!(BoxCorners),
|
||||
Self::GradientRamp(_) => item!(Gradient),
|
||||
Self::BrushStrokes(_) => item!(BrushTrace),
|
||||
Self::Strokes(_) => list!(Stroke),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -335,6 +347,7 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*downcast::<Item<Gradient>>(input).unwrap()))),
|
||||
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(downcast::<Item<BrushTrace>>(input).unwrap().into_element().0.iter_element_values().cloned().collect())),
|
||||
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(downcast::<List<Stroke>>(input).unwrap().into_iter().map(Item::into_element).collect())),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -369,6 +382,7 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::<Item<Gradient>>().unwrap()))),
|
||||
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Vec<BrushStroke>>().unwrap().clone())),
|
||||
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Item<BrushTrace>>().unwrap().element().0.iter_element_values().cloned().collect())),
|
||||
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(input.downcast_ref::<List<Stroke>>().unwrap().iter_element_values().cloned().collect())),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
@@ -397,6 +411,7 @@ macro_rules! tagged_value {
|
||||
if name == std::any::type_name::<BoxCorners>() { return Some(TaggedValue::BoxCorners(Vec::new())) }
|
||||
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
|
||||
if name == std::any::type_name::<BrushTrace>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
|
||||
if name == std::any::type_name::<List<Stroke>>() { return Some(TaggedValue::Strokes(Vec::new())) }
|
||||
// Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time
|
||||
macro_rules! check_bare {
|
||||
($type_default:ty) => {
|
||||
@@ -423,6 +438,9 @@ macro_rules! tagged_value {
|
||||
if **element == concrete!(f64) {
|
||||
return Some(TaggedValue::F64Array(Vec::new()));
|
||||
}
|
||||
if **element == concrete!(Stroke) {
|
||||
return Some(TaggedValue::Strokes(Vec::new()));
|
||||
}
|
||||
macro_rules! check {
|
||||
($type_default:ty) => {
|
||||
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
|
||||
@@ -450,6 +468,7 @@ macro_rules! tagged_value {
|
||||
Self::BoxCorners(values) => format!("BoxCorners({values:?})"),
|
||||
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
|
||||
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
|
||||
Self::Strokes(strokes) => format!("Strokes({strokes:?})"),
|
||||
// =======================
|
||||
// AUTO-GENERATED VARIANTS
|
||||
// =======================
|
||||
|
||||
@@ -1059,7 +1059,7 @@ mod test {
|
||||
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec![NodeId(8464972237805743576), NodeId(3528778906331798968), NodeId(1126597937993520391), NodeId(17582929706900579130)]
|
||||
vec![NodeId(12331852515109999872), NodeId(5084548161767585362), NodeId(14635346976242256925), NodeId(16015195863711239715)]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::any::DynAnyNode;
|
||||
use graphene_std::brush::Stroke;
|
||||
use graphene_std::brush::brush_stroke::BrushTrace;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::gradient::Gradient;
|
||||
@@ -82,6 +83,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<graphene_std::brush::Stroke>]),
|
||||
// Context nullification
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item<graphene_std::ContextFeatures>]),
|
||||
@@ -145,6 +147,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::brush::Stroke>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<RenderIntermediate>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>]),
|
||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Option<&wgpu_executor::WgpuExecutor>>]),
|
||||
@@ -353,6 +356,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
RelativeAbsolute,
|
||||
SelectiveColorChoice,
|
||||
BrushTrace,
|
||||
Stroke,
|
||||
XY,
|
||||
ScaleType,
|
||||
ReferencePoint,
|
||||
|
||||
23
node-graph/libraries/brush-types/Cargo.toml
Normal file
23
node-graph/libraries/brush-types/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "brush-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "The brush stroke data format for Graphene"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
serde = ["dep:serde", "core-types/serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
graphene-hash = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
131
node-graph/libraries/brush-types/src/lib.rs
Normal file
131
node-graph/libraries/brush-types/src/lib.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use core_types::CacheHash;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2, Vec2};
|
||||
use std::f32::consts::{PI, TAU};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CacheHash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Channel<T> {
|
||||
Uniform(T),
|
||||
Samples(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T: Copy> Channel<T> {
|
||||
pub fn get(&self, index: usize) -> T {
|
||||
match self {
|
||||
Self::Uniform(value) => *value,
|
||||
Self::Samples(values) => values[index],
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Uniform(_) => None,
|
||||
Self::Samples(values) => Some(values.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: dyn_any::StaticTypeSized> dyn_any::StaticType for Channel<T> {
|
||||
type Static = Channel<T::Static>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CacheHash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Stroke {
|
||||
pub position: Vec<DVec2>,
|
||||
pub pressure: Channel<f32>,
|
||||
pub tilt: Channel<Vec2>,
|
||||
pub twist: Channel<f32>,
|
||||
pub time: Channel<f64>,
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
impl Default for Stroke {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Vec::new(),
|
||||
pressure: Channel::Uniform(1.),
|
||||
tilt: Channel::Uniform(Vec2::ZERO),
|
||||
twist: Channel::Uniform(0.),
|
||||
time: Channel::Uniform(0.),
|
||||
seed: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub fn len(&self) -> usize {
|
||||
self.position.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.position.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
let n = self.len();
|
||||
[self.pressure.len(), self.tilt.len(), self.twist.len(), self.time.len()].into_iter().flatten().all(|len| len == n)
|
||||
}
|
||||
|
||||
pub fn sample(&self, index: usize) -> Sample {
|
||||
Sample {
|
||||
position: self.position[index],
|
||||
pressure: self.pressure.get(index),
|
||||
tilt: self.tilt.get(index),
|
||||
twist: self.twist.get(index),
|
||||
time: self.time.get(index),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_lerp(&self, index: usize, t: f32) -> Sample {
|
||||
let a = self.sample(index);
|
||||
let b = self.sample((index + 1).min(self.len().saturating_sub(1)));
|
||||
Sample {
|
||||
position: a.position.lerp(b.position, t as f64),
|
||||
pressure: a.pressure + (b.pressure - a.pressure) * t,
|
||||
tilt: a.tilt.lerp(b.tilt, t),
|
||||
twist: {
|
||||
let delta = (b.twist - a.twist).rem_euclid(TAU);
|
||||
let delta = if delta > PI { delta - TAU } else { delta };
|
||||
a.twist + delta * t
|
||||
},
|
||||
time: a.time + (b.time - a.time) * t as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn samples(&self) -> impl Iterator<Item = Sample> + '_ {
|
||||
(0..self.len()).map(|index| self.sample(index))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Stroke {
|
||||
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
let Some(first) = self.position.first() else { return RenderBoundingBox::None };
|
||||
let (min, max) = self.position.iter().fold((*first, *first), |(min, max), &point| (min.min(point), max.max(point)));
|
||||
let corners = [min, DVec2::new(max.x, min.y), max, DVec2::new(min.x, max.y)].map(|corner| transform.transform_point2(corner));
|
||||
let (min, max) = corners.iter().fold((corners[0], corners[0]), |(min, max), &point| (min.min(point), max.max(point)));
|
||||
RenderBoundingBox::Rectangle([min, max])
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
self.bounding_box(transform, include_stroke)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Stroke {
|
||||
fn render_complexity(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Sample {
|
||||
pub position: DVec2,
|
||||
pub pressure: f32,
|
||||
pub tilt: Vec2,
|
||||
pub twist: f32,
|
||||
pub time: f64,
|
||||
}
|
||||
@@ -97,6 +97,14 @@ pub const ATTR_JOIN: &str = "join";
|
||||
pub const ATTR_JOIN_MITER_LIMIT: &str = "join_miter_limit";
|
||||
/// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item<Cover>` inside a `Coverage`.
|
||||
pub const ATTR_ALIGN: &str = "align";
|
||||
/// Brush stroke item's `Color` its strokes are painted with.
|
||||
pub const ATTR_COLOR: &str = "color";
|
||||
/// Brush stroke item's tip diameter in document-space units (`f64`).
|
||||
pub const ATTR_DIAMETER: &str = "diameter";
|
||||
/// Brush stroke item's edge hardness from `0.` (softest) to `1.` (hardest) (`f64`).
|
||||
pub const ATTR_HARDNESS: &str = "hardness";
|
||||
/// Brush stroke item's per-pass paint coverage from `0.` to `1.` (`f64`).
|
||||
pub const ATTR_FLOW: &str = "flow";
|
||||
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
|
||||
pub const ATTR_FONT_SIZE: &str = "font_size";
|
||||
/// Text item's font, as a `Resource` of the loaded font file.
|
||||
|
||||
@@ -20,6 +20,7 @@ wasm = [
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
graphene-hash = { workspace = true }
|
||||
brush-types = { workspace = true }
|
||||
raster-types = { workspace = true, features = ["wgpu"] }
|
||||
vector-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::appearance::{Appearance, Cover, Coverage};
|
||||
use brush_types::Stroke;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::list::{ATTR_APPEARANCE, ATTR_PAINT, Item, ItemAttributeValues, List, NodeIdPath};
|
||||
@@ -34,6 +35,7 @@ pub enum Graphic {
|
||||
ColorList(List<Color>),
|
||||
GradientList(List<Gradient>),
|
||||
TextList(List<String>),
|
||||
StrokeList(List<Stroke>),
|
||||
}
|
||||
|
||||
impl Default for Graphic {
|
||||
@@ -140,7 +142,19 @@ impl From<List<Gradient>> for Graphic {
|
||||
}
|
||||
}
|
||||
|
||||
// String
|
||||
// Stroke
|
||||
impl From<Stroke> for Graphic {
|
||||
fn from(stroke: Stroke) -> Self {
|
||||
Graphic::StrokeList(List::new_from_element(stroke))
|
||||
}
|
||||
}
|
||||
impl From<List<Stroke>> for Graphic {
|
||||
fn from(stroke: List<Stroke>) -> Self {
|
||||
Graphic::StrokeList(stroke)
|
||||
}
|
||||
}
|
||||
|
||||
// Text
|
||||
impl From<String> for Graphic {
|
||||
fn from(text: String) -> Self {
|
||||
Graphic::Text(Item::new_from_element(text))
|
||||
@@ -310,6 +324,7 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
|
||||
Graphic::RasterGPUList(list) => bake_list_transform(list, transform),
|
||||
Graphic::GradientList(list) => bake_list_transform(list, transform),
|
||||
Graphic::TextList(list) => bake_list_transform(list, transform),
|
||||
Graphic::StrokeList(list) => bake_list_transform(list, transform),
|
||||
// A color has no spatial extent, so there is no placement for a transform to move
|
||||
Graphic::None(_) | Graphic::NoneList(_) | Graphic::Color(_) | Graphic::ColorList(_) => {}
|
||||
}
|
||||
@@ -380,6 +395,12 @@ impl TryFromGraphic for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromGraphic for Stroke {
|
||||
fn try_from_graphic(graphic: Graphic) -> Option<List<Self>> {
|
||||
if let Graphic::StrokeList(t) = graphic { Some(t) } else { None }
|
||||
}
|
||||
}
|
||||
|
||||
// Local trait to convert types to List<Graphic> (avoids orphan rule issues)
|
||||
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
|
||||
fn into_graphic_list(self) -> List<Graphic>;
|
||||
@@ -431,6 +452,17 @@ impl IntoGraphicList for List<Gradient> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<Stroke> {
|
||||
fn into_graphic_list(self) -> List<Graphic> {
|
||||
let layer_path: NodeIdPath = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0);
|
||||
let mut graphic_list = List::new_from_element(Graphic::StrokeList(self));
|
||||
if !layer_path.0.is_empty() {
|
||||
graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
||||
}
|
||||
graphic_list
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicList for List<String> {
|
||||
fn into_graphic_list(self) -> List<Graphic> {
|
||||
List::new_from_element(Graphic::TextList(self))
|
||||
@@ -542,6 +574,7 @@ impl Graphic {
|
||||
Graphic::ColorList(list) => all_clipped(list),
|
||||
Graphic::GradientList(list) => all_clipped(list),
|
||||
Graphic::TextList(list) => all_clipped(list),
|
||||
Graphic::StrokeList(list) => all_clipped(list),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,6 +628,7 @@ impl Graphic {
|
||||
})
|
||||
}
|
||||
Graphic::Text(_) | Graphic::TextList(_) => false,
|
||||
Graphic::StrokeList(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,6 +661,7 @@ impl Graphic {
|
||||
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),
|
||||
Graphic::StrokeList(list) => every_item_has_zero_opacity(list),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,6 +683,7 @@ impl Graphic {
|
||||
Graphic::RasterCPUList(list) => list.is_empty(),
|
||||
Graphic::RasterGPUList(list) => list.is_empty(),
|
||||
Graphic::TextList(list) => list.is_empty(),
|
||||
Graphic::StrokeList(list) => list.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -802,6 +838,7 @@ impl BoundingBox for Graphic {
|
||||
Graphic::ColorList(list) => list.bounding_box(transform, include_stroke),
|
||||
Graphic::GradientList(list) => list.bounding_box(transform, include_stroke),
|
||||
Graphic::TextList(list) => list.bounding_box(transform, include_stroke),
|
||||
Graphic::StrokeList(list) => list.bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,6 +859,7 @@ impl BoundingBox for Graphic {
|
||||
Graphic::ColorList(color) => color.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::GradientList(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::TextList(list) => list.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::StrokeList(list) => list.thumbnail_bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -844,6 +882,7 @@ impl RenderComplexity for Graphic {
|
||||
Self::ColorList(list) => list.render_complexity(),
|
||||
Self::GradientList(list) => list.render_complexity(),
|
||||
Self::TextList(list) => list.render_complexity(),
|
||||
Self::StrokeList(list) => list.render_complexity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde"]
|
||||
serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/serde", "brush-types/serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
brush-types = { workspace = true }
|
||||
graphene-hash = { workspace = true }
|
||||
graphene-resource = { workspace = true }
|
||||
text-nodes = { workspace = true }
|
||||
|
||||
@@ -292,7 +292,8 @@ impl RenderExt for Graphic {
|
||||
| Graphic::RasterGPUList(_)
|
||||
| Graphic::GraphicList(_)
|
||||
| Graphic::GradientList(_)
|
||||
| Graphic::TextList(_) => {
|
||||
| Graphic::TextList(_)
|
||||
| Graphic::StrokeList(_) => {
|
||||
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.
|
||||
let inverse = |len: f64| if len > 0. { 1. / len } else { 0. };
|
||||
|
||||
@@ -1066,6 +1066,7 @@ impl Render for Graphic {
|
||||
Graphic::ColorList(list) => list.render_svg(render, render_params),
|
||||
Graphic::GradientList(list) => list.render_svg(render, render_params),
|
||||
Graphic::TextList(list) => list.render_svg(render, render_params),
|
||||
Graphic::StrokeList(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,6 +1094,7 @@ impl Render for Graphic {
|
||||
Graphic::ColorList(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::GradientList(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::TextList(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||
Graphic::StrokeList(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1163,6 +1165,14 @@ impl Render for Graphic {
|
||||
Graphic::TextList(list) => {
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
|
||||
// TODO: Find a way to handle more than the first item
|
||||
if !list.is_empty() {
|
||||
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
|
||||
}
|
||||
}
|
||||
Graphic::StrokeList(list) => {
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
|
||||
// TODO: Find a way to handle more than the first item
|
||||
if !list.is_empty() {
|
||||
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
|
||||
@@ -1187,6 +1197,7 @@ impl Render for Graphic {
|
||||
Graphic::ColorList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||
Graphic::GradientList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||
Graphic::TextList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||
Graphic::StrokeList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1207,6 +1218,7 @@ impl Render for Graphic {
|
||||
Graphic::ColorList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||
Graphic::GradientList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||
Graphic::TextList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||
Graphic::StrokeList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1227,6 +1239,7 @@ impl Render for Graphic {
|
||||
Graphic::ColorList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||
Graphic::GradientList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||
Graphic::TextList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||
Graphic::StrokeList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1836,7 +1849,8 @@ fn render_vector_item_to_vello(
|
||||
| Graphic::RasterCPUList(_)
|
||||
| Graphic::RasterGPUList(_)
|
||||
| Graphic::GraphicList(_)
|
||||
| Graphic::TextList(_) => {
|
||||
| Graphic::TextList(_)
|
||||
| Graphic::StrokeList(_) => {
|
||||
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
|
||||
paint.render_to_vello(scene, multiplied_transform, context, paint_render_params);
|
||||
scene.pop_layer();
|
||||
@@ -1928,7 +1942,8 @@ fn render_vector_item_to_vello(
|
||||
| Graphic::RasterCPUList(_)
|
||||
| Graphic::RasterGPUList(_)
|
||||
| Graphic::GraphicList(_)
|
||||
| Graphic::TextList(_) => {
|
||||
| Graphic::TextList(_)
|
||||
| Graphic::StrokeList(_) => {
|
||||
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
|
||||
|
||||
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);
|
||||
@@ -2557,6 +2572,12 @@ fn render_raster_gpu_item_to_vello(item: ItemRef<'_, Raster<GPU>>, scene: &mut S
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for List<brush_types::Stroke> {
|
||||
fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) {}
|
||||
|
||||
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
|
||||
}
|
||||
|
||||
// Since colors and gradients are technically infinitely big, we have to implement
|
||||
// workarounds for rendering them correctly in a way which still allows us
|
||||
// to cache the intermediate render data (SVG string/Vello scene).
|
||||
|
||||
@@ -13,8 +13,10 @@ serde = ["dep:serde", "core-types/serde", "raster-types/serde", "raster-nodes/se
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
brush-types = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
graphene-hash = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
raster-types = { workspace = true }
|
||||
raster-nodes = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
use core_types::list::{ATTR_COLOR, ATTR_DIAMETER, ATTR_FLOW, ATTR_HARDNESS, Item, List};
|
||||
use core_types::registry::types::Percentage;
|
||||
use core_types::{Color, Ctx};
|
||||
use graphic_types::Graphic;
|
||||
|
||||
pub mod brush;
|
||||
mod brush_cache;
|
||||
pub mod brush_stroke;
|
||||
|
||||
pub use brush_types::*;
|
||||
|
||||
#[node_macro::node(category("Raster: Brush"))]
|
||||
fn brush_strokes(
|
||||
_: impl Ctx,
|
||||
strokes: List<Stroke>,
|
||||
color: List<Color>,
|
||||
#[default(40.)] diameter: Item<f64>,
|
||||
#[default(0.)] hardness: Item<Percentage>,
|
||||
#[default(100.)] flow: Item<Percentage>,
|
||||
) -> List<Graphic> {
|
||||
let (diameter, hardness, flow) = (diameter.into_element(), hardness.into_element(), flow.into_element());
|
||||
List::new_from_item(
|
||||
Item::new_from_element(Graphic::from(strokes))
|
||||
.with_attribute(ATTR_COLOR, color.element(0).copied().unwrap_or_default())
|
||||
.with_attribute(ATTR_DIAMETER, diameter.max(0.))
|
||||
.with_attribute(ATTR_HARDNESS, (hardness / 100.).clamp(0., 1.))
|
||||
.with_attribute(ATTR_FLOW, (flow / 100.).clamp(0., 1.)),
|
||||
)
|
||||
}
|
||||
|
||||
pub mod migrations {
|
||||
use crate::brush_stroke::BrushStroke;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ authors.workspace = true
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
brush-types = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
raster-types = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use brush_types::Stroke;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
|
||||
use core_types::registry::types::{Angle, SeedValue, SignedInteger};
|
||||
@@ -870,6 +871,7 @@ pub async fn extend<T: 'n + Send + Clone>(
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
List<Artboard>,
|
||||
List<Stroke>,
|
||||
)]
|
||||
base: List<T>,
|
||||
/// The list whose items will appear at the end of the extended list.
|
||||
@@ -890,6 +892,7 @@ pub async fn extend<T: 'n + Send + Clone>(
|
||||
List<Color>,
|
||||
List<Gradient>,
|
||||
List<Artboard>,
|
||||
List<Stroke>,
|
||||
)]
|
||||
new: List<T>,
|
||||
) -> List<T> {
|
||||
@@ -942,6 +945,7 @@ pub async fn into_group<T: Into<Graphic> + 'n>(
|
||||
List<String>,
|
||||
List<DVec2>,
|
||||
Item<DAffine2>, // TODO: Remove this
|
||||
List<Stroke>,
|
||||
)]
|
||||
content: T,
|
||||
) -> Item<Graphic> {
|
||||
|
||||
@@ -268,6 +268,8 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
||||
// Rasters, colors, and gradients bound no region, so they contribute no operand
|
||||
Graphic::None(_) | Graphic::NoneList(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Color(_) | Graphic::Gradient(_) => Vec::new(),
|
||||
Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::ColorList(_) | Graphic::GradientList(_) => Vec::new(),
|
||||
// Strokes have no vector outline representation; a brush node renders them to rasters
|
||||
Graphic::StrokeList(_) => Vec::new(),
|
||||
// Normalized to GraphicList above
|
||||
Graphic::Graphic(_) => Vec::new(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user