mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 18:28:12 +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:
Generated
+16
@@ -370,10 +370,12 @@ dependencies = [
|
|||||||
name = "brush-nodes"
|
name = "brush-nodes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
"graphene-hash",
|
"graphene-hash",
|
||||||
|
"graphic-types",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
"raster-nodes",
|
"raster-nodes",
|
||||||
"raster-types",
|
"raster-types",
|
||||||
@@ -381,6 +383,17 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brush-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"core-types",
|
||||||
|
"dyn-any",
|
||||||
|
"glam",
|
||||||
|
"graphene-hash",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.19.0"
|
||||||
@@ -2208,6 +2221,7 @@ dependencies = [
|
|||||||
name = "graphic-nodes"
|
name = "graphic-nodes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -2223,6 +2237,7 @@ dependencies = [
|
|||||||
name = "graphic-types"
|
name = "graphic-types"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -4871,6 +4886,7 @@ name = "rendering"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
"brush-types",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ no-std-types = { path = "node-graph/libraries/no-std-types" }
|
|||||||
raster-types = { path = "node-graph/libraries/raster-types" }
|
raster-types = { path = "node-graph/libraries/raster-types" }
|
||||||
vector-types = { path = "node-graph/libraries/vector-types" }
|
vector-types = { path = "node-graph/libraries/vector-types" }
|
||||||
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
graphic-types = { path = "node-graph/libraries/graphic-types" }
|
||||||
|
brush-types = { path = "node-graph/libraries/brush-types" }
|
||||||
rendering = { path = "node-graph/libraries/rendering" }
|
rendering = { path = "node-graph/libraries/rendering" }
|
||||||
brush-nodes = { path = "node-graph/nodes/brush" }
|
brush-nodes = { path = "node-graph/nodes/brush" }
|
||||||
blending-nodes = { path = "node-graph/nodes/blending" }
|
blending-nodes = { path = "node-graph/nodes/blending" }
|
||||||
|
|||||||
@@ -494,6 +494,15 @@ impl TableItemLayout for Artboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TableItemLayout for graphene_std::brush::Stroke {
|
||||||
|
fn type_name() -> &'static str {
|
||||||
|
"Stroke"
|
||||||
|
}
|
||||||
|
fn identifier(&self) -> String {
|
||||||
|
format!("Stroke ({} {})", self.len(), if self.len() == 1 { "sample" } else { "samples" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TableItemLayout for DashPattern {
|
impl TableItemLayout for DashPattern {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"DashPattern"
|
"DashPattern"
|
||||||
@@ -607,6 +616,7 @@ impl TableItemLayout for Graphic {
|
|||||||
Self::ColorList(list) => list.identifier(),
|
Self::ColorList(list) => list.identifier(),
|
||||||
Self::GradientList(list) => list.identifier(),
|
Self::GradientList(list) => list.identifier(),
|
||||||
Self::TextList(list) => list.identifier(),
|
Self::TextList(list) => list.identifier(),
|
||||||
|
Self::StrokeList(list) => list.identifier(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Don't put a breadcrumb for Graphic
|
// Don't put a breadcrumb for Graphic
|
||||||
@@ -631,6 +641,7 @@ impl TableItemLayout for Graphic {
|
|||||||
Self::ColorList(list) => list.layout_with_breadcrumb(data),
|
Self::ColorList(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::GradientList(list) => list.layout_with_breadcrumb(data),
|
Self::GradientList(list) => list.layout_with_breadcrumb(data),
|
||||||
Self::TextList(list) => list.layout_with_breadcrumb(data),
|
Self::TextList(list) => list.layout_with_breadcrumb(data),
|
||||||
|
Self::StrokeList(list) => list.layout_with_breadcrumb(data),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::DocumentNode;
|
|||||||
use crate::application_io::PlatformEditorApi;
|
use crate::application_io::PlatformEditorApi;
|
||||||
use crate::application_io::resource::Resource;
|
use crate::application_io::resource::Resource;
|
||||||
use crate::proto::{Any as DAny, FutureAny};
|
use crate::proto::{Any as DAny, FutureAny};
|
||||||
|
use brush_nodes::Stroke;
|
||||||
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
|
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{Item, List, NodeIdPath};
|
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(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this document upgrade code
|
||||||
#[serde(alias = "BrushStrokeTable")]
|
#[serde(alias = "BrushStrokeTable")]
|
||||||
BrushStrokes(Vec<BrushStroke>),
|
BrushStrokes(Vec<BrushStroke>),
|
||||||
|
Strokes(Vec<Stroke>),
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -140,6 +142,7 @@ macro_rules! tagged_value {
|
|||||||
Self::BoxCorners(values) => values.cache_hash(state),
|
Self::BoxCorners(values) => values.cache_hash(state),
|
||||||
Self::GradientRamp(ramp) => ramp.cache_hash(state),
|
Self::GradientRamp(ramp) => ramp.cache_hash(state),
|
||||||
Self::BrushStrokes(strokes) => strokes.cache_hash(state),
|
Self::BrushStrokes(strokes) => strokes.cache_hash(state),
|
||||||
|
Self::Strokes(strokes) => strokes.cache_hash(state),
|
||||||
// =======================
|
// =======================
|
||||||
// NON-SERIALIZED VARIANTS
|
// NON-SERIALIZED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -203,6 +206,10 @@ macro_rules! tagged_value {
|
|||||||
Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))),
|
Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))),
|
||||||
Self::GradientRamp(ramp) => Box::new(Item::<Gradient>::from(ramp)),
|
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::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
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -266,6 +273,10 @@ macro_rules! tagged_value {
|
|||||||
Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))),
|
Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))),
|
||||||
Self::GradientRamp(ramp) => Arc::new(Item::<Gradient>::from(ramp)),
|
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::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
|
// AUTO-GENERATED VARIANTS
|
||||||
// =======================
|
// =======================
|
||||||
@@ -295,6 +306,7 @@ macro_rules! tagged_value {
|
|||||||
Self::BoxCorners(_) => item!(BoxCorners),
|
Self::BoxCorners(_) => item!(BoxCorners),
|
||||||
Self::GradientRamp(_) => item!(Gradient),
|
Self::GradientRamp(_) => item!(Gradient),
|
||||||
Self::BrushStrokes(_) => item!(BrushTrace),
|
Self::BrushStrokes(_) => item!(BrushTrace),
|
||||||
|
Self::Strokes(_) => list!(Stroke),
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// 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::<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::<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::<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
|
// 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::<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::<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::<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
|
// 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::<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::<$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::<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
|
// Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time
|
||||||
macro_rules! check_bare {
|
macro_rules! check_bare {
|
||||||
($type_default:ty) => {
|
($type_default:ty) => {
|
||||||
@@ -423,6 +438,9 @@ macro_rules! tagged_value {
|
|||||||
if **element == concrete!(f64) {
|
if **element == concrete!(f64) {
|
||||||
return Some(TaggedValue::F64Array(Vec::new()));
|
return Some(TaggedValue::F64Array(Vec::new()));
|
||||||
}
|
}
|
||||||
|
if **element == concrete!(Stroke) {
|
||||||
|
return Some(TaggedValue::Strokes(Vec::new()));
|
||||||
|
}
|
||||||
macro_rules! check {
|
macro_rules! check {
|
||||||
($type_default:ty) => {
|
($type_default:ty) => {
|
||||||
if **element == concrete!($type_default) { return Some(TaggedValue::TypeDefault(input.clone())); }
|
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::BoxCorners(values) => format!("BoxCorners({values:?})"),
|
||||||
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
|
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
|
||||||
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
|
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
|
||||||
|
Self::Strokes(strokes) => format!("Strokes({strokes:?})"),
|
||||||
// =======================
|
// =======================
|
||||||
// AUTO-GENERATED VARIANTS
|
// 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.
|
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ids,
|
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 graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||||
use graphene_std::animation::RealTimeMode;
|
use graphene_std::animation::RealTimeMode;
|
||||||
use graphene_std::any::DynAnyNode;
|
use graphene_std::any::DynAnyNode;
|
||||||
|
use graphene_std::brush::Stroke;
|
||||||
use graphene_std::brush::brush_stroke::BrushTrace;
|
use graphene_std::brush::brush_stroke::BrushTrace;
|
||||||
use graphene_std::extract_xy::XY;
|
use graphene_std::extract_xy::XY;
|
||||||
use graphene_std::gradient::Gradient;
|
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 => 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 => 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 => Item<BrushTrace>]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<graphene_std::brush::Stroke>]),
|
||||||
// Context nullification
|
// Context nullification
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Item<&PlatformEditorApi>, Context => Item<graphene_std::ContextFeatures>]),
|
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")]
|
#[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 => 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 => 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<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<&wgpu_executor::WgpuExecutor>]),
|
||||||
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<Option<&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,
|
RelativeAbsolute,
|
||||||
SelectiveColorChoice,
|
SelectiveColorChoice,
|
||||||
BrushTrace,
|
BrushTrace,
|
||||||
|
Stroke,
|
||||||
XY,
|
XY,
|
||||||
ScaleType,
|
ScaleType,
|
||||||
ReferencePoint,
|
ReferencePoint,
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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";
|
pub const ATTR_JOIN_MITER_LIMIT: &str = "join_miter_limit";
|
||||||
/// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item<Cover>` inside a `Coverage`.
|
/// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
pub const ATTR_ALIGN: &str = "align";
|
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.`).
|
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
|
||||||
pub const ATTR_FONT_SIZE: &str = "font_size";
|
pub const ATTR_FONT_SIZE: &str = "font_size";
|
||||||
/// Text item's font, as a `Resource` of the loaded font file.
|
/// Text item's font, as a `Resource` of the loaded font file.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ wasm = [
|
|||||||
# Local dependencies
|
# Local dependencies
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
graphene-hash = { workspace = true }
|
graphene-hash = { workspace = true }
|
||||||
|
brush-types = { workspace = true }
|
||||||
raster-types = { workspace = true, features = ["wgpu"] }
|
raster-types = { workspace = true, features = ["wgpu"] }
|
||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
node-macro = { workspace = true }
|
node-macro = { workspace = true }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::appearance::{Appearance, Cover, Coverage};
|
use crate::appearance::{Appearance, Cover, Coverage};
|
||||||
|
use brush_types::Stroke;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
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};
|
||||||
@@ -34,6 +35,7 @@ pub enum Graphic {
|
|||||||
ColorList(List<Color>),
|
ColorList(List<Color>),
|
||||||
GradientList(List<Gradient>),
|
GradientList(List<Gradient>),
|
||||||
TextList(List<String>),
|
TextList(List<String>),
|
||||||
|
StrokeList(List<Stroke>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Graphic {
|
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 {
|
impl From<String> for Graphic {
|
||||||
fn from(text: String) -> Self {
|
fn from(text: String) -> Self {
|
||||||
Graphic::Text(Item::new_from_element(text))
|
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::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::StrokeList(list) => bake_list_transform(list, transform),
|
||||||
// A color has no spatial extent, so there is no placement for a transform to move
|
// A color has no spatial extent, so there is no placement for a transform to move
|
||||||
Graphic::None(_) | Graphic::NoneList(_) | Graphic::Color(_) | Graphic::ColorList(_) => {}
|
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)
|
// 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 {
|
pub trait IntoGraphicList: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static {
|
||||||
fn into_graphic_list(self) -> List<Graphic>;
|
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> {
|
impl IntoGraphicList for List<String> {
|
||||||
fn into_graphic_list(self) -> List<Graphic> {
|
fn into_graphic_list(self) -> List<Graphic> {
|
||||||
List::new_from_element(Graphic::TextList(self))
|
List::new_from_element(Graphic::TextList(self))
|
||||||
@@ -542,6 +574,7 @@ impl Graphic {
|
|||||||
Graphic::ColorList(list) => all_clipped(list),
|
Graphic::ColorList(list) => all_clipped(list),
|
||||||
Graphic::GradientList(list) => all_clipped(list),
|
Graphic::GradientList(list) => all_clipped(list),
|
||||||
Graphic::TextList(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::Text(_) | Graphic::TextList(_) => false,
|
||||||
|
Graphic::StrokeList(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,6 +661,7 @@ impl Graphic {
|
|||||||
Graphic::RasterCPUList(list) => every_item_has_zero_opacity(list),
|
Graphic::RasterCPUList(list) => every_item_has_zero_opacity(list),
|
||||||
Graphic::RasterGPUList(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::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::RasterCPUList(list) => list.is_empty(),
|
||||||
Graphic::RasterGPUList(list) => list.is_empty(),
|
Graphic::RasterGPUList(list) => list.is_empty(),
|
||||||
Graphic::TextList(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::ColorList(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::GradientList(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::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::ColorList(color) => color.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::GradientList(gradient) => gradient.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::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::ColorList(list) => list.render_complexity(),
|
||||||
Self::GradientList(list) => list.render_complexity(),
|
Self::GradientList(list) => list.render_complexity(),
|
||||||
Self::TextList(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]
|
[features]
|
||||||
default = ["serde"]
|
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]
|
[dependencies]
|
||||||
# Local dependencies
|
# Local dependencies
|
||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
|
brush-types = { workspace = true }
|
||||||
graphene-hash = { workspace = true }
|
graphene-hash = { workspace = true }
|
||||||
graphene-resource = { workspace = true }
|
graphene-resource = { workspace = true }
|
||||||
text-nodes = { workspace = true }
|
text-nodes = { workspace = true }
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ impl RenderExt for Graphic {
|
|||||||
| Graphic::RasterGPUList(_)
|
| Graphic::RasterGPUList(_)
|
||||||
| Graphic::GraphicList(_)
|
| Graphic::GraphicList(_)
|
||||||
| Graphic::GradientList(_)
|
| Graphic::GradientList(_)
|
||||||
| Graphic::TextList(_) => {
|
| Graphic::TextList(_)
|
||||||
|
| Graphic::StrokeList(_) => {
|
||||||
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. };
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ impl Render for Graphic {
|
|||||||
Graphic::ColorList(list) => list.render_svg(render, render_params),
|
Graphic::ColorList(list) => list.render_svg(render, render_params),
|
||||||
Graphic::GradientList(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::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::ColorList(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||||
Graphic::GradientList(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::TextList(list) => list.render_to_vello(scene, transform, context, render_params),
|
||||||
|
Graphic::StrokeList(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,6 +1165,14 @@ impl Render for Graphic {
|
|||||||
Graphic::TextList(list) => {
|
Graphic::TextList(list) => {
|
||||||
metadata.upstream_footprints.insert(element_id, footprint);
|
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
|
// TODO: Find a way to handle more than the first item
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0));
|
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::ColorList(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::GradientList(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::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::ColorList(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::GradientList(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::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::ColorList(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::GradientList(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::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::RasterCPUList(_)
|
||||||
| Graphic::RasterGPUList(_)
|
| Graphic::RasterGPUList(_)
|
||||||
| Graphic::GraphicList(_)
|
| Graphic::GraphicList(_)
|
||||||
| Graphic::TextList(_) => {
|
| Graphic::TextList(_)
|
||||||
|
| Graphic::StrokeList(_) => {
|
||||||
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
|
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);
|
paint.render_to_vello(scene, multiplied_transform, context, paint_render_params);
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
@@ -1928,7 +1942,8 @@ fn render_vector_item_to_vello(
|
|||||||
| Graphic::RasterCPUList(_)
|
| Graphic::RasterCPUList(_)
|
||||||
| Graphic::RasterGPUList(_)
|
| Graphic::RasterGPUList(_)
|
||||||
| Graphic::GraphicList(_)
|
| Graphic::GraphicList(_)
|
||||||
| Graphic::TextList(_) => {
|
| Graphic::TextList(_)
|
||||||
|
| Graphic::StrokeList(_) => {
|
||||||
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
|
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);
|
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
|
// Since colors and gradients are technically infinitely big, we have to implement
|
||||||
// workarounds for rendering them correctly in a way which still allows us
|
// workarounds for rendering them correctly in a way which still allows us
|
||||||
// to cache the intermediate render data (SVG string/Vello scene).
|
// 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]
|
[dependencies]
|
||||||
# Local dependencies
|
# Local dependencies
|
||||||
dyn-any = { workspace = true }
|
dyn-any = { workspace = true }
|
||||||
|
brush-types = { workspace = true }
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
graphene-hash = { workspace = true }
|
graphene-hash = { workspace = true }
|
||||||
|
graphic-types = { workspace = true }
|
||||||
raster-types = { workspace = true }
|
raster-types = { workspace = true }
|
||||||
raster-nodes = { workspace = true }
|
raster-nodes = { workspace = true }
|
||||||
node-macro = { 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;
|
pub mod brush;
|
||||||
mod brush_cache;
|
mod brush_cache;
|
||||||
pub mod brush_stroke;
|
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 {
|
pub mod migrations {
|
||||||
use crate::brush_stroke::BrushStroke;
|
use crate::brush_stroke::BrushStroke;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ authors.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# Local dependencies
|
# Local dependencies
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
|
brush-types = { workspace = true }
|
||||||
graphic-types = { workspace = true }
|
graphic-types = { workspace = true }
|
||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
raster-types = { workspace = true }
|
raster-types = { workspace = true }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use brush_types::Stroke;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
|
use core_types::list::{AttributeValueDyn, Item, List, ListDyn, NodeIdPath};
|
||||||
use core_types::registry::types::{Angle, SeedValue, SignedInteger};
|
use core_types::registry::types::{Angle, SeedValue, SignedInteger};
|
||||||
@@ -870,6 +871,7 @@ pub async fn extend<T: 'n + Send + Clone>(
|
|||||||
List<Color>,
|
List<Color>,
|
||||||
List<Gradient>,
|
List<Gradient>,
|
||||||
List<Artboard>,
|
List<Artboard>,
|
||||||
|
List<Stroke>,
|
||||||
)]
|
)]
|
||||||
base: List<T>,
|
base: List<T>,
|
||||||
/// The list whose items will appear at the end of the extended list.
|
/// 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<Color>,
|
||||||
List<Gradient>,
|
List<Gradient>,
|
||||||
List<Artboard>,
|
List<Artboard>,
|
||||||
|
List<Stroke>,
|
||||||
)]
|
)]
|
||||||
new: List<T>,
|
new: List<T>,
|
||||||
) -> List<T> {
|
) -> List<T> {
|
||||||
@@ -942,6 +945,7 @@ pub async fn into_group<T: Into<Graphic> + 'n>(
|
|||||||
List<String>,
|
List<String>,
|
||||||
List<DVec2>,
|
List<DVec2>,
|
||||||
Item<DAffine2>, // TODO: Remove this
|
Item<DAffine2>, // TODO: Remove this
|
||||||
|
List<Stroke>,
|
||||||
)]
|
)]
|
||||||
content: T,
|
content: T,
|
||||||
) -> Item<Graphic> {
|
) -> 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
|
// 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::None(_) | Graphic::NoneList(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Color(_) | Graphic::Gradient(_) => Vec::new(),
|
||||||
Graphic::RasterCPUList(_) | Graphic::RasterGPUList(_) | Graphic::ColorList(_) | Graphic::GradientList(_) => 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
|
// Normalized to GraphicList above
|
||||||
Graphic::Graphic(_) => Vec::new(),
|
Graphic::Graphic(_) => Vec::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user