-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Brush stroke types #4467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Brush stroke types #4467
Changes from all commits
7d2cd8c
edeeec2
896cf2e
45356af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Calling (Based on your team's feedback about avoiding panics in application code.) [b5917bc3-2dbc-49a4-8246-5cad0a2e4976] Prompt for AI agents |
||
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a brush stroke has nonzero diameter, this bound covers only centerline sample points. Prompt for AI agents |
||
| 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, | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When a
List<Stroke>input needs a default,TaggedValue::from_type(&list!(Stroke))returnsNone, so new brush nodes receiveTaggedValue::Noneinstead of an empty stroke list. Add aStrokecase in theType::Listbranch returningTaggedValue::Strokes(Vec::new()).Prompt for AI agents