Skip to content

Commit 76b95fb

Browse files
merge: brush-stroke-types + master
2 parents 7498f3f + 618b4c1 commit 76b95fb

18 files changed

Lines changed: 317 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ no-std-types = { path = "node-graph/libraries/no-std-types" }
8080
raster-types = { path = "node-graph/libraries/raster-types" }
8181
vector-types = { path = "node-graph/libraries/vector-types" }
8282
graphic-types = { path = "node-graph/libraries/graphic-types" }
83+
brush-types = { path = "node-graph/libraries/brush-types" }
8384
rendering = { path = "node-graph/libraries/rendering" }
8485
brush-nodes = { path = "node-graph/nodes/brush" }
8586
blending-nodes = { path = "node-graph/nodes/blending" }

editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,15 @@ impl TableItemLayout for Artboard {
492492
}
493493
}
494494

495+
impl TableItemLayout for graphene_std::brush::Stroke {
496+
fn type_name() -> &'static str {
497+
"Stroke"
498+
}
499+
fn identifier(&self) -> String {
500+
format!("Stroke ({} {})", self.len(), if self.len() == 1 { "sample" } else { "samples" })
501+
}
502+
}
503+
495504
impl TableItemLayout for DashPattern {
496505
fn type_name() -> &'static str {
497506
"DashPattern"
@@ -605,6 +614,7 @@ impl TableItemLayout for Graphic {
605614
Self::ColorList(list) => list.identifier(),
606615
Self::GradientList(list) => list.identifier(),
607616
Self::TextList(list) => list.identifier(),
617+
Self::StrokeList(list) => list.identifier(),
608618
}
609619
}
610620
// Don't put a breadcrumb for Graphic
@@ -629,6 +639,7 @@ impl TableItemLayout for Graphic {
629639
Self::ColorList(list) => list.layout_with_breadcrumb(data),
630640
Self::GradientList(list) => list.layout_with_breadcrumb(data),
631641
Self::TextList(list) => list.layout_with_breadcrumb(data),
642+
Self::StrokeList(list) => list.layout_with_breadcrumb(data),
632643
}
633644
}
634645
}

node-graph/graph-craft/src/document/value.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::DocumentNode;
22
use crate::application_io::PlatformEditorApi;
33
use crate::application_io::resource::Resource;
44
use crate::proto::{Any as DAny, FutureAny};
5+
use brush_nodes::Stroke;
56
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
67
use core_types::color::SRGBA8;
78
use core_types::list::{Item, List, NodeIdPath};
@@ -97,6 +98,7 @@ macro_rules! tagged_value {
9798
#[serde(deserialize_with = "brush_nodes::migrations::migrate_to_brush_strokes")] // TODO: Eventually remove this document upgrade code
9899
#[serde(alias = "BrushStrokeTable")]
99100
BrushStrokes(Vec<BrushStroke>),
101+
Strokes(Vec<Stroke>),
100102
// =======================
101103
// AUTO-GENERATED VARIANTS
102104
// =======================
@@ -140,6 +142,7 @@ macro_rules! tagged_value {
140142
Self::BoxCorners(values) => values.cache_hash(state),
141143
Self::GradientRamp(ramp) => ramp.cache_hash(state),
142144
Self::BrushStrokes(strokes) => strokes.cache_hash(state),
145+
Self::Strokes(strokes) => strokes.cache_hash(state),
143146
// =======================
144147
// NON-SERIALIZED VARIANTS
145148
// =======================
@@ -203,6 +206,10 @@ macro_rules! tagged_value {
203206
Self::BoxCorners(values) => Box::new(Item::new_from_element(BoxCorners::from(values))),
204207
Self::GradientRamp(ramp) => Box::new(Item::<Gradient>::from(ramp)),
205208
Self::BrushStrokes(strokes) => Box::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
209+
Self::Strokes(strokes) => {
210+
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
211+
Box::new(list)
212+
}
206213
// =======================
207214
// AUTO-GENERATED VARIANTS
208215
// =======================
@@ -266,6 +273,10 @@ macro_rules! tagged_value {
266273
Self::BoxCorners(values) => Arc::new(Item::new_from_element(BoxCorners::from(values))),
267274
Self::GradientRamp(ramp) => Arc::new(Item::<Gradient>::from(ramp)),
268275
Self::BrushStrokes(strokes) => Arc::new(core_types::list::Item::new_from_element(BrushTrace::from(strokes))),
276+
Self::Strokes(strokes) => {
277+
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
278+
Arc::new(list)
279+
}
269280
// =======================
270281
// AUTO-GENERATED VARIANTS
271282
// =======================
@@ -295,6 +306,7 @@ macro_rules! tagged_value {
295306
Self::BoxCorners(_) => item!(BoxCorners),
296307
Self::GradientRamp(_) => item!(Gradient),
297308
Self::BrushStrokes(_) => item!(BrushTrace),
309+
Self::Strokes(_) => list!(Stroke),
298310
// =======================
299311
// AUTO-GENERATED VARIANTS
300312
// =======================
@@ -335,6 +347,7 @@ macro_rules! tagged_value {
335347
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(&*downcast::<Item<Gradient>>(input).unwrap()))),
336348
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
337349
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(downcast::<Item<BrushTrace>>(input).unwrap().into_element().0.iter_element_values().cloned().collect())),
350+
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(downcast::<List<Stroke>>(input).unwrap().into_iter().map(Item::into_element).collect())),
338351
// =======================
339352
// AUTO-GENERATED VARIANTS
340353
// =======================
@@ -369,6 +382,7 @@ macro_rules! tagged_value {
369382
x if x == TypeId::of::<Item<Gradient>>() => Ok(TaggedValue::GradientRamp(GradientRamp::from(input.downcast_ref::<Item<Gradient>>().unwrap()))),
370383
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Vec<BrushStroke>>().unwrap().clone())),
371384
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Item<BrushTrace>>().unwrap().element().0.iter_element_values().cloned().collect())),
385+
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(input.downcast_ref::<List<Stroke>>().unwrap().iter_element_values().cloned().collect())),
372386
// =======================
373387
// AUTO-GENERATED VARIANTS
374388
// =======================
@@ -398,6 +412,7 @@ macro_rules! tagged_value {
398412
if name == std::any::type_name::<BoxCorners>() { return Some(TaggedValue::BoxCorners(Vec::new())) }
399413
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
400414
if name == std::any::type_name::<BrushTrace>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
415+
if name == std::any::type_name::<List<Stroke>>() { return Some(TaggedValue::Strokes(Vec::new())) }
401416
// Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time
402417
macro_rules! check_bare {
403418
($type_default:ty) => {
@@ -451,6 +466,7 @@ macro_rules! tagged_value {
451466
Self::BoxCorners(values) => format!("BoxCorners({values:?})"),
452467
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
453468
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
469+
Self::Strokes(strokes) => format!("Strokes({strokes:?})"),
454470
// =======================
455471
// AUTO-GENERATED VARIANTS
456472
// =======================

node-graph/interpreted-executor/src/node_registry.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use graph_craft::document::value::RenderOutput;
66
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
77
use graphene_std::animation::RealTimeMode;
88
use graphene_std::any::DynAnyNode;
9+
use graphene_std::brush::Stroke;
910
use graphene_std::brush::brush_stroke::BrushTrace;
1011
use graphene_std::extract_xy::XY;
1112
use graphene_std::gradient::Gradient;
@@ -82,6 +83,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
8283
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<AttributeValueDyn>]),
8384
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ListDyn]),
8485
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
86+
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<graphene_std::brush::Stroke>]),
8587
// Context nullification
8688
#[cfg(feature = "gpu")]
8789
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
145147
#[cfg(feature = "gpu")]
146148
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
147149
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<BrushTrace>]),
150+
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::brush::Stroke>]),
148151
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<RenderIntermediate>]),
149152
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Item<&wgpu_executor::WgpuExecutor>]),
150153
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
353356
RelativeAbsolute,
354357
SelectiveColorChoice,
355358
BrushTrace,
359+
Stroke,
356360
XY,
357361
ScaleType,
358362
ReferencePoint,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "brush-types"
3+
version = "0.1.0"
4+
edition = "2024"
5+
description = "The brush stroke data format for Graphene"
6+
authors = ["Graphite Authors <contact@graphite.art>"]
7+
license = "MIT OR Apache-2.0"
8+
9+
[features]
10+
default = ["serde"]
11+
serde = ["dep:serde", "core-types/serde"]
12+
13+
[dependencies]
14+
# Local dependencies
15+
core-types = { workspace = true }
16+
graphene-hash = { workspace = true }
17+
18+
# Workspace dependencies
19+
dyn-any = { workspace = true }
20+
glam = { workspace = true }
21+
22+
# Optional workspace dependencies
23+
serde = { workspace = true, optional = true }
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use core_types::CacheHash;
2+
use core_types::bounds::{BoundingBox, RenderBoundingBox};
3+
use core_types::render_complexity::RenderComplexity;
4+
use dyn_any::DynAny;
5+
use glam::{DAffine2, DVec2, Vec2};
6+
use std::f32::consts::{PI, TAU};
7+
8+
#[derive(Clone, Debug, PartialEq, CacheHash)]
9+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10+
pub enum Channel<T> {
11+
Uniform(T),
12+
Samples(Vec<T>),
13+
}
14+
15+
impl<T: Copy> Channel<T> {
16+
pub fn get(&self, index: usize) -> T {
17+
match self {
18+
Self::Uniform(value) => *value,
19+
Self::Samples(values) => values[index],
20+
}
21+
}
22+
23+
fn len(&self) -> Option<usize> {
24+
match self {
25+
Self::Uniform(_) => None,
26+
Self::Samples(values) => Some(values.len()),
27+
}
28+
}
29+
}
30+
31+
unsafe impl<T: dyn_any::StaticTypeSized> dyn_any::StaticType for Channel<T> {
32+
type Static = Channel<T::Static>;
33+
}
34+
35+
#[derive(Clone, Debug, PartialEq, CacheHash, DynAny)]
36+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37+
pub struct Stroke {
38+
pub position: Vec<DVec2>,
39+
pub pressure: Channel<f32>,
40+
pub tilt: Channel<Vec2>,
41+
pub twist: Channel<f32>,
42+
pub time: Channel<f64>,
43+
pub seed: u64,
44+
}
45+
46+
impl Default for Stroke {
47+
fn default() -> Self {
48+
Self {
49+
position: Vec::new(),
50+
pressure: Channel::Uniform(1.),
51+
tilt: Channel::Uniform(Vec2::ZERO),
52+
twist: Channel::Uniform(0.),
53+
time: Channel::Uniform(0.),
54+
seed: 0,
55+
}
56+
}
57+
}
58+
59+
impl Stroke {
60+
pub fn len(&self) -> usize {
61+
self.position.len()
62+
}
63+
64+
pub fn is_empty(&self) -> bool {
65+
self.position.is_empty()
66+
}
67+
68+
pub fn is_valid(&self) -> bool {
69+
let n = self.len();
70+
[self.pressure.len(), self.tilt.len(), self.twist.len(), self.time.len()].into_iter().flatten().all(|len| len == n)
71+
}
72+
73+
pub fn sample(&self, index: usize) -> Sample {
74+
Sample {
75+
position: self.position[index],
76+
pressure: self.pressure.get(index),
77+
tilt: self.tilt.get(index),
78+
twist: self.twist.get(index),
79+
time: self.time.get(index),
80+
}
81+
}
82+
83+
pub fn sample_lerp(&self, index: usize, t: f32) -> Sample {
84+
let a = self.sample(index);
85+
let b = self.sample(index + 1);
86+
Sample {
87+
position: a.position.lerp(b.position, t as f64),
88+
pressure: a.pressure + (b.pressure - a.pressure) * t,
89+
tilt: a.tilt.lerp(b.tilt, t),
90+
twist: {
91+
let delta = (b.twist - a.twist).rem_euclid(TAU);
92+
let delta = if delta > PI { delta - TAU } else { delta };
93+
a.twist + delta * t
94+
},
95+
time: a.time + (b.time - a.time) * t as f64,
96+
}
97+
}
98+
99+
pub fn samples(&self) -> impl Iterator<Item = Sample> + '_ {
100+
(0..self.len()).map(|index| self.sample(index))
101+
}
102+
}
103+
104+
impl BoundingBox for Stroke {
105+
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
106+
let Some(first) = self.position.first() else { return RenderBoundingBox::None };
107+
let (min, max) = self.position.iter().fold((*first, *first), |(min, max), &point| (min.min(point), max.max(point)));
108+
let corners = [min, DVec2::new(max.x, min.y), max, DVec2::new(min.x, max.y)].map(|corner| transform.transform_point2(corner));
109+
let (min, max) = corners.iter().fold((corners[0], corners[0]), |(min, max), &point| (min.min(point), max.max(point)));
110+
RenderBoundingBox::Rectangle([min, max])
111+
}
112+
113+
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
114+
self.bounding_box(transform, include_stroke)
115+
}
116+
}
117+
118+
impl RenderComplexity for Stroke {
119+
fn render_complexity(&self) -> usize {
120+
self.len()
121+
}
122+
}
123+
124+
#[derive(Clone, Copy, Debug, PartialEq)]
125+
pub struct Sample {
126+
pub position: DVec2,
127+
pub pressure: f32,
128+
pub tilt: Vec2,
129+
pub twist: f32,
130+
pub time: f64,
131+
}

node-graph/libraries/core-types/src/list.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ pub const ATTR_JOIN: &str = "join";
9797
pub const ATTR_JOIN_MITER_LIMIT: &str = "join_miter_limit";
9898
/// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item<Cover>` inside a `Coverage`.
9999
pub const ATTR_ALIGN: &str = "align";
100+
/// Brush stroke item's `Color` its strokes are painted with.
101+
pub const ATTR_COLOR: &str = "color";
102+
/// Brush stroke item's tip diameter in document-space units (`f64`).
103+
pub const ATTR_DIAMETER: &str = "diameter";
104+
/// Brush stroke item's edge hardness from `0.` (softest) to `1.` (hardest) (`f64`).
105+
pub const ATTR_HARDNESS: &str = "hardness";
106+
/// Brush stroke item's per-pass paint coverage from `0.` to `1.` (`f64`).
107+
pub const ATTR_FLOW: &str = "flow";
100108
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
101109
pub const ATTR_FONT_SIZE: &str = "font_size";
102110
/// Text item's font, as a `Resource` of the loaded font file.

node-graph/libraries/graphic-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ wasm = [
2020
# Local dependencies
2121
core-types = { workspace = true }
2222
graphene-hash = { workspace = true }
23+
brush-types = { workspace = true }
2324
raster-types = { workspace = true, features = ["wgpu"] }
2425
vector-types = { workspace = true }
2526
node-macro = { workspace = true }

0 commit comments

Comments
 (0)