This commit is contained in:
mtvare6
2025-07-01 08:47:00 +05:30
226 changed files with 10047 additions and 17120 deletions

View File

@@ -2,8 +2,7 @@ use crate::{Ctx, ExtractAnimationTime, ExtractTime};
const DAY: f64 = 1000. * 3600. * 24.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
pub enum RealTimeMode {
#[label("UTC")]
Utc,

View File

@@ -1,308 +0,0 @@
use crate::text::FontCache;
use crate::transform::Footprint;
use crate::vector::style::ViewMode;
use alloc::sync::Arc;
use core::fmt::Debug;
use core::future::Future;
use core::hash::{Hash, Hasher};
use core::pin::Pin;
use core::ptr::addr_of;
use core::time::Duration;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, UVec2};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurfaceId(pub u64);
impl core::fmt::Display for SurfaceId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!("{}", self.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurfaceFrame {
pub surface_id: SurfaceId,
pub resolution: UVec2,
pub transform: DAffine2,
}
impl Hash for SurfaceFrame {
fn hash<H: Hasher>(&self, state: &mut H) {
self.surface_id.hash(state);
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
}
}
unsafe impl StaticType for SurfaceFrame {
type Static = SurfaceFrame;
}
pub trait Size {
fn size(&self) -> UVec2;
}
#[cfg(target_arch = "wasm32")]
impl Size for web_sys::HtmlCanvasElement {
fn size(&self) -> UVec2 {
UVec2::new(self.width(), self.height())
}
}
#[derive(Debug, Clone)]
pub struct ImageTexture {
#[cfg(feature = "wgpu")]
pub texture: Arc<wgpu::Texture>,
#[cfg(not(feature = "wgpu"))]
pub texture: (),
}
impl Hash for ImageTexture {
#[cfg(feature = "wgpu")]
fn hash<H: Hasher>(&self, state: &mut H) {
self.texture.hash(state);
}
#[cfg(not(feature = "wgpu"))]
fn hash<H: Hasher>(&self, _state: &mut H) {}
}
impl PartialEq for ImageTexture {
fn eq(&self, other: &Self) -> bool {
#[cfg(feature = "wgpu")]
{
self.texture == other.texture
}
#[cfg(not(feature = "wgpu"))]
{
self.texture == other.texture
}
}
}
unsafe impl StaticType for ImageTexture {
type Static = ImageTexture;
}
#[cfg(feature = "wgpu")]
impl Size for ImageTexture {
fn size(&self) -> UVec2 {
UVec2::new(self.texture.width(), self.texture.height())
}
}
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
fn from(x: SurfaceHandleFrame<S>) -> Self {
Self {
surface_id: x.surface_handle.window_id,
transform: x.transform,
resolution: x.surface_handle.surface.size(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SurfaceHandle<Surface> {
pub window_id: SurfaceId,
pub surface: Surface,
}
// #[cfg(target_arch = "wasm32")]
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
// #[cfg(target_arch = "wasm32")]
// unsafe impl<T: dyn_any::WasmNotSync> Sync for SurfaceHandle<T> {}
impl<S: Size> Size for SurfaceHandle<S> {
fn size(&self) -> UVec2 {
self.surface.size()
}
}
unsafe impl<T: 'static> StaticType for SurfaceHandle<T> {
type Static = SurfaceHandle<T>;
}
#[derive(Clone, Debug, PartialEq)]
pub struct SurfaceHandleFrame<Surface> {
pub surface_handle: Arc<SurfaceHandle<Surface>>,
pub transform: DAffine2,
}
unsafe impl<T: 'static> StaticType for SurfaceHandleFrame<T> {
type Static = SurfaceHandleFrame<T>;
}
// TODO: think about how to automatically clean up memory
/*
impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> {
fn drop(&mut self) {
self.application_io.destroy_surface(self.surface_id)
}
}*/
#[cfg(target_arch = "wasm32")]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
#[cfg(not(target_arch = "wasm32"))]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
pub trait ApplicationIo {
type Surface;
type Executor;
fn window(&self) -> Option<SurfaceHandle<Self::Surface>>;
fn create_window(&self) -> SurfaceHandle<Self::Surface>;
fn destroy_window(&self, surface_id: SurfaceId);
fn gpu_executor(&self) -> Option<&Self::Executor> {
None
}
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError>;
}
impl<T: ApplicationIo> ApplicationIo for &T {
type Surface = T::Surface;
type Executor = T::Executor;
fn window(&self) -> Option<SurfaceHandle<Self::Surface>> {
(**self).window()
}
fn create_window(&self) -> SurfaceHandle<T::Surface> {
(**self).create_window()
}
fn destroy_window(&self, surface_id: SurfaceId) {
(**self).destroy_window(surface_id)
}
fn gpu_executor(&self) -> Option<&T::Executor> {
(**self).gpu_executor()
}
fn load_resource<'a>(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
(**self).load_resource(url)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApplicationError {
NotFound,
InvalidUrl,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum NodeGraphUpdateMessage {
// ImaginateStatusUpdate,
}
pub trait NodeGraphUpdateSender {
fn send(&self, message: NodeGraphUpdateMessage);
}
impl<T: NodeGraphUpdateSender> NodeGraphUpdateSender for std::sync::Mutex<T> {
fn send(&self, message: NodeGraphUpdateMessage) {
self.lock().as_mut().unwrap().send(message)
}
}
pub trait GetEditorPreferences {
// fn hostname(&self) -> &str;
fn use_vello(&self) -> bool;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ExportFormat {
#[default]
Svg,
Png {
transparent: bool,
},
Jpeg,
Canvas,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct TimingInformation {
pub time: f64,
pub animation_time: Duration,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct RenderConfig {
pub viewport: Footprint,
pub export_format: ExportFormat,
pub time: TimingInformation,
pub view_mode: ViewMode,
pub hide_artboards: bool,
pub for_export: bool,
}
struct Logger;
impl NodeGraphUpdateSender for Logger {
fn send(&self, message: NodeGraphUpdateMessage) {
log::warn!("dispatching message with fallback node graph update sender {:?}", message);
}
}
struct DummyPreferences;
impl GetEditorPreferences for DummyPreferences {
// fn hostname(&self) -> &str {
// "dummy_endpoint"
// }
fn use_vello(&self) -> bool {
false
}
}
pub struct EditorApi<Io> {
/// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`].
pub font_cache: FontCache,
/// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web).
pub application_io: Option<Arc<Io>>,
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
/// Editor preferences made available to the graph through the [`WasmEditorApi`].
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
}
impl<Io> Eq for EditorApi<Io> {}
impl<Io: Default> Default for EditorApi<Io> {
fn default() -> Self {
Self {
font_cache: FontCache::default(),
application_io: None,
node_graph_message_sender: Box::new(Logger),
editor_preferences: Box::new(DummyPreferences),
}
}
}
impl<Io> Hash for EditorApi<Io> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_cache.hash(state);
self.application_io.as_ref().map_or(0, |io| io.as_ref() as *const _ as usize).hash(state);
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
}
}
impl<Io> PartialEq for EditorApi<Io> {
fn eq(&self, other: &Self) -> bool {
self.font_cache == other.font_cache
&& self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize)
&& std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _)
&& std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _)
}
}
impl<T> Debug for EditorApi<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("EditorApi").field("font_cache", &self.font_cache).finish()
}
}
unsafe impl<T: StaticTypeSized> StaticType for EditorApi<T> {
type Static = EditorApi<T::Static>;
}

View File

@@ -0,0 +1,240 @@
use dyn_any::DynAny;
use std::hash::Hash;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct AlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f32| (x * 1e3).round() / 1e3;
write!(
f,
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
self.blend_mode,
round(self.opacity * 100.),
round(self.fill * 100.),
if self.clip { "Yes" } else { "No" }
)
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
AlphaBlending {
opacity: lerp(self.opacity, other.opacity, t),
fill: lerp(self.fill, other.fill, t),
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
clip: if t < 0.5 { self.clip } else { other.clip },
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
#[repr(i32)]
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}

View File

@@ -0,0 +1,175 @@
use crate::raster::Image;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable};
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}

View File

@@ -0,0 +1,24 @@
use crate::Color;
use glam::{DAffine2, DVec2};
pub trait BoundingBox {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>;
}
macro_rules! none_impl {
($t:path) => {
impl BoundingBox for $t {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
None
}
}
};
}
none_impl!(String);
none_impl!(bool);
none_impl!(f32);
none_impl!(f64);
none_impl!(DVec2);
none_impl!(Option<Color>);
none_impl!(Vec<Color>);

View File

@@ -1,18 +1,16 @@
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use bytemuck::{Pod, Zeroable};
use core::hash::Hash;
use dyn_any::DynAny;
use half::f16;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::Euclid;
#[cfg(feature = "serde")]
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
use std::hash::Hash;
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
pub struct RGBA16F {
red: f16,
green: f16,
@@ -84,8 +82,7 @@ impl Alpha for RGBA16F {
impl Pixel for RGBA16F {}
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct SRGBA8 {
red: u8,
green: u8,
@@ -165,8 +162,7 @@ impl Alpha for SRGBA8 {
impl Pixel for SRGBA8 {}
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct Luma(pub f32);
impl Luminance for Luma {
@@ -206,8 +202,7 @@ impl Pixel for Luma {}
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct Color {
red: f32,
green: f32,
@@ -217,7 +212,7 @@ pub struct Color {
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for Color {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.red.to_bits().hash(state);
self.green.to_bits().hash(state);
self.blue.to_bits().hash(state);
@@ -350,7 +345,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
///
@@ -388,7 +383,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
/// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
/// assert_eq!(color, color2)
@@ -403,7 +398,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
/// ```
#[inline(always)]
@@ -421,7 +416,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
/// ```
pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
@@ -463,7 +458,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.r() == 0.114);
/// ```
@@ -476,7 +471,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.g() == 0.103);
/// ```
@@ -489,7 +484,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.b() == 0.98);
/// ```
@@ -502,7 +497,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.a() == 0.97);
/// ```
@@ -778,7 +773,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97));
/// ```
@@ -791,7 +786,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@@ -808,7 +803,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@@ -818,7 +813,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@@ -830,7 +825,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// // TODO: Add test
/// ```
@@ -845,7 +840,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
/// ```
pub fn to_hsla(&self) -> [f32; 4] {
@@ -881,7 +876,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba_str("7C67FA61").unwrap();
/// ```
pub fn from_rgba_str(color_str: &str) -> Option<Color> {
@@ -899,7 +894,7 @@ impl Color {
/// Creates a color from a 6-character RGB hex string (without a # prefix).
///
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgb_str("7C67FA").unwrap();
/// ```
pub fn from_rgb_str(color_str: &str) -> Option<Color> {

View File

@@ -0,0 +1,205 @@
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
pub use crate::blending::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}

View File

@@ -69,7 +69,7 @@ pub fn float_to_srgb_u8(mut f: f32) -> u8 {
// We clamped f to [0, 1], and the integer representations
// of the positive finite non-NaN floats are monotonic.
// This makes the later LUT lookup panicless.
unsafe { core::hint::unreachable_unchecked() }
unsafe { std::hint::unreachable_unchecked() }
}
// Compute a piecewise linear interpolation that is always

View File

@@ -0,0 +1,7 @@
mod color;
mod color_traits;
mod discrete_srgb;
pub use color::*;
pub use color_traits::*;
pub use discrete_srgb::*;

View File

@@ -1,7 +1,7 @@
use crate::transform::Footprint;
use core::any::Any;
use core::borrow::Borrow;
use core::panic::Location;
use std::any::Any;
use std::borrow::Borrow;
use std::panic::Location;
use std::sync::Arc;
pub trait Ctx: Clone + Send {}
@@ -240,7 +240,7 @@ type DynBox = Box<dyn Any + Send + Sync>;
#[derive(dyn_any::DynAny)]
pub struct OwnedContextImpl {
footprint: Option<crate::transform::Footprint>,
footprint: Option<Footprint>,
varargs: Option<Arc<[DynBox]>>,
parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
// This could be converted into a single enum to save extra bytes
@@ -249,8 +249,8 @@ pub struct OwnedContextImpl {
animation_time: Option<f64>,
}
impl core::fmt::Debug for OwnedContextImpl {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
impl std::fmt::Debug for OwnedContextImpl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedContextImpl")
.field("footprint", &self.footprint)
.field("varargs", &self.varargs)
@@ -269,8 +269,8 @@ impl Default for OwnedContextImpl {
}
}
impl core::hash::Hash for OwnedContextImpl {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for OwnedContextImpl {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.footprint.hash(state);
self.varargs.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
self.parent.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
@@ -348,7 +348,7 @@ impl OwnedContextImpl {
#[derive(Default, Clone, Copy, dyn_any::DynAny)]
pub struct ContextImpl<'a> {
pub(crate) footprint: Option<&'a crate::transform::Footprint>,
pub(crate) footprint: Option<&'a Footprint>,
varargs: Option<&'a [DynRef<'a>]>,
// This could be converted into a single enum to save extra bytes
index: Option<usize>,

View File

@@ -0,0 +1,26 @@
use crate::raster_types::{CPU, RasterDataTable};
use crate::{Color, Ctx};
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
ty.size()
}
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
value.clone()
}

View File

@@ -0,0 +1,21 @@
use crate::Ctx;
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
/// Obtain the X or Y component of a coordinate.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
match axis {
XY::X => vector.into().x,
XY::Y => vector.into().y,
}
}
/// The X or Y component of a coordinate.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum XY {
#[default]
X,
Y,
}

View File

@@ -1,5 +1,5 @@
use crate::Node;
use core::marker::PhantomData;
use std::marker::PhantomData;
#[derive(Clone)]
pub struct FnNode<T: Fn(I) -> O, I, O>(T, PhantomData<(I, O)>);

View File

@@ -0,0 +1,248 @@
use crate::Color;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(pub Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
self.0.iter().for_each(|(position, color)| {
position.to_bits().hash(state);
color.hash(state);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops {
pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() {
return Color::BLACK;
}
if t <= self.0[0].0 {
return self.0[0].1;
}
if t >= self.0[self.0.len() - 1].0 {
return self.0[self.0.len() - 1].1;
}
for i in 0..self.0.len() - 1 {
let (t1, c1) = self.0[i];
let (t2, c2) = self.0[i + 1];
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
return c1.lerp(&c2, normalized_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl std::hash::Hash for Gradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.0
.iter()
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let stops = self
.stops
.0
.iter()
.zip(other.stops.0.iter())
.map(|((a_pos, a_color), (b_pos, b_color))| {
let position = a_pos + (b_pos - a_pos) * time;
let color = a_color.lerp(b_color, time as f32);
(position, color)
})
.collect::<Vec<_>>();
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
// Lerp between the nearest colors if applicable
(a, Some(b)) => a.lerp(
&b,
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
),
// Use the start or the end color if applicable
(v, _) => v,
};
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
}

View File

@@ -1,5 +1,7 @@
use crate::blending::AlphaBlending;
use crate::bounds::BoundingBox;
use crate::instances::{Instance, Instances};
use crate::raster::BlendMode;
use crate::math::quad::Quad;
use crate::raster::image::Image;
use crate::raster_types::{CPU, GPU, Raster, RasterDataTable};
use crate::transform::TransformMut;
@@ -7,82 +9,20 @@ use crate::uuid::NodeId;
use crate::vector::{VectorData, VectorDataTable};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use dyn_any::DynAny;
use glam::{DAffine2, IVec2};
use glam::{DAffine2, DVec2, IVec2};
use std::hash::Hash;
pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[serde(default)]
pub struct AlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl core::hash::Hash for AlphaBlending {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f32| (x * 1e3).round() / 1e3;
write!(
f,
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
self.blend_mode,
round(self.opacity * 100.),
round(self.fill * 100.),
if self.clip { "Yes" } else { "No" }
)
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
AlphaBlending {
opacity: lerp(self.opacity, other.opacity, t),
fill: lerp(self.fill, other.fill, t),
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
clip: if t < 0.5 { self.clip } else { other.clip },
}
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GraphicGroupTable, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
pub struct OldGraphicGroup {
elements: Vec<(GraphicElement, Option<NodeId>)>,
transform: DAffine2,
alpha_blending: AlphaBlending,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
pub struct GraphicGroup {
elements: Vec<(GraphicElement, Option<NodeId>)>,
}
@@ -164,8 +104,7 @@ impl From<RasterDataTable<GPU>> for GraphicGroupTable {
}
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroupTable),
@@ -245,6 +184,25 @@ impl GraphicElement {
}
}
impl BoundingBox for GraphicElement {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
match self {
GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform, include_stroke),
GraphicElement::RasterDataCPU(raster) => raster.bounding_box(transform, include_stroke),
GraphicElement::RasterDataGPU(raster) => raster.bounding_box(transform, include_stroke),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform, include_stroke),
}
}
}
impl BoundingBox for GraphicGroupTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter_map(|element| element.instance.bounding_box(transform * *element.transform, include_stroke))
.reduce(Quad::combine_bounds)
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -281,8 +239,7 @@ impl serde::Serialize for Raster<GPU> {
}
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct Artboard {
pub graphic_group: GraphicGroupTable,
pub label: String,
@@ -311,12 +268,25 @@ impl Artboard {
}
}
impl BoundingBox for Artboard {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
Some(artboard_bounds)
} else {
[self.graphic_group.bounding_box(transform, include_stroke), Some(artboard_bounds)]
.into_iter()
.flatten()
.reduce(Quad::combine_bounds)
}
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<ArtboardGroupTable, D::Error> {
use serde::Deserialize;
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct ArtboardGroup {
pub artboards: Vec<(Artboard, Option<NodeId>)>,
}
@@ -348,8 +318,21 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
pub type ArtboardGroupTable = Instances<Artboard>;
impl BoundingBox for ArtboardGroupTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter_map(|instance| instance.instance.bounding_box(transform, include_stroke))
.reduce(Quad::combine_bounds)
}
}
#[node_macro::node(category(""))]
async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable {
async fn layer<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] mut stack: Instances<I>,
#[implementations(GraphicElement, VectorData, Raster<CPU>, Raster<GPU>)] element: I,
node_path: Vec<NodeId>,
) -> Instances<I> {
// Get the penultimate element of the node path, or None if the path is too short
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
@@ -432,7 +415,7 @@ async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: boo
output
}
#[node_macro::node(category("General"))]
#[node_macro::node(category("Vector"))]
async fn flatten_vector(_: impl Ctx, group: GraphicGroupTable) -> VectorDataTable {
// TODO: Avoid mutable reference, instead return a new GraphicGroupTable?
fn flatten_group(output_group_table: &mut VectorDataTable, current_group_table: GraphicGroupTable) {
@@ -571,3 +554,7 @@ impl From<GraphicGroupTable> for GraphicElement {
GraphicElement::GraphicGroup(graphic_group)
}
}
pub trait ToGraphicElement {
fn to_graphic_element(&self) -> GraphicElement;
}

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,7 @@ impl<T> Instances<T> {
})
}
pub fn instance_ref_iter(&self) -> impl DoubleEndedIterator<Item = InstanceRef<T>> + Clone {
pub fn instance_ref_iter(&self) -> impl DoubleEndedIterator<Item = InstanceRef<'_, T>> + Clone {
self.instance
.iter()
.zip(self.mask.iter())
@@ -78,7 +78,7 @@ impl<T> Instances<T> {
})
}
pub fn instance_mut_iter(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<T>> {
pub fn instance_mut_iter(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<'_, T>> {
self.instance
.iter_mut()
.zip(self.mask.iter_mut())
@@ -94,7 +94,7 @@ impl<T> Instances<T> {
})
}
pub fn get(&self, index: usize) -> Option<InstanceRef<T>> {
pub fn get(&self, index: usize) -> Option<InstanceRef<'_, T>> {
if index >= self.instance.len() {
return None;
}
@@ -108,7 +108,7 @@ impl<T> Instances<T> {
})
}
pub fn get_mut(&mut self, index: usize) -> Option<InstanceMut<T>> {
pub fn get_mut(&mut self, index: usize) -> Option<InstanceMut<'_, T>> {
if index >= self.instance.len() {
return None;
}
@@ -143,8 +143,8 @@ impl<T> Default for Instances<T> {
}
}
impl<T: Hash> core::hash::Hash for Instances<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl<T: Hash> Hash for Instances<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for instance in &self.instance {
instance.hash(state);
}
@@ -230,7 +230,7 @@ impl<T> Instance<T> {
}
}
pub fn to_instance_ref(&self) -> InstanceRef<T> {
pub fn to_instance_ref(&self) -> InstanceRef<'_, T> {
InstanceRef {
instance: &self.instance,
mask: &self.mask,
@@ -240,7 +240,7 @@ impl<T> Instance<T> {
}
}
pub fn to_instance_mut(&mut self) -> InstanceMut<T> {
pub fn to_instance_mut(&mut self) -> InstanceMut<'_, T> {
InstanceMut {
instance: &mut self.instance,
mask: &mut self.mask,

View File

@@ -1,46 +1,47 @@
extern crate alloc;
#[macro_use]
extern crate log;
pub use crate as graphene_core;
pub use num_traits;
pub use ctor;
pub mod animation;
pub mod blending;
pub mod blending_nodes;
pub mod bounds;
pub mod color;
pub mod consts;
pub mod context;
pub mod debug;
pub mod extract_xy;
pub mod generic;
pub mod gradient;
mod graphic_element;
pub mod instances;
pub mod logic;
pub mod math;
pub mod memo;
pub mod misc;
pub mod ops;
pub mod raster;
pub mod raster_types;
pub mod registry;
pub mod structural;
pub mod text;
pub mod transform;
pub mod transform_nodes;
pub mod uuid;
pub mod value;
pub mod memo;
pub mod raster;
pub mod transform;
mod graphic_element;
pub use graphic_element::*;
pub mod vector;
pub mod application_io;
pub mod registry;
pub use crate as graphene_core;
pub use blending::*;
pub use context::*;
use core::any::TypeId;
use core::future::Future;
use core::pin::Pin;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphic_element::*;
pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color;
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
pub use types::Cow;
// pub trait Node: for<'n> NodeIO<'n> {
@@ -54,11 +55,11 @@ pub trait Node<'i, Input> {
fn reset(&self) {}
/// Returns the name of the node for diagnostic purposes.
fn node_name(&self) -> &'static str {
core::any::type_name::<Self>()
std::any::type_name::<Self>()
}
/// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes.
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
log::warn!("Node::serialize not implemented for {}", core::any::type_name::<Self>());
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
log::warn!("Node::serialize not implemented for {}", std::any::type_name::<Self>());
None
}
}
@@ -75,13 +76,13 @@ where
TypeId::of::<Input::Static>()
}
fn input_type_name(&self) -> &'static str {
core::any::type_name::<Input>()
std::any::type_name::<Input>()
}
fn output_type(&self) -> core::any::TypeId {
fn output_type(&self) -> TypeId {
TypeId::of::<<Self::Output as StaticTypeSized>::Static>()
}
fn output_type_name(&self) -> &'static str {
core::any::type_name::<Self::Output>()
std::any::type_name::<Self::Output>()
}
fn to_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes {
NodeIOTypes {
@@ -122,7 +123,7 @@ impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for Box<
(**self).eval(input)
}
}
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for alloc::sync::Arc<N> {
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for std::sync::Arc<N> {
type Output = O;
fn eval(&'i self, input: I) -> O {
(**self).eval(input)
@@ -142,13 +143,7 @@ impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)>
}
}
pub use crate::application_io::{SurfaceFrame, SurfaceId};
#[cfg(feature = "wasm")]
pub type WasmSurfaceHandle = application_io::SurfaceHandle<web_sys::HtmlCanvasElement>;
#[cfg(feature = "wasm")]
pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + core::fmt::Debug {
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug {
fn get_input(&'a self, index: usize) -> Option<&'a T>;
fn set_input(&'a mut self, index: usize, value: T);
}
@@ -169,3 +164,12 @@ pub trait NodeInputDecleration {
fn identifier() -> &'static str;
type Result;
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}

View File

@@ -2,16 +2,15 @@ use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))]
fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
#[cfg(not(target_arch = "spirv"))]
#[node_macro::node(category("Debug"), name("Log to Console"))]
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("{:#?}", value);
value
}
#[node_macro::node(category("Text"))]
fn to_string<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
format!("{:?}", value)
}
@@ -38,7 +37,7 @@ fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usiz
string.len()
}
#[node_macro::node(category("Text"))]
#[node_macro::node(category("Math: Logic"))]
async fn switch<T, C: Send + 'n + Clone>(
#[implementations(Context)] ctx: C,
condition: bool,

View File

@@ -1,8 +1,7 @@
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
#[derive(Clone, DynAny)]
#[derive(Clone, Debug, DynAny)]
pub struct AxisAlignedBbox {
pub start: DVec2,
pub end: DVec2,
@@ -60,8 +59,7 @@ impl From<(DVec2, DVec2)> for AxisAlignedBbox {
}
}
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Bbox {
pub top_left: DVec2,
pub top_right: DVec2,

View File

@@ -0,0 +1,25 @@
use crate::math::quad::Quad;
use crate::math::rect::Rect;
use bezier_rs::Bezier;
pub trait QuadExt {
/// Get all the edges in the rect as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl QuadExt for Quad {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}
pub trait RectExt {
/// Get all the edges in the quad as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl RectExt for Rect {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}

View File

@@ -0,0 +1,4 @@
pub mod bbox;
pub mod math_ext;
pub mod quad;
pub mod rect;

View File

@@ -58,11 +58,6 @@ impl Quad {
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
}
/// Get all the edges in the quad as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Generates the axis aligned bounding box of the quad
pub fn bounding_box(&self) -> [DVec2; 2] {
[
@@ -143,7 +138,7 @@ impl Quad {
}
}
impl core::ops::Mul<Quad> for DAffine2 {
impl std::ops::Mul<Quad> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Quad) -> Self::Output {

View File

@@ -1,4 +1,4 @@
use super::Quad;
use crate::math::quad::Quad;
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy, PartialEq)]
@@ -43,11 +43,6 @@ impl Rect {
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
}
/// Get all the edges in the rect as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Gets the center of a rect
#[must_use]
pub fn center(&self) -> DVec2 {
@@ -97,21 +92,21 @@ impl Rect {
}
}
impl core::ops::Mul<Rect> for DAffine2 {
type Output = super::Quad;
impl std::ops::Mul<Rect> for DAffine2 {
type Output = Quad;
fn mul(self, rhs: Rect) -> Self::Output {
self * super::Quad::from_box(rhs.0)
self * Quad::from_box(rhs.0)
}
}
impl core::ops::Index<usize> for Rect {
impl std::ops::Index<usize> for Rect {
type Output = DVec2;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl core::ops::IndexMut<usize> for Rect {
impl std::ops::IndexMut<usize> for Rect {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}

View File

@@ -1,9 +1,9 @@
use crate::{Node, WasmNotSend};
use alloc::sync::Arc;
use core::future::Future;
use core::ops::Deref;
use dyn_any::DynFuture;
use std::future::Future;
use std::hash::DefaultHasher;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::Mutex;
/// Caches the output of a given Node and acts as a proxy
@@ -15,7 +15,7 @@ pub struct MemoNode<T, CachedNode> {
impl<'i, I: Hash + 'i, T: 'i + Clone + WasmNotSend, CachedNode: 'i> Node<'i, I> for MemoNode<T, CachedNode>
where
CachedNode: for<'any_input> Node<'any_input, I>,
for<'a> <CachedNode as Node<'a, I>>::Output: core::future::Future<Output = T> + WasmNotSend,
for<'a> <CachedNode as Node<'a, I>>::Output: Future<Output = T> + WasmNotSend,
{
// TODO: This should return a reference to the cached cached_value
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
@@ -63,7 +63,7 @@ pub struct ImpureMemoNode<I, T, CachedNode> {
impl<'i, I: 'i, T: 'i + Clone + WasmNotSend, CachedNode: 'i> Node<'i, I> for ImpureMemoNode<I, T, CachedNode>
where
CachedNode: for<'any_input> Node<'any_input, I>,
for<'a> <CachedNode as Node<'a, I>>::Output: core::future::Future<Output = T> + WasmNotSend,
for<'a> <CachedNode as Node<'a, I>>::Output: Future<Output = T> + WasmNotSend,
{
// TODO: This should return a reference to the cached cached_value
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
@@ -93,7 +93,7 @@ impl<T, I, CachedNode> ImpureMemoNode<I, T, CachedNode> {
ImpureMemoNode {
cache: Default::default(),
node,
_phantom: core::marker::PhantomData,
_phantom: std::marker::PhantomData,
}
}
}
@@ -130,9 +130,9 @@ where
})
}
fn serialize(&self) -> Option<Arc<dyn core::any::Any + Send + Sync>> {
fn serialize(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let io = self.io.lock().unwrap();
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any + Send + Sync>)
(io).as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
}
}
@@ -142,14 +142,13 @@ impl<I, T, N> MonitorNode<I, T, N> {
}
}
use core::hash::{Hash, Hasher};
use std::hash::{Hash, Hasher};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct MemoHash<T: Hash> {
hash: u64,
value: T,
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -159,7 +158,6 @@ impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHas
}
}
#[cfg(feature = "serde")]
impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -179,12 +177,12 @@ impl<T: Hash> MemoHash<T> {
}
fn calc_hash(data: &T) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
hasher.finish()
}
pub fn inner_mut(&mut self) -> MemoHashGuard<T> {
pub fn inner_mut(&mut self) -> MemoHashGuard<'_, T> {
MemoHashGuard { inner: self }
}
pub fn into_inner(self) -> T {
@@ -206,7 +204,7 @@ impl<T: Hash> Hash for MemoHash<T> {
}
}
impl<T: Hash> core::ops::Deref for MemoHash<T> {
impl<T: Hash> Deref for MemoHash<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -218,14 +216,14 @@ pub struct MemoHashGuard<'a, T: Hash> {
inner: &'a mut MemoHash<T>,
}
impl<T: Hash> core::ops::Drop for MemoHashGuard<'_, T> {
impl<T: Hash> Drop for MemoHashGuard<'_, T> {
fn drop(&mut self) {
let hash = MemoHash::<T>::calc_hash(&self.inner.value);
self.inner.hash = hash;
}
}
impl<T: Hash> core::ops::Deref for MemoHashGuard<'_, T> {
impl<T: Hash> Deref for MemoHashGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -233,7 +231,7 @@ impl<T: Hash> core::ops::Deref for MemoHashGuard<'_, T> {
}
}
impl<T: Hash> core::ops::DerefMut for MemoHashGuard<'_, T> {
impl<T: Hash> std::ops::DerefMut for MemoHashGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner.value
}

View File

@@ -1,546 +1,5 @@
use crate::Ctx;
use crate::raster::BlendMode;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::{Fraction, Percentage};
use crate::vector::style::GradientStops;
use crate::{Color, Node};
use core::marker::PhantomData;
use core::ops::{Add, Div, Mul, Rem, Sub};
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
use math_parser::ast;
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
use math_parser::value::{Number, Value};
use num_traits::Pow;
use rand::{Rng, SeedableRng};
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
/// The struct that stores the context for the maths parser.
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
struct MathNodeContext {
a: f64,
b: f64,
}
impl ValueProvider for MathNodeContext {
fn get_value(&self, name: &str) -> Option<Value> {
if name.eq_ignore_ascii_case("a") {
Some(Value::from_f64(self.a))
} else if name.eq_ignore_ascii_case("b") {
Some(Value::from_f64(self.b))
} else {
None
}
}
}
/// Calculates a mathematical expression with input values "A" and "B"
#[node_macro::node(category("General"), properties("math_properties"))]
fn math<U: num_traits::float::Float>(
_: impl Ctx,
/// The value of "A" when calculating the expression
#[implementations(f64, f32)]
operand_a: U,
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
#[default(A + B)]
expression: String,
/// The value of "B" when calculating the expression
#[implementations(f64, f32)]
#[default(1.)]
operand_b: U,
) -> U {
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) {
Ok(expr) => expr,
Err(e) => {
warn!("Invalid expression: `{expression}`\n{e:?}");
return U::from(0.).unwrap();
}
};
let context = EvalContext::new(
MathNodeContext {
a: operand_a.to_f64().unwrap(),
b: operand_b.to_f64().unwrap(),
},
NothingMap,
);
let value = match node.eval(&context) {
Ok(value) => value,
Err(e) => {
warn!("Expression evaluation error: {e:?}");
return U::from(0.).unwrap();
}
};
let Value::Number(num) = value;
match num {
Number::Real(val) => U::from(val).unwrap(),
Number::Complex(c) => U::from(c.re).unwrap(),
}
}
/// The addition operation (+) calculates the sum of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] augend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] addend: T,
) -> <U as Add<T>>::Output {
augend + addend
}
/// The subtraction operation (-) calculates the difference between two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] minuend: U,
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] subtrahend: T,
) -> <U as Sub<T>>::Output {
minuend - subtrahend
}
/// The multiplication operation (×) calculates the product of two numbers.
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] multiplier: U,
#[default(1.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)]
multiplicand: T,
) -> <U as Mul<T>>::Output {
multiplier * multiplicand
}
/// The division operation (÷) calculates the quotient of two numbers.
///
/// Produces 0 if the denominator is 0.
#[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
_: impl Ctx,
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)] numerator: U,
#[default(1.)]
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)]
denominator: T,
) -> <U as Div<T>>::Output
where
<U as Div<T>>::Output: Default,
{
if denominator == T::default() {
return <U as Div<T>>::Output::default();
}
numerator / denominator
}
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
#[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
modulus: T,
always_positive: bool,
) -> <U as Rem<T>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
/// The exponent operation (^) calculates the result of raising a number to a power.
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<U: Pow<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32)] base: U,
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32)]
power: T,
) -> <U as num_traits::Pow<T>>::Output {
base.pow(power)
}
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
#[node_macro::node(category("Math: Arithmetic"))]
fn root<U: num_traits::float::Float>(
_: impl Ctx,
#[default(2.)]
#[implementations(f64, f32)]
radicand: U,
#[default(2.)]
#[implementations(f64, f32)]
degree: U,
) -> U {
if degree == U::from(2.).unwrap() {
radicand.sqrt()
} else if degree == U::from(3.).unwrap() {
radicand.cbrt()
} else {
radicand.powf(U::from(1.).unwrap() / degree)
}
}
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<U: num_traits::float::Float>(
_: impl Ctx,
#[implementations(f64, f32)] value: U,
#[default(2.)]
#[implementations(f64, f32)]
base: U,
) -> U {
if base == U::from(2.).unwrap() {
value.log2()
} else if base == U::from(10.).unwrap() {
value.log10()
} else if base - U::from(std::f64::consts::E).unwrap() < U::epsilon() * U::from(1e6).unwrap() {
value.ln()
} else {
value.log(base)
}
}
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn sine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.sin() } else { theta.to_radians().sin() }
}
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn cosine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.cos() } else { theta.to_radians().cos() }
}
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))]
fn tangent<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
if radians { theta.tan() } else { theta.to_radians().tan() }
}
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn sine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
if radians { value.asin() } else { value.asin().to_degrees() }
}
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
#[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
if radians { value.acos() } else { value.acos().to_degrees() }
}
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified coordinate.
#[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<U: TangentInverse>(_: impl Ctx, #[implementations(f64, f32, DVec2)] value: U, radians: bool) -> U::Output {
value.atan(radians)
}
pub trait TangentInverse {
type Output: num_traits::float::Float;
fn atan(self, radians: bool) -> Self::Output;
}
impl TangentInverse for f32 {
type Output = f32;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for f64 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.atan() } else { self.atan().to_degrees() }
}
}
impl TangentInverse for glam::DVec2 {
type Output = f64;
fn atan(self, radians: bool) -> Self::Output {
if radians { self.y.atan2(self.x) } else { self.y.atan2(self.x).to_degrees() }
}
}
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn random<U: num_traits::float::Float>(
_: impl Ctx,
_primary: (),
seed: u64,
#[implementations(f64, f32)]
#[default(0.)]
min: U,
#[implementations(f64, f32)]
#[default(1.)]
max: U,
) -> f64 {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let result = rng.random::<f64>();
let (min, max) = if min < max { (min, max) } else { (max, min) };
let (min, max) = (min.to_f64().unwrap(), max.to_f64().unwrap());
result * (max - min) + min
}
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u32"), category("Math: Numeric"))]
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
value.to_u32().unwrap()
}
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To u64"), category("Math: Numeric"))]
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
value.to_u64().unwrap()
}
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
value.to_f64().unwrap()
}
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))]
fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.round()
}
/// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn floor<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.floor()
}
/// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn ceiling<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.ceil()
}
/// The absolute value function (abs) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))]
fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
value.abs()
}
/// The minimum function (min) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
if value > other_value { value } else { other_value }
}
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn clamp<T: core::cmp::PartialOrd>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T,
) -> T {
let (min, max) = if min < max { (min, max) } else { (max, min) };
if value < min {
min
} else if value > max {
max
} else {
value
}
}
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<U: core::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
) -> bool {
other_value == value
}
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<U: core::cmp::PartialEq<T>, T>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
) -> bool {
other_value != value
}
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn less_than<T: core::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
or_equal: bool,
) -> bool {
if or_equal { value <= other_value } else { value < other_value }
}
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
#[node_macro::node(category("Math: Logic"))]
fn greater_than<T: core::cmp::PartialOrd<T>>(
_: impl Ctx,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
or_equal: bool,
) -> bool {
if or_equal { value >= other_value } else { value > other_value }
}
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_or(_: impl Ctx, value: bool, other_value: bool) -> bool {
value || other_value
}
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_and(_: impl Ctx, value: bool, other_value: bool) -> bool {
value && other_value
}
/// The logical not operation (!) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))]
fn logical_not(_: impl Ctx, input: bool) -> bool {
!input
}
/// Constructs a bool value which may be set to true or false.
#[node_macro::node(category("Value"))]
fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool {
bool_value
}
/// Constructs a number value which may be set to any real number.
#[node_macro::node(category("Value"))]
fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 {
number
}
/// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider.
#[node_macro::node(category("Value"))]
fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
percentage
}
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
#[node_macro::node(category("Value"))]
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
DVec2::new(x, y)
}
/// Constructs a color value which may be set to any color, or no color.
#[node_macro::node(category("Value"))]
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
color
}
// // Aims for interoperable compatibility with:
// // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
// #[node_macro::node(category("Raster: Adjustment"))]
// async fn gradient_map<T: Adjust<Color>>(
// _: impl Ctx,
// #[implementations(
// Color,
// RasterDataTable<CPU>,
// GradientStops,
// )]
// mut image: T,
// gradient: GradientStops,
// reverse: bool,
// ) -> T {
// image.adjust(|color| {
// let intensity = color.luminance_srgb();
// let intensity = if reverse { 1. - intensity } else { intensity };
// gradient.evaluate(intensity as f64)
// });
// image
// }
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("General"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color {
let position = position.clamp(0., 1.);
gradient.evaluate(position)
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
gradient
}
/// Constructs a blend mode choice value which may be set to any of the available blend modes in order to tell another node which blending operation to use.
#[node_macro::node(category("Value"))]
fn blend_mode_value(_: impl Ctx, _primary: (), blend_mode: BlendMode) -> BlendMode {
blend_mode
}
/// Constructs a string value which may be set to any plain text.
#[node_macro::node(category("Value"))]
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
string
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
ty.size()
}
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Meant for debugging purposes, not general use. Clones the input value.
#[node_macro::node(category("Debug"))]
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
value.clone()
}
#[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
vector_a.dot(vector_b)
}
/// Obtain the X or Y component of a coordinate.
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
match axis {
XY::X => vector.into().x,
XY::Y => vector.into().y,
}
}
/// The X or Y component of a coordinate.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[widget(Dropdown)]
pub enum XY {
#[default]
X,
Y,
}
use crate::Node;
use std::marker::PhantomData;
// TODO: Rename to "Passthrough"
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
@@ -566,7 +25,7 @@ where
self.0.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.0.serialize()
}
}
@@ -586,7 +45,7 @@ impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Nod
pub struct IntoNode<O>(PhantomData<O>);
impl<O> IntoNode<O> {
pub const fn new() -> Self {
Self(core::marker::PhantomData)
Self(PhantomData)
}
}
impl<O> Default for IntoNode<O> {
@@ -598,7 +57,7 @@ impl<'input, I: 'input, O: 'input> Node<'input, I> for IntoNode<O>
where
I: Into<O> + Sync + Send,
{
type Output = ::dyn_any::DynFuture<'input, O>;
type Output = dyn_any::DynFuture<'input, O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
@@ -606,75 +65,82 @@ where
}
}
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
pub trait Convert<T>: Sized {
/// Converts this type into the (usually inferred) output type.
#[must_use]
fn convert(self) -> T;
}
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
macro_rules! impl_convert {
($from:ty,$to:ty) => {
impl Convert<$to> for $from {
fn convert(self) -> $to {
self as $to
}
}
};
($to:ty) => {
impl_convert!(f32, $to);
impl_convert!(f64, $to);
impl_convert!(i8, $to);
impl_convert!(u8, $to);
impl_convert!(u16, $to);
impl_convert!(i16, $to);
impl_convert!(i32, $to);
impl_convert!(u32, $to);
impl_convert!(i64, $to);
impl_convert!(u64, $to);
impl_convert!(i128, $to);
impl_convert!(u128, $to);
impl_convert!(isize, $to);
impl_convert!(usize, $to);
};
}
impl_convert!(f32);
impl_convert!(f64);
impl_convert!(i8);
impl_convert!(u8);
impl_convert!(u16);
impl_convert!(i16);
impl_convert!(i32);
impl_convert!(u32);
impl_convert!(i64);
impl_convert!(u64);
impl_convert!(i128);
impl_convert!(u128);
impl_convert!(isize);
impl_convert!(usize);
// Convert
pub struct ConvertNode<O>(PhantomData<O>);
impl<_O> ConvertNode<_O> {
pub const fn new() -> Self {
Self(core::marker::PhantomData)
}
}
impl<_O> Default for ConvertNode<_O> {
fn default() -> Self {
Self::new()
}
}
impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I> for ConvertNode<_O> {
type Output = ::dyn_any::DynFuture<'input, _O>;
#[inline]
fn eval(&'input self, input: I) -> Self::Output {
Box::pin(async move { input.convert() })
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::generic::*;
#[test]
pub fn dot_product_function() {
let vector_a = glam::DVec2::new(1., 2.);
let vector_b = glam::DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b), 11.);
}
#[test]
fn test_basic_expression() {
let result = math((), 0., "2 + 2".to_string(), 0.);
assert_eq!(result, 4.);
}
#[test]
fn test_complex_expression() {
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
assert_eq!(result, 20.);
}
#[test]
fn test_default_expression() {
let result = math((), 0., "0".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test]
fn test_invalid_expression() {
let result = math((), 0., "invalid".to_string(), 0.);
assert_eq!(result, 0.);
}
#[test]
pub fn identity_node() {
assert_eq!(identity(&4), &4);
}
#[test]
pub fn foo() {
let fnn = FnNode::new(|(a, b)| (b, a));
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
}
#[test]
pub fn add_vectors() {
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
}
#[test]
pub fn subtract_f64() {
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.);
}
#[test]
pub fn divide_vectors() {
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
}
#[test]
pub fn modulo_positive() {
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64);
}
#[test]
pub fn modulo_negative() {
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64);
}
}

View File

@@ -1,226 +1,25 @@
pub use self::color::{Color, Luma, SRGBA8};
use crate::Ctx;
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use bytemuck::{Pod, Zeroable};
use core::fmt::Debug;
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
/// as to not yet rename all references
pub mod color {
pub use super::*;
}
pub mod adjustments;
pub mod bbox;
#[cfg(not(target_arch = "spirv"))]
pub mod brush_cache;
pub mod color;
#[cfg(not(target_arch = "spirv"))]
pub mod curve;
pub mod discrete_srgb;
pub mod image;
pub use self::image::Image;
pub use adjustments::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: core::ops::Sub<Self, Output = Self>,
Self: core::ops::Mul<Self, Output = Self>,
Self: core::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
#[cfg(feature = "serde")]
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
#[cfg(feature = "serde")]
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
core::mem::size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}
pub trait Bitmap {
type Pixel: Pixel;
fn width(&self) -> u32;
@@ -286,112 +85,3 @@ impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
(*self).get_pixel_mut(x, y)
}
}
pub use self::image::Image;
pub mod image;
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}

View File

@@ -1,21 +1,18 @@
#![allow(clippy::too_many_arguments)]
use crate::GraphicElement;
use crate::blending::BlendMode;
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
use crate::raster::curve::{Curve, ValueMapperNode};
use crate::raster::image::Image;
use crate::raster::{Channel, Color, Pixel};
use crate::raster_types::{CPU, Raster, RasterDataTable};
use crate::registry::types::{Angle, Percentage, SignedPercentage};
use crate::vector::VectorDataTable;
use crate::vector::style::GradientStops;
use crate::{Ctx, Node};
use crate::{GraphicElement, GraphicGroupTable};
use core::cmp::Ordering;
use core::fmt::Debug;
use dyn_any::DynAny;
#[cfg(feature = "serde")]
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
use std::cmp::Ordering;
use std::fmt::Debug;
// TODO: Implement the following:
// Color Balance
@@ -32,8 +29,7 @@ use spirv_std::num_traits::float::Float;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum LuminanceCalculation {
#[default]
@@ -45,218 +41,6 @@ pub enum LuminanceCalculation {
MaximumChannels,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl core::fmt::Display for BlendMode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
#[cfg(feature = "vello")]
impl From<BlendMode> for vello::peniko::Mix {
fn from(val: BlendMode) -> Self {
match val {
// Normal group
BlendMode::Normal => vello::peniko::Mix::Normal,
// Darken group
BlendMode::Darken => vello::peniko::Mix::Darken,
BlendMode::Multiply => vello::peniko::Mix::Multiply,
BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn,
// Lighten group
BlendMode::Lighten => vello::peniko::Mix::Lighten,
BlendMode::Screen => vello::peniko::Mix::Screen,
BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge,
// Contrast group
BlendMode::Overlay => vello::peniko::Mix::Overlay,
BlendMode::SoftLight => vello::peniko::Mix::SoftLight,
BlendMode::HardLight => vello::peniko::Mix::HardLight,
// Inversion group
BlendMode::Difference => vello::peniko::Mix::Difference,
BlendMode::Exclusion => vello::peniko::Mix::Exclusion,
// Component group
BlendMode::Hue => vello::peniko::Mix::Hue,
BlendMode::Saturation => vello::peniko::Mix::Saturation,
BlendMode::Color => vello::peniko::Mix::Color,
BlendMode::Luminosity => vello::peniko::Mix::Luminosity,
_ => todo!(),
}
}
}
#[node_macro::node(category("Raster: Adjustment"))]
fn luminance<T: Adjust<Color>>(
_: impl Ctx,
@@ -281,7 +65,7 @@ fn luminance<T: Adjust<Color>>(
input
}
#[node_macro::node(category("Raster"))]
#[node_macro::node(category("Raster: Channels"))]
fn extract_channel<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@@ -304,7 +88,7 @@ fn extract_channel<T: Adjust<Color>>(
input
}
#[node_macro::node(category("Raster"))]
#[node_macro::node(category("Raster: Channels"))]
fn make_opaque<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
@@ -346,7 +130,7 @@ fn brightness_contrast<T: Adjust<Color>>(
let brightness = brightness as f32 / 255.;
let contrast = contrast as f32 / 100.;
let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
let contrast = if contrast > 0. { (contrast * std::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
let offset = brightness * contrast + brightness - contrast / 2.;
@@ -368,13 +152,13 @@ fn brightness_contrast<T: Adjust<Color>>(
y: [0., 130. + brightness * 51., 233. + brightness * 10., 255.].map(|x| x / 255.),
};
let brightness_curve_solutions = brightness_curve_points.solve();
let mut brightness_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let mut brightness_lut: [f32; WINDOW_SIZE] = std::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
brightness_curve_points.interpolate(x, &brightness_curve_solutions)
});
// Special handling for when brightness is negative
if brightness_is_negative {
brightness_lut = core::array::from_fn(|i| {
brightness_lut = std::array::from_fn(|i| {
let mut x = i;
while x > 1 && brightness_lut[x] > i as f32 / WINDOW_SIZE as f32 {
x -= 1;
@@ -393,7 +177,7 @@ fn brightness_contrast<T: Adjust<Color>>(
y: [0., 64. - contrast * 30., 192. + contrast * 30., 255.].map(|x| x / 255.),
};
let contrast_curve_solutions = contrast_curve_points.solve();
let contrast_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
let contrast_lut: [f32; WINDOW_SIZE] = std::array::from_fn(|i| {
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
contrast_curve_points.interpolate(x, &contrast_curve_solutions)
});
@@ -919,8 +703,7 @@ async fn vibrance<T: Adjust<Color>>(
}
/// Color Channel
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Radio)]
pub enum RedGreenBlue {
#[default]
@@ -930,8 +713,7 @@ pub enum RedGreenBlue {
}
/// Color Channel
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Radio)]
pub enum RedGreenBlueAlpha {
#[default]
@@ -942,8 +724,7 @@ pub enum RedGreenBlueAlpha {
}
/// Style of noise pattern
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum NoiseType {
#[default]
@@ -958,8 +739,7 @@ pub enum NoiseType {
WhiteNoise,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
/// Style of layered levels of the noise pattern
pub enum FractalType {
#[default]
@@ -975,8 +755,7 @@ pub enum FractalType {
}
/// Distance function used by the cellular noise
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
pub enum CellularDistanceFunction {
#[default]
Euclidean,
@@ -986,8 +765,7 @@ pub enum CellularDistanceFunction {
Hybrid,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
pub enum CellularReturnType {
CellValue,
#[default]
@@ -1006,8 +784,7 @@ pub enum CellularReturnType {
}
/// Type of domain warp
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum DomainWarpType {
#[default]
@@ -1033,6 +810,7 @@ async fn channel_mixer<T: Adjust<Color>>(
mut image: T,
monochrome: bool,
#[default(40.)]
#[name("Red")]
monochrome_r: f64,
@@ -1116,8 +894,7 @@ async fn channel_mixer<T: Adjust<Color>>(
image
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Radio)]
pub enum RelativeAbsolute {
#[default]
@@ -1126,8 +903,7 @@ pub enum RelativeAbsolute {
}
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
pub enum SelectiveColorChoice {
#[default]
Reds,
@@ -1158,43 +934,54 @@ async fn selective_color<T: Adjust<Color>>(
GradientStops,
)]
mut image: T,
mode: RelativeAbsolute,
#[name("(Reds) Cyan")] r_c: f64,
#[name("(Reds) Magenta")] r_m: f64,
#[name("(Reds) Yellow")] r_y: f64,
#[name("(Reds) Black")] r_k: f64,
#[name("(Yellows) Cyan")] y_c: f64,
#[name("(Yellows) Magenta")] y_m: f64,
#[name("(Yellows) Yellow")] y_y: f64,
#[name("(Yellows) Black")] y_k: f64,
#[name("(Greens) Cyan")] g_c: f64,
#[name("(Greens) Magenta")] g_m: f64,
#[name("(Greens) Yellow")] g_y: f64,
#[name("(Greens) Black")] g_k: f64,
#[name("(Cyans) Cyan")] c_c: f64,
#[name("(Cyans) Magenta")] c_m: f64,
#[name("(Cyans) Yellow")] c_y: f64,
#[name("(Cyans) Black")] c_k: f64,
#[name("(Blues) Cyan")] b_c: f64,
#[name("(Blues) Magenta")] b_m: f64,
#[name("(Blues) Yellow")] b_y: f64,
#[name("(Blues) Black")] b_k: f64,
#[name("(Magentas) Cyan")] m_c: f64,
#[name("(Magentas) Magenta")] m_m: f64,
#[name("(Magentas) Yellow")] m_y: f64,
#[name("(Magentas) Black")] m_k: f64,
#[name("(Whites) Cyan")] w_c: f64,
#[name("(Whites) Magenta")] w_m: f64,
#[name("(Whites) Yellow")] w_y: f64,
#[name("(Whites) Black")] w_k: f64,
#[name("(Neutrals) Cyan")] n_c: f64,
#[name("(Neutrals) Magenta")] n_m: f64,
#[name("(Neutrals) Yellow")] n_y: f64,
#[name("(Neutrals) Black")] n_k: f64,
#[name("(Blacks) Cyan")] k_c: f64,
#[name("(Blacks) Magenta")] k_m: f64,
#[name("(Blacks) Yellow")] k_y: f64,
#[name("(Blacks) Black")] k_k: f64,
_colors: SelectiveColorChoice,
) -> T {
image.adjust(|color| {
@@ -1274,70 +1061,6 @@ async fn selective_color<T: Adjust<Color>>(
image
}
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
//
@@ -1419,7 +1142,7 @@ fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve
anchor: [1.; 2],
handles: [curve.last_handle, [0.; 2]],
};
for sample in curve.manipulator_groups.iter().chain(core::iter::once(&end)) {
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
@@ -1501,22 +1224,10 @@ fn color_overlay<T: Adjust<Color>>(
#[cfg(test)]
mod test {
use crate::raster::adjustments::BlendMode;
use crate::Color;
use crate::blending::BlendMode;
use crate::raster::image::Image;
use crate::raster_types::{Raster, RasterDataTable};
use crate::{Color, Node};
use std::pin::Pin;
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, T: 'i + Clone + Send> Node<'i, ()> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: ()) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn color_overlay_multiply() {

View File

@@ -3,28 +3,27 @@ use crate::raster_types::CPU;
use crate::raster_types::Raster;
use crate::vector::brush_stroke::BrushStroke;
use crate::vector::brush_stroke::BrushStyle;
use core::hash::Hash;
use dyn_any::DynAny;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::Mutex;
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
struct BrushCacheImpl {
// The full previous input that was cached.
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
background: Instance<Raster<CPU>>,
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
blended_image: Instance<Raster<CPU>>,
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
last_stroke_texture: Instance<Raster<CPU>>,
// A cache for brush textures.
#[cfg_attr(feature = "serde", serde(skip))]
#[serde(skip)]
brush_texture_cache: HashMap<BrushStyle, Raster<CPU>>,
}
@@ -53,7 +52,7 @@ impl BrushCacheImpl {
// Take our previous blended image (and invalidate the cache).
// Since we're about to replace our cache anyway, this saves a clone.
background = core::mem::take(&mut self.blended_image);
background = std::mem::take(&mut self.blended_image);
// Check if the first non-blended stroke is an extension of the last one.
let mut first_stroke_texture = Instance {
@@ -70,7 +69,7 @@ impl BrushCacheImpl {
let new_points = strokes[0].compute_blit_points();
let is_point_prefix = new_points.get(..prev_points.len()) == Some(&prev_points);
if same_style && is_point_prefix {
first_stroke_texture = core::mem::take(&mut self.last_stroke_texture);
first_stroke_texture = std::mem::take(&mut self.last_stroke_texture);
first_stroke_point_skip = prev_points.len();
}
}
@@ -93,7 +92,7 @@ impl BrushCacheImpl {
impl Hash for BrushCacheImpl {
// Zero hash.
fn hash<H: core::hash::Hasher>(&self, _state: &mut H) {}
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}
#[derive(Clone, Debug, Default)]
@@ -104,8 +103,7 @@ pub struct BrushPlan {
pub first_stroke_point_skip: usize,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, DynAny)]
#[derive(Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushCache {
inner: Arc<Mutex<BrushCacheImpl>>,
proto: bool,
@@ -151,7 +149,7 @@ impl PartialEq for BrushCache {
}
impl Hash for BrushCache {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner.lock().unwrap().hash(state);
}
}

View File

@@ -1,10 +1,9 @@
use super::{Channel, Linear, LuminanceMut};
use crate::Node;
use core::ops::{Add, Mul, Sub};
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use std::ops::{Add, Mul, Sub};
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct Curve {
#[serde(rename = "manipulatorGroups")]
pub manipulator_groups: Vec<CurveManipulatorGroup>,
@@ -31,8 +30,7 @@ impl std::hash::Hash for Curve {
}
}
#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct CurveManipulatorGroup {
pub anchor: [f32; 2],
pub handles: [[f32; 2]; 2],
@@ -95,12 +93,7 @@ impl CubicSplines {
// Gaussian elimination: forward elimination
for row in 0..4 {
let pivot_row_index = (row..4)
.max_by(|&a_row, &b_row| {
augmented_matrix[a_row][row]
.abs()
.partial_cmp(&augmented_matrix[b_row][row].abs())
.unwrap_or(core::cmp::Ordering::Equal)
})
.max_by(|&a_row, &b_row| augmented_matrix[a_row][row].abs().partial_cmp(&augmented_matrix[b_row][row].abs()).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
// Swap the current row with the row that has the largest pivot element

View File

@@ -1,17 +1,13 @@
use crate::{
AlphaBlending,
instances::{Instance, Instances},
raster_types::Raster,
};
use super::Color;
use super::discrete_srgb::float_to_srgb_u8;
use alloc::vec::Vec;
use crate::AlphaBlending;
use crate::color::float_to_srgb_u8;
use crate::instances::{Instance, Instances};
use crate::raster_types::Raster;
use core::hash::{Hash, Hasher};
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use std::vec::Vec;
#[cfg(feature = "serde")]
mod base64_serde {
//! Basic wrapper for [`serde`] to perform [`base64`] encoding
@@ -40,23 +36,22 @@ mod base64_serde {
}
}
#[derive(Clone, PartialEq, Default, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, PartialEq, Default, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct Image<P: Pixel> {
pub width: u32,
pub height: u32,
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
pub data: Vec<P>,
/// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion
/// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation.
#[cfg_attr(feature = "serde", serde(skip))]
#[serde(skip)]
pub base64_string: Option<String>,
// TODO: Add an `origin` field to store where in the local space the image is anchored.
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
}
impl<P: Pixel + Debug> Debug for Image<P> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length = self.data.len();
f.debug_struct("Image")
.field("width", &self.width)
@@ -203,7 +198,7 @@ where
impl<P: Pixel> IntoIterator for Image<P> {
type Item = P;
type IntoIter = alloc::vec::IntoIter<P>;
type IntoIter = std::vec::IntoIter<P>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
@@ -233,8 +228,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
}
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroupTable),
@@ -243,8 +237,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
@@ -272,8 +265,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
@@ -299,7 +291,15 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
*image_frame_table.instance_mut_iter().next().unwrap().alpha_blending = alpha_blending;
image_frame_table
}
FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(image_frame.instance_ref_iter().next().unwrap().instance.image.clone())),
FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(
image_frame
.instance_ref_iter()
.next()
.unwrap_or(Instances::new(ImageFrame::default()).instance_ref_iter().next().unwrap())
.instance
.image
.clone(),
)),
FormatVersions::ImageFrameTable(image_frame_table) => RasterDataTable::new(Raster::new_cpu(image_frame_table.instance_ref_iter().next().unwrap().instance.clone())),
FormatVersions::RasterDataTable(raster_data_table) => raster_data_table,
})
@@ -329,8 +329,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
}
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum GraphicElement {
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroupTable),
@@ -339,8 +338,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
RasterFrame(RasterFrame),
}
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct ImageFrame<P: Pixel> {
pub image: Image<P>,
}
@@ -368,8 +366,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
type Static = ImageFrame<P::Static>;
}
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,

View File

@@ -1,8 +1,11 @@
use crate::Color;
use crate::bounds::BoundingBox;
use crate::instances::Instances;
use crate::math::quad::Quad;
use crate::raster::Image;
use core::ops::Deref;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg(feature = "wgpu")]
use std::sync::Arc;
@@ -11,18 +14,18 @@ pub struct CPU;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct GPU;
trait Storage {}
trait Storage: 'static {}
impl Storage for CPU {}
impl Storage for GPU {}
#[derive(Clone, Debug, Hash, PartialEq)]
#[allow(private_bounds)]
pub struct Raster<T: 'static + Storage> {
pub struct Raster<T: Storage> {
data: RasterStorage,
storage: T,
}
unsafe impl<T: 'static + Storage> dyn_any::StaticType for Raster<T> {
unsafe impl<T: Storage> dyn_any::StaticType for Raster<T> {
type Static = Raster<T>;
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
@@ -54,6 +57,10 @@ impl Raster<CPU> {
let RasterStorage::Cpu(cpu) = self.data else { unreachable!() };
cpu
}
pub fn is_empty(&self) -> bool {
let data = self.data();
data.height == 0 || data.width == 0
}
}
impl Default for Raster<CPU> {
fn default() -> Self {
@@ -90,6 +97,10 @@ impl Raster<GPU> {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu.clone()
}
pub fn is_empty(&self) -> bool {
let data = self.data();
data.width() == 0 || data.height() == 0
}
}
#[cfg(feature = "wgpu")]
impl Deref for Raster<GPU> {
@@ -100,3 +111,28 @@ impl Deref for Raster<GPU> {
}
}
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
// TODO: Make this not dupliated
impl BoundingBox for RasterDataTable<CPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
}
}
impl BoundingBox for RasterDataTable<GPU> {
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
.flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
}
}

View File

@@ -59,7 +59,7 @@ pub struct FieldMetadata {
pub unit: Option<&'static str>,
}
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
@@ -108,10 +108,10 @@ pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::ne
pub static NODE_METADATA: LazyLock<Mutex<HashMap<String, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
#[cfg(not(target_arch = "wasm32"))]
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n + Send>>;
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>;
#[cfg(target_arch = "wasm32")]
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
pub type LocalFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
pub type DynFuture<'n, T> = Pin<Box<dyn std::future::Future<Output = T> + 'n>>;
pub type LocalFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n>>;
#[cfg(not(target_arch = "wasm32"))]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
#[cfg(target_arch = "wasm32")]
@@ -169,8 +169,8 @@ impl Drop for NodeContainer {
}
}
impl core::fmt::Debug for NodeContainer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
impl std::fmt::Debug for NodeContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeContainer").finish()
}
}
@@ -184,7 +184,7 @@ impl NodeContainer {
#[cfg(feature = "dealloc_nodes")]
unsafe fn dealloc_unchecked(&mut self) {
unsafe {
std::mem::drop(Box::from_raw(self.node as *mut TypeErasedNode));
drop(Box::from_raw(self.node as *mut TypeErasedNode));
}
}
}
@@ -219,7 +219,7 @@ where
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.node.serialize()
}
}
@@ -227,8 +227,8 @@ impl<I, O> DowncastBothNode<I, O> {
pub const fn new(node: SharedNodeContainer) -> Self {
Self {
node,
_i: core::marker::PhantomData,
_o: core::marker::PhantomData,
_i: PhantomData,
_o: PhantomData,
}
}
}
@@ -252,7 +252,7 @@ where
}
#[inline(always)]
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.node.serialize()
}
}
@@ -271,14 +271,14 @@ pub struct DynAnyNode<I, O, Node> {
impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode<I, O, N>
where
I: 'input + dyn_any::StaticType + WasmNotSend,
O: 'input + dyn_any::StaticType + WasmNotSend,
I: 'input + StaticType + WasmNotSend,
O: 'input + StaticType + WasmNotSend,
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
{
type Output = FutureAny<'input>;
#[inline]
fn eval(&'input self, input: Any<'input>) -> Self::Output {
let node_name = core::any::type_name::<N>();
let node_name = std::any::type_name::<N>();
let output = |input| {
let result = self.node.eval(input);
async move { Box::new(result.await) as Any<'input> }
@@ -293,21 +293,21 @@ where
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.node.serialize()
}
}
impl<'input, I, O, N> DynAnyNode<I, O, N>
where
I: 'input + dyn_any::StaticType,
O: 'input + dyn_any::StaticType,
I: 'input + StaticType,
O: 'input + StaticType,
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
{
pub const fn new(node: N) -> Self {
Self {
node,
_i: core::marker::PhantomData,
_o: core::marker::PhantomData,
_i: PhantomData,
_o: PhantomData,
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::Node;
use core::marker::PhantomData;
use std::marker::PhantomData;
/// This is how we can generically define composition of two nodes.
/// This is done generically as shown: <https://files.keavon.com/-/SurprisedGaseousAnhinga/capture.png>
@@ -58,10 +58,10 @@ pub struct AsyncComposeNode<First, Second, I> {
impl<'i, Input: 'static, First, Second> Node<'i, Input> for AsyncComposeNode<First, Second, Input>
where
First: Node<'i, Input>,
First::Output: core::future::Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
First::Output: Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
type Output = core::pin::Pin<Box<dyn core::future::Future<Output = <Second as Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output>>::Output> + 'i>>;
type Output = std::pin::Pin<Box<dyn Future<Output = <Second as Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output>>::Output> + 'i>>;
fn eval(&'i self, input: Input) -> Self::Output {
Box::pin(async move {
let arg = self.first.eval(input).await;
@@ -73,8 +73,8 @@ where
impl<'i, First, Second, Input: 'i> AsyncComposeNode<First, Second, Input>
where
First: Node<'i, Input>,
First::Output: core::future::Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
First::Output: Future,
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
pub const fn new(first: First, second: Second) -> Self {
AsyncComposeNode::<First, Second, Input> { first, second, phantom: PhantomData }
@@ -97,8 +97,8 @@ pub trait AndThen<'i, Input: 'i>: Sized {
fn and_then<Second>(self, second: Second) -> AsyncComposeNode<Self, Second, Input>
where
Self: Node<'i, Input>,
Self::Output: core::future::Future,
Second: Node<'i, <<Self as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
Self::Output: Future,
Second: Node<'i, <<Self as Node<'i, Input>>::Output as Future>::Output> + 'i,
{
AsyncComposeNode::new(self, second)
}

View File

@@ -61,8 +61,8 @@ impl FontCache {
}
}
impl core::hash::Hash for FontCache {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for FontCache {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.preview_urls.len().hash(state);
self.preview_urls.iter().for_each(|(font, url)| {
font.hash(state);

View File

@@ -208,7 +208,7 @@ pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting:
bounds
}
pub fn load_face(data: &[u8]) -> rustybuzz::Face {
pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> {
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
}

View File

@@ -1,8 +1,6 @@
use crate::instances::Instances;
use crate::raster::bbox::AxisAlignedBbox;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Artboard, CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use crate::Artboard;
use crate::math::bbox::AxisAlignedBbox;
pub use crate::vector::ReferencePoint;
use core::f64;
use glam::{DAffine2, DMat2, DVec2};
@@ -67,8 +65,7 @@ impl TransformMut for Footprint {
}
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum RenderQuality {
/// Low quality, fast rendering
Preview,
@@ -81,8 +78,7 @@ pub enum RenderQuality {
/// Render at full quality
Full,
}
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Footprint {
/// Inverse of the transform which will be applied to the node output during the rendering process
pub transform: DAffine2,
@@ -136,8 +132,8 @@ impl From<()> for Footprint {
}
}
impl core::hash::Hash for Footprint {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for Footprint {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state));
self.resolution.hash(state)
}
@@ -154,186 +150,3 @@ impl<T: TransformMut> ApplyTransform for T {
impl ApplyTransform for () {
fn apply_transform(&mut self, &_modification: &DAffine2) {}
}
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target
}
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
*data_transform.transform = transform.transform();
}
data
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {
#[default]
None,
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl ReferencePoint {
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
let size = bounding_box.size();
let offset = match self {
ReferencePoint::None => return None,
ReferencePoint::TopLeft => DVec2::ZERO,
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
};
Some(bounding_box.start + offset)
}
}
impl From<&str> for ReferencePoint {
fn from(input: &str) -> Self {
match input {
"None" => ReferencePoint::None,
"TopLeft" => ReferencePoint::TopLeft,
"TopCenter" => ReferencePoint::TopCenter,
"TopRight" => ReferencePoint::TopRight,
"CenterLeft" => ReferencePoint::CenterLeft,
"Center" => ReferencePoint::Center,
"CenterRight" => ReferencePoint::CenterRight,
"BottomLeft" => ReferencePoint::BottomLeft,
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
}
}
}
impl From<ReferencePoint> for Option<DVec2> {
fn from(input: ReferencePoint) -> Self {
match input {
ReferencePoint::None => None,
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
}
}
}
impl From<DVec2> for ReferencePoint {
fn from(input: DVec2) -> Self {
const TOLERANCE: f64 = 1e-5_f64;
if input.y.abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::TopLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::TopCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::TopRight;
}
} else if (input.y - 0.5).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::CenterLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::Center;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::CenterRight;
}
} else if (input.y - 1.).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::BottomLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::BottomCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::BottomRight;
}
}
ReferencePoint::None
}
}

View File

@@ -0,0 +1,89 @@
use crate::instances::Instances;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::transform::{ApplyTransform, Footprint, Transform};
use crate::vector::VectorDataTable;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use core::f64;
use glam::{DAffine2, DVec2};
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target
}
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
*data_transform.transform = transform.transform();
}
data
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}

View File

@@ -1,4 +1,4 @@
use core::any::TypeId;
use std::any::TypeId;
pub use std::borrow::Cow;
@@ -6,20 +6,20 @@ pub use std::borrow::Cow;
macro_rules! concrete {
($type:ty) => {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(core::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(core::any::type_name::<$type>()),
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: None,
size: core::mem::size_of::<$type>(),
align: core::mem::align_of::<$type>(),
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
};
($type:ty, $name:ty) => {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(core::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(core::any::type_name::<$type>()),
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
alias: Some($crate::Cow::Borrowed(stringify!($name))),
size: core::mem::size_of::<$type>(),
align: core::mem::align_of::<$type>(),
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
};
}
@@ -28,11 +28,11 @@ macro_rules! concrete {
macro_rules! concrete_with_name {
($type:ty, $name:expr_2021) => {
$crate::Type::Concrete($crate::TypeDescriptor {
id: Some(core::any::TypeId::of::<$type>()),
id: Some(std::any::TypeId::of::<$type>()),
name: $crate::Cow::Borrowed($name),
alias: None,
size: core::mem::size_of::<$type>(),
align: core::mem::align_of::<$type>(),
size: std::mem::size_of::<$type>(),
align: std::mem::align_of::<$type>(),
})
};
}
@@ -114,8 +114,8 @@ impl NodeIOTypes {
}
}
impl core::fmt::Debug for NodeIOTypes {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
impl std::fmt::Debug for NodeIOTypes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"node({}) → {}",
[&self.call_argument].into_iter().chain(&self.inputs).map(|input| input.to_string()).collect::<Vec<_>>().join(", "),
@@ -124,8 +124,7 @@ impl core::fmt::Debug for NodeIOTypes {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct ProtoNodeIdentifier {
pub name: Cow<'static, str>,
}
@@ -141,7 +140,7 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
let name = String::deserialize(deserializer)?;
let name = match name.as_str() {
"f32" => "f64".to_string(),
"graphene_core::transform::Footprint" => "core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
"graphene_core::transform::Footprint" => "std::option::Option<std::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
"graphene_core::graphic_element::GraphicGroup" => "graphene_core::instances::Instances<graphene_core::graphic_element::GraphicGroup>".to_string(),
"graphene_core::vector::vector_data::VectorData" => "graphene_core::instances::Instances<graphene_core::vector::vector_data::VectorData>".to_string(),
"graphene_core::raster::image::ImageFrame<Color>"
@@ -156,10 +155,9 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
Ok(Cow::Owned(name))
}
#[derive(Clone, Debug, Eq, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Eq, specta::Type, serde::Serialize, serde::Deserialize)]
pub struct TypeDescriptor {
#[cfg_attr(feature = "serde", serde(skip))]
#[serde(skip)]
#[specta(skip)]
pub id: Option<TypeId>,
#[serde(deserialize_with = "migrate_type_descriptor_names")]
@@ -172,8 +170,8 @@ pub struct TypeDescriptor {
pub align: usize,
}
impl core::hash::Hash for TypeDescriptor {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for TypeDescriptor {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
@@ -192,8 +190,7 @@ impl PartialEq for TypeDescriptor {
}
/// Graph runtime type information used for type inference.
#[derive(Clone, PartialEq, Eq, Hash, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
pub enum Type {
/// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type.
Generic(Cow<'static, str>),
@@ -264,10 +261,10 @@ impl Type {
pub fn new<T: dyn_any::StaticType + Sized>() -> Self {
Self::Concrete(TypeDescriptor {
id: Some(TypeId::of::<T::Static>()),
name: Cow::Borrowed(core::any::type_name::<T::Static>()),
name: Cow::Borrowed(std::any::type_name::<T::Static>()),
alias: None,
size: core::mem::size_of::<T>(),
align: core::mem::align_of::<T>(),
size: size_of::<T>(),
align: align_of::<T>(),
})
}
@@ -318,8 +315,8 @@ fn format_type(ty: &str) -> String {
.join("<")
}
impl core::fmt::Debug for Type {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
impl std::fmt::Debug for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let result = match self {
Self::Generic(name) => name.to_string(),
#[cfg(feature = "type_id_logging")]

View File

@@ -43,9 +43,9 @@ mod u64_string {
}
mod uuid_generation {
use core::cell::Cell;
use rand_chacha::ChaCha20Rng;
use rand_chacha::rand_core::{RngCore, SeedableRng};
use std::cell::Cell;
use std::sync::Mutex;
static RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);
@@ -79,8 +79,8 @@ impl NodeId {
}
}
impl core::fmt::Display for NodeId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@@ -1,6 +1,6 @@
use crate::Node;
use core::cell::{Cell, RefCell, RefMut};
use core::marker::PhantomData;
use std::cell::{Cell, RefCell, RefMut};
use std::marker::PhantomData;
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct IntNode<const N: u32>;

View File

@@ -1,8 +1,57 @@
use super::poisson_disk::poisson_disk_sample;
use crate::vector::misc::dvec2_to_point;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
return None;
}
// Get the segment which lies at the split.
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
// Divide the segment.
let first_segment = segment.subsegment(0.0..t);
let second_segment = segment.subsegment(t..1.);
let mut first_bezpath = BezPath::new();
let mut second_bezpath = BezPath::new();
// Append the segments up to the subdividing segment from original bezpath to first bezpath.
for segment in bezpath.segments().take(segment_index) {
if first_bezpath.elements().is_empty() {
first_bezpath.move_to(segment.start());
}
first_bezpath.push(segment.as_path_el());
}
// Append the first segment of the subdivided segment.
if first_bezpath.elements().is_empty() {
first_bezpath.move_to(first_segment.start());
}
first_bezpath.push(first_segment.as_path_el());
// Append the second segment of the subdivided segment in the second bezpath.
if second_bezpath.elements().is_empty() {
second_bezpath.move_to(second_segment.start());
}
second_bezpath.push(second_segment.as_path_el());
// Append the segments after the subdividing segment from original bezpath to second bezpath.
for segment in bezpath.segments().skip(segment_index + 1) {
if second_bezpath.elements().is_empty() {
second_bezpath.move_to(segment.start());
}
second_bezpath.push(segment.as_path_el());
}
Some((first_bezpath, second_bezpath))
}
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
@@ -18,7 +67,15 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_l
}
}
pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, segments_length: &[f64]) -> Option<BezPath> {
pub fn sample_polyline_on_bezpath(
bezpath: BezPath,
point_spacing_type: PointSpacingType,
amount: f64,
start_offset: f64,
stop_offset: f64,
adaptive_spacing: bool,
segments_length: &[f64],
) -> Option<BezPath> {
let mut sample_bezpath = BezPath::new();
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
@@ -29,22 +86,33 @@ pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f6
// Adjust the usable length by subtracting start and stop offsets.
let mut used_length = total_length - start_offset - stop_offset;
// Sanity check that the usable length is positive.
if used_length <= 0. {
return None;
}
// Determine the number of points to generate along the path.
let sample_count = if adaptive_spacing {
// Calculate point count to evenly distribute points while covering the entire path.
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
(used_length / spacing).round()
} else {
// Calculate point count based on exact spacing, which may not cover the entire path.
const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
let count = (used_length / spacing + f64::EPSILON).floor();
used_length -= used_length % spacing;
count
// Determine the number of points to generate along the path.
let sample_count = match point_spacing_type {
PointSpacingType::Separation => {
let spacing = amount.min(used_length - f64::EPSILON);
if adaptive_spacing {
// Calculate point count to evenly distribute points while covering the entire path.
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
(used_length / spacing).round().min(SAFETY_MAX_COUNT)
} else {
// Calculate point count based on exact spacing, which may not cover the entire path.
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
let count = (used_length / spacing + f64::EPSILON).floor().min(SAFETY_MAX_COUNT);
if count != SAFETY_MAX_COUNT {
used_length -= used_length % spacing;
}
count
}
}
PointSpacingType::Quantity => (amount - 1.).floor().clamp(1., SAFETY_MAX_COUNT),
};
// Skip if there are no points to generate.
@@ -108,7 +176,7 @@ pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segment
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 {
pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
let mut low_t = 0.;
let mut mid_t = 0.5;
let mut high_t = 1.;
@@ -139,7 +207,7 @@ pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accur
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
/// The returned tuple represents the segment index and the `t` value along that segment.
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
let mut accumulator = 0.;
for (index, length) in lengths.iter().enumerate() {
let length_ratio = length / total_length;
@@ -158,7 +226,7 @@ enum BezPathTValue {
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
let segment_count = bezpath.segments().count();
assert!(segment_count >= 1);

View File

@@ -99,7 +99,7 @@ async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
mod test {
use super::*;
use crate::Node;
use crate::ops::ExtractXyNode;
use crate::extract_xy::{ExtractXyNode, XY};
use crate::vector::VectorData;
use bezier_rs::Subpath;
use glam::DVec2;
@@ -109,7 +109,7 @@ mod test {
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: I) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
@@ -121,7 +121,7 @@ mod test {
let owned = OwnedContextImpl::default().into_context();
let rect = crate::vector::generator_nodes::RectangleNode::new(
FutureWrapperNode(()),
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(crate::ops::XY::Y)),
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64),
FutureWrapperNode(false),
FutureWrapperNode(0_f64),

View File

@@ -1,16 +1,23 @@
use crate::vector::{PointId, VectorData, VectorDataIndex};
use glam::DVec2;
use crate::vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
use glam::{DAffine2, DVec2};
use petgraph::prelude::UnGraphMap;
use rustc_hash::FxHashSet;
impl VectorData {
pub trait MergeByDistanceExt {
/// Collapse all points with edges shorter than the specified distance
pub(crate) fn merge_by_distance(&mut self, distance: f64) {
fn merge_by_distance_topological(&mut self, distance: f64);
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
}
impl MergeByDistanceExt for VectorData {
fn merge_by_distance_topological(&mut self, distance: f64) {
// Treat self as an undirected graph
let indices = VectorDataIndex::build_from(self);
// TODO: We lose information on the winding order by using an undirected graph. Switch to a directed graph and fix the algorithm to handle that.
// Graph containing only short edges, referencing the data graph
let mut short_edges = UnGraphMap::new();
for segment_id in self.segment_ids().iter().copied() {
let length = indices.segment_chord_length(segment_id);
if length < distance {
@@ -92,4 +99,116 @@ impl VectorData {
self.segment_domain.retain(|id| !segments_to_delete.contains(id), usize::MAX);
self.point_domain.retain(&mut self.segment_domain, |id| !points_to_delete.contains(id));
}
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) {
let point_count = self.point_domain.positions().len();
// Find min x and y for grid cell normalization
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
// Calculate mins without collecting all positions
for &pos in self.point_domain.positions() {
let transformed_pos = transform.transform_point2(pos);
min_x = min_x.min(transformed_pos.x);
min_y = min_y.min(transformed_pos.y);
}
// Create a spatial grid with cell size of 'distance'
use std::collections::HashMap;
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
// Add points to grid cells without collecting all positions first
for i in 0..point_count {
let pos = transform.transform_point2(self.point_domain.positions()[i]);
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
grid.entry((grid_x, grid_y)).or_default().push(i);
}
// Create point index mapping for merged points
let mut point_index_map = vec![None; point_count];
let mut merged_positions = Vec::new();
let mut merged_indices = Vec::new();
// Process each point
for i in 0..point_count {
// Skip points that have already been processed
if point_index_map[i].is_some() {
continue;
}
let pos_i = transform.transform_point2(self.point_domain.positions()[i]);
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
let mut group = vec![i];
// Check only neighboring cells (3x3 grid around current cell)
for dx in -1..=1 {
for dy in -1..=1 {
let neighbor_cell = (grid_x + dx, grid_y + dy);
if let Some(indices) = grid.get(&neighbor_cell) {
for &j in indices {
if j > i && point_index_map[j].is_none() {
let pos_j = transform.transform_point2(self.point_domain.positions()[j]);
if pos_i.distance(pos_j) <= distance {
group.push(j);
}
}
}
}
}
}
// Create merged point - calculate positions as needed
let merged_position = group
.iter()
.map(|&idx| transform.transform_point2(self.point_domain.positions()[idx]))
.fold(DVec2::ZERO, |sum, pos| sum + pos)
/ group.len() as f64;
let merged_position = transform.inverse().transform_point2(merged_position);
let merged_index = merged_positions.len();
merged_positions.push(merged_position);
merged_indices.push(self.point_domain.ids()[group[0]]);
// Update mapping for all points in the group
for &idx in &group {
point_index_map[idx] = Some(merged_index);
}
}
// Create new point domain with merged points
let mut new_point_domain = PointDomain::new();
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
new_point_domain.push(idx, pos);
}
// Update segment domain
let mut new_segment_domain = SegmentDomain::new();
for segment_idx in 0..self.segment_domain.ids().len() {
let id = self.segment_domain.ids()[segment_idx];
let start = self.segment_domain.start_point()[segment_idx];
let end = self.segment_domain.end_point()[segment_idx];
let handles = self.segment_domain.handles()[segment_idx];
let stroke = self.segment_domain.stroke()[segment_idx];
// Get new indices for start and end points
let new_start = point_index_map[start].unwrap();
let new_end = point_index_map[end].unwrap();
// Skip segments where start and end points were merged
if new_start != new_end {
new_segment_domain.push(id, new_start, new_end, handles, stroke);
}
}
// Create new vector data
self.point_domain = new_point_domain;
self.segment_domain = new_segment_domain;
}
}

View File

@@ -1,6 +1,6 @@
pub mod bezpath_algorithms;
mod instance;
mod merge_by_distance;
pub mod instance;
pub mod merge_by_distance;
pub mod offset_subpath;
mod poisson_disk;
pub mod poisson_disk;
pub mod spline;

View File

@@ -8,7 +8,7 @@ const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
/// Constant used to determine if `f64`s are equivalent.
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
fn segment_to_bezier(seg: kurbo::PathSeg) -> bezier_rs::Bezier {
fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier {
match seg {
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y),

View File

@@ -1,6 +1,6 @@
use core::f64;
use glam::DVec2;
use std::collections::HashMap;
use std::f64;
const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
@@ -27,7 +27,7 @@ pub fn poisson_disk_sample(
// - Dividing into an integer number of cells across the dartboard domain, to avoid wastefully throwing darts beyond the width and height of the dartboard domain
// - Being fully covered by the radius around a dart thrown anywhere in its area, where the worst-case is a corner which has a distance of sqrt(2) to the opposite corner
let greater_dimension = width.max(height);
let base_level_grid_size = greater_dimension / (greater_dimension * std::f64::consts::SQRT_2 / (diameter / 2.)).ceil();
let base_level_grid_size = greater_dimension / (greater_dimension * f64::consts::SQRT_2 / (diameter / 2.)).ceil();
// Initialize the problem by including all base-level squares in the active list since they're all part of the yet-to-be-targetted dartboard domain
let base_level = ActiveListLevel::new_filled(base_level_grid_size, offset, width, height, &point_in_shape_checker, &line_intersect_shape_checker);

View File

@@ -1,13 +1,12 @@
use crate::Color;
use crate::math::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use crate::raster::bbox::AxisAlignedBbox;
use dyn_any::DynAny;
use glam::DVec2;
use std::hash::{Hash, Hasher};
/// The style of a brush.
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushStyle {
pub color: Color,
pub diameter: f64,
@@ -55,8 +54,7 @@ impl PartialEq for BrushStyle {
}
/// A single sample of brush parameters across the brush stroke.
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushInputSample {
// The position of the sample in layer space, in pixels.
// The origin of layer space is not specified.
@@ -72,8 +70,7 @@ impl Hash for BrushInputSample {
}
/// The parameters for a single stroke brush.
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)]
pub struct BrushStroke {
pub style: BrushStyle,
pub trace: Vec<BrushInputSample>,

View File

@@ -0,0 +1,162 @@
use crate::math::math_ext::QuadExt;
use crate::math::quad::Quad;
use crate::vector::PointId;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FreePoint {
pub id: PointId,
pub position: DVec2,
}
impl FreePoint {
pub fn new(id: PointId, position: DVec2) -> Self {
Self { id, position }
}
pub fn apply_transform(&mut self, transform: DAffine2) {
self.position = transform.transform_point2(self.position);
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ClickTargetType {
Subpath(Subpath<PointId>),
FreePoint(FreePoint),
}
/// Represents a clickable target for the layer
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ClickTarget {
target_type: ClickTargetType,
stroke_width: f64,
bounding_box: Option<[DVec2; 2]>,
}
impl ClickTarget {
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
let bounding_box = subpath.loose_bounding_box();
Self {
target_type: ClickTargetType::Subpath(subpath),
stroke_width,
bounding_box,
}
}
pub fn new_with_free_point(point: FreePoint) -> Self {
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
let stroke_width = 10.;
let bounding_box = Some([
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
]);
Self {
target_type: ClickTargetType::FreePoint(point),
stroke_width,
bounding_box,
}
}
pub fn target_type(&self) -> &ClickTargetType {
&self.target_type
}
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.bounding_box
}
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
}
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
match self.target_type {
ClickTargetType::Subpath(ref mut subpath) => {
subpath.apply_transform(affine_transform);
}
ClickTargetType::FreePoint(ref mut point) => {
point.apply_transform(affine_transform);
}
}
self.update_bbox();
}
fn update_bbox(&mut self) {
match self.target_type {
ClickTargetType::Subpath(ref subpath) => {
self.bounding_box = subpath.bounding_box();
}
ClickTargetType::FreePoint(ref point) => {
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
}
}
}
/// Does the click target intersect the path
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
// Check if the matrix is not invertible
let mut layer_transform = layer_transform;
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
}
let inverse = layer_transform.inverse();
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
match self.target_type() {
ClickTargetType::Subpath(subpath) => {
// Check if outlines intersect
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
if subpath.iter().any(outline_intersects) {
return true;
}
// Check if selection is entirely within the shape
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
return true;
}
// Check if shape is entirely within selection
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
}
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
}
}
/// Does the click target intersect the point (accounting for stroke size)
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)];
let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y;
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
if !self
.bounding_box
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
{
return false;
}
// Allows for selecting lines
// TODO: actual intersection of stroke
let inflated_quad = Quad::from_box(target_bounds);
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
}
/// Does the click target intersect the point (not accounting for stroke size)
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
// Check if the point is within the bounding box
if self
.bounding_box
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
{
// Check if the point is within the shape
match self.target_type() {
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
ClickTargetType::FreePoint(free_point) => free_point.position == point,
}
} else {
false
}
}
}

View File

@@ -119,12 +119,12 @@ fn star<T: AsU64>(
#[hard_min(2.)]
#[implementations(u32, u64, f64)]
sides: T,
#[default(50)] radius: f64,
#[default(25)] inner_radius: f64,
#[default(50)] radius_1: f64,
#[default(25)] radius_2: f64,
) -> VectorDataTable {
let points = sides.as_u64();
let diameter: f64 = radius * 2.;
let inner_diameter = inner_radius * 2.;
let diameter: f64 = radius_1 * 2.;
let inner_diameter = radius_2 * 2.;
VectorDataTable::new(VectorData::from_subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
}
@@ -158,8 +158,8 @@ fn grid<T: GridSpacing>(
#[implementations(f64, DVec2)]
spacing: T,
#[default(30., 30.)] angles: DVec2,
#[default(10)] rows: u32,
#[default(10)] columns: u32,
#[default(10)] rows: u32,
) -> VectorDataTable {
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
let (angle_a, angle_b) = angles.into();

View File

@@ -13,31 +13,6 @@ pub enum CentroidType {
Length,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum BooleanOperation {
#[default]
#[icon("BooleanUnion")]
Union,
#[icon("BooleanSubtractFront")]
SubtractFront,
#[icon("BooleanSubtractBack")]
SubtractBack,
#[icon("BooleanIntersect")]
Intersect,
#[icon("BooleanDifference")]
Difference,
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}
pub trait AsU64 {
fn as_u64(&self) -> u64;
}
@@ -94,6 +69,26 @@ pub enum ArcType {
PieSlice,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum MergeByDistanceAlgorithm {
#[default]
Spatial,
Topological,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum PointSpacingType {
#[default]
/// The desired spacing distance between points.
Separation,
/// The exact number of points to span the path.
Quantity,
}
pub fn point_to_dvec2(point: Point) -> DVec2 {
DVec2 { x: point.x, y: point.y }
}

View File

@@ -1,12 +1,15 @@
mod algorithms;
pub mod algorithms;
pub mod brush_stroke;
pub mod click_target;
pub mod generator_nodes;
pub mod misc;
mod reference_point;
pub mod style;
mod vector_data;
mod vector_nodes;
pub use bezier_rs;
pub use reference_point::*;
pub use style::PathStyle;
pub use vector_data::*;
pub use vector_nodes::*;

View File

@@ -0,0 +1,103 @@
use crate::math::bbox::AxisAlignedBbox;
use glam::DVec2;
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {
#[default]
None,
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl ReferencePoint {
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
let size = bounding_box.size();
let offset = match self {
ReferencePoint::None => return None,
ReferencePoint::TopLeft => DVec2::ZERO,
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
};
Some(bounding_box.start + offset)
}
}
impl From<&str> for ReferencePoint {
fn from(input: &str) -> Self {
match input {
"None" => ReferencePoint::None,
"TopLeft" => ReferencePoint::TopLeft,
"TopCenter" => ReferencePoint::TopCenter,
"TopRight" => ReferencePoint::TopRight,
"CenterLeft" => ReferencePoint::CenterLeft,
"Center" => ReferencePoint::Center,
"CenterRight" => ReferencePoint::CenterRight,
"BottomLeft" => ReferencePoint::BottomLeft,
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
}
}
}
impl From<ReferencePoint> for Option<DVec2> {
fn from(input: ReferencePoint) -> Self {
match input {
ReferencePoint::None => None,
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
}
}
}
impl From<DVec2> for ReferencePoint {
fn from(input: DVec2) -> Self {
const TOLERANCE: f64 = 1e-5_f64;
if input.y.abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::TopLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::TopCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::TopRight;
}
} else if (input.y - 0.5).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::CenterLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::Center;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::CenterRight;
}
} else if (input.y - 1.).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::BottomLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::BottomCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::BottomRight;
}
}
ReferencePoint::None
}
}

View File

@@ -1,312 +1,9 @@
//! Contains stylistic options for SVG elements.
use crate::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::renderer::{RenderParams, format_transform_matrix};
pub use crate::gradient::*;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::fmt::Write;
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
self.0.iter().for_each(|(position, color)| {
position.to_bits().hash(state);
color.hash(state);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops {
pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() {
return Color::BLACK;
}
if t <= self.0[0].0 {
return self.0[0].1;
}
if t >= self.0[self.0.len() - 1].0 {
return self.0[self.0.len() - 1].1;
}
for i in 0..self.0.len() - 1 {
let (t1, c1) = self.0[i];
let (t2, c2) = self.0[i + 1];
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
return c1.lerp(&c2, normalized_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl core::hash::Hash for Gradient {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.0
.iter()
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let stops = self
.stops
.0
.iter()
.zip(other.stops.0.iter())
.map(|((a_pos, a_color), (b_pos, b_color))| {
let position = a_pos + (b_pos - a_pos) * time;
let color = a_color.lerp(b_color, time as f32);
(position, color)
})
.collect::<Vec<_>>();
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
}
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 {
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let mut stop = String::new();
for (position, color) in self.stops.0.iter() {
stop.push_str("<stop");
if *position != 0. {
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
}
let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
stop.push_str(" />")
}
let mod_gradient = if transformed_bound_transform.matrix2.determinant() != 0. {
transformed_bound_transform.inverse()
} else {
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
};
let mod_points = element_transform * stroke_transform * bound_transform;
let start = mod_points.transform_point2(self.start);
let end = mod_points.transform_point2(self.end);
let gradient_id = crate::uuid::generate_uuid();
let matrix = format_transform_matrix(mod_gradient);
let gradient_transform = if matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{}""#, matrix) };
match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, stop
);
}
GradientType::Radial => {
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, stop
);
}
}
gradient_id
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
// Lerp between the nearest colors if applicable
(a, Some(b)) => a.lerp(
&b,
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
),
// Use the start or the end color if applicable
(v, _) => v,
};
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
}
use glam::DAffine2;
/// Describes the fill of a layer.
///
@@ -380,24 +77,6 @@ impl Fill {
}
}
/// Renders the fill, adding necessary defs through mutating the first argument.
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String {
match self {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => {
let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
format!(r##" fill="url('#{gradient_id}')""##)
}
}
}
/// Extract a gradient from the fill
pub fn as_gradient(&self) -> Option<&Gradient> {
match self {
@@ -521,7 +200,7 @@ pub enum StrokeCap {
}
impl StrokeCap {
fn svg_name(&self) -> &'static str {
pub fn svg_name(&self) -> &'static str {
match self {
StrokeCap::Butt => "butt",
StrokeCap::Round => "round",
@@ -541,7 +220,7 @@ pub enum StrokeJoin {
}
impl StrokeJoin {
fn svg_name(&self) -> &'static str {
pub fn svg_name(&self) -> &'static str {
match self {
StrokeJoin::Bevel => "bevel",
StrokeJoin::Miter => "miter",
@@ -611,8 +290,8 @@ pub struct Stroke {
pub paint_order: PaintOrder,
}
impl core::hash::Hash for Stroke {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for Stroke {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.weight.to_bits().hash(state);
{
@@ -711,60 +390,6 @@ impl Stroke {
self.join_miter_limit as f32
}
/// Provide the SVG attributes for the stroke.
pub fn render(&self, aligned_strokes: bool, override_paint_order: bool, _render_params: &RenderParams) -> String {
// Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if !self.has_renderable_stroke() {
return String::new();
}
// Set to None if the value is the SVG default
let weight = (self.weight != 1.).then_some(self.weight);
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap);
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || override_paint_order).then_some(PaintOrder::StrokeBelow);
// Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
if let Some(mut weight) = weight {
if stroke_align.is_some() && aligned_strokes {
weight *= 2.;
}
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
}
if let Some(dash_array) = dash_array {
let _ = write!(&mut attributes, r#" stroke-dasharray="{}""#, dash_array);
}
if let Some(dash_offset) = dash_offset {
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
}
if let Some(stroke_cap) = stroke_cap {
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name());
}
if let Some(stroke_join) = stroke_join {
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name());
}
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, stroke_join_miter_limit);
}
// Add vector-effect attribute to make strokes non-scaling
if self.non_scaling {
let _ = write!(&mut attributes, r#" vector-effect="non-scaling-stroke""#);
}
if paint_order.is_some() {
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
}
attributes
}
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
self.color = *color;
@@ -846,12 +471,12 @@ impl Default for Stroke {
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct PathStyle {
stroke: Option<Stroke>,
fill: Fill,
pub stroke: Option<Stroke>,
pub fill: Fill,
}
impl core::hash::Hash for PathStyle {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for PathStyle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.stroke.hash(state);
self.fill.hash(state);
}
@@ -1008,41 +633,6 @@ impl PathStyle {
pub fn clear_stroke(&mut self) {
self.stroke = None;
}
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
svg_defs: &mut String,
element_transform: DAffine2,
stroke_transform: DAffine2,
bounds: [DVec2; 2],
transformed_bounds: [DVec2; 2],
aligned_strokes: bool,
override_paint_order: bool,
render_params: &RenderParams,
) -> String {
let view_mode = render_params.view_mode;
match view_mode {
ViewMode::Outline => {
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
// Outline strokes should be non-scaling by default
outline_stroke.non_scaling = true;
let stroke_attribute = outline_stroke.render(aligned_strokes, override_paint_order, render_params);
format!("{fill_attribute}{stroke_attribute}")
}
_ => {
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
let stroke_attribute = self
.stroke
.as_ref()
.map(|stroke| stroke.render(aligned_strokes, override_paint_order, render_params))
.unwrap_or_default();
format!("{fill_attribute}{stroke_attribute}")
}
}
}
}
/// Represents different ways of rendering an object

View File

@@ -4,12 +4,16 @@ mod modification;
use super::misc::{dvec2_to_point, point_to_dvec2};
use super::style::{PathStyle, Stroke};
use crate::bounds::BoundingBox;
use crate::instances::Instances;
use crate::renderer::{ClickTargetType, FreePoint};
use crate::math::quad::Quad;
use crate::transform::Transform;
use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable};
pub use attributes::*;
use bezier_rs::ManipulatorGroup;
use bezier_rs::{BezierHandles, ManipulatorGroup};
use core::borrow::Borrow;
use core::hash::Hash;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
pub use indexed::VectorDataIndex;
@@ -21,8 +25,7 @@ use std::collections::HashMap;
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
@@ -75,8 +78,7 @@ pub type VectorDataTable = Instances<VectorData>;
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
///
/// Segments are connected if they share endpoints.
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct VectorData {
pub style: PathStyle,
@@ -105,8 +107,8 @@ impl Default for VectorData {
}
}
impl core::hash::Hash for VectorData {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl std::hash::Hash for VectorData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.point_domain.hash(state);
self.segment_domain.hash(state);
self.region_domain.hash(state);
@@ -188,11 +190,6 @@ impl VectorData {
self.point_domain.push(id, point.position);
}
/// Appends a Kurbo BezPath to the vector data.
pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) {
AppendBezpath::append_bezpath(self, bezpath);
}
/// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
Self::from_subpaths([subpath], false)
@@ -337,6 +334,13 @@ impl VectorData {
index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index]))
}
/// Returns the number of linear segments connected to the given point.
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
self.segment_bezier_iter()
.filter(|(_, bez, start, end)| ((*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear)))
.count()
}
/// Get an array slice of all segment IDs.
pub fn segment_ids(&self) -> &[SegmentId] {
self.segment_domain.ids()
@@ -414,7 +418,7 @@ impl VectorData {
}
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
let pair = self.colinear_manipulators.iter().find(|pair| pair.iter().any(|&val| val == handle))?;
let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?;
let other = pair.iter().copied().find(|&val| val != handle)?;
if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) {
Some(other)
@@ -494,9 +498,31 @@ impl VectorData {
}
}
impl BoundingBox for VectorDataTable {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
self.instance_ref_iter()
.flat_map(|instance| {
if !include_stroke {
return instance.instance.bounding_box_with_transform(transform * *instance.transform);
}
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
let scale = transform.decompose_scale();
// We use the full line width here to account for different styles of stroke caps
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
})
.reduce(Quad::combine_bounds)
}
}
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub enum ManipulatorPointId {
/// A control anchor - the start or end point of a bézier.
Anchor(PointId),
@@ -583,8 +609,7 @@ impl ManipulatorPointId {
}
/// The type of handle found on a bézier curve.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub enum HandleType {
/// The first handle on a cubic bézier or the only handle on a quadratic bézier.
Primary,
@@ -593,8 +618,7 @@ pub enum HandleType {
}
/// Represents a primary or end handle found in a particular segment.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
pub struct HandleId {
pub ty: HandleType,
pub segment: SegmentId,
@@ -642,16 +666,6 @@ impl HandleId {
handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX)
}
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
#[must_use]
pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
let Self { ty, segment } = self;
match ty {
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
}
}
/// Convert an end handle to the primary handle and a primary handle to an end handle. Note that the new handle may not exist (e.g. for a quadratic bézier).
#[must_use]
pub fn opposite(self) -> Self {

View File

@@ -1,18 +1,18 @@
use crate::vector::misc::dvec2_to_point;
use crate::vector::vector_data::{HandleId, VectorData};
use bezier_rs::{BezierHandles, ManipulatorGroup};
use core::iter::zip;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::iter::zip;
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
macro_rules! create_ids {
($($id:ident),*) => {
$(
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(serde::Serialize, serde::Deserialize)]
/// A strongly typed ID
pub struct $id(u64);
@@ -53,7 +53,7 @@ create_ids! { InstanceId, PointId, SegmentId, RegionId, StrokeId, FillId }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NoHash(Option<u64>);
impl core::hash::Hasher for NoHash {
impl Hasher for NoHash {
fn finish(&self) -> u64 {
self.0.unwrap()
}
@@ -70,15 +70,14 @@ impl core::hash::Hasher for NoHash {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NoHashBuilder;
impl core::hash::BuildHasher for NoHashBuilder {
impl std::hash::BuildHasher for NoHashBuilder {
type Hasher = NoHash;
fn build_hasher(&self) -> Self::Hasher {
NoHash::default()
}
}
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
/// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.
pub struct PointDomain {
id: Vec<PointId>,
@@ -86,8 +85,8 @@ pub struct PointDomain {
pub(crate) position: Vec<DVec2>,
}
impl core::hash::Hash for PointDomain {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl Hash for PointDomain {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.position.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state));
}
@@ -133,6 +132,11 @@ impl PointDomain {
self.position.push(position);
}
pub fn push_unchecked(&mut self, id: PointId, position: DVec2) {
self.id.push(id);
self.position.push(position);
}
pub fn positions(&self) -> &[DVec2] {
&self.position
}
@@ -195,15 +199,14 @@ impl PointDomain {
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
/// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes.
pub struct SegmentDomain {
#[serde(alias = "ids")]
id: Vec<SegmentId>,
start_point: Vec<usize>,
end_point: Vec<usize>,
handles: Vec<bezier_rs::BezierHandles>,
handles: Vec<BezierHandles>,
stroke: Vec<StrokeId>,
}
@@ -293,7 +296,7 @@ impl SegmentDomain {
self.end_point[segment_index] = new;
}
pub fn handles(&self) -> &[bezier_rs::BezierHandles] {
pub fn handles(&self) -> &[BezierHandles] {
&self.handles
}
@@ -301,7 +304,7 @@ impl SegmentDomain {
&self.stroke
}
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain");
self.id.push(id);
@@ -319,12 +322,12 @@ impl SegmentDomain {
self.id.iter().copied().zip(self.end_point.iter_mut())
}
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut bezier_rs::BezierHandles, usize, usize)> {
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut BezierHandles, usize, usize)> {
let nested = self.id.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
}
pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut bezier_rs::BezierHandles, &mut usize, &mut usize)> {
pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut BezierHandles, &mut usize, &mut usize)> {
let nested = self.handles.iter_mut().zip(&mut self.start_point).zip(&mut self.end_point);
nested.map(|((a, b), c)| (a, b, c))
}
@@ -368,7 +371,7 @@ impl SegmentDomain {
self.id.iter().position(|&check_id| check_id == id)
}
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
fn resolve_range(&self, range: &std::ops::RangeInclusive<SegmentId>) -> Option<std::ops::RangeInclusive<usize>> {
match (self.id_to_index(*range.start()), self.id_to_index(*range.end())) {
(Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.id.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
_ => {
@@ -439,14 +442,13 @@ impl SegmentDomain {
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
/// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the
/// [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes.
pub struct RegionDomain {
#[serde(alias = "ids")]
id: Vec<RegionId>,
segment_range: Vec<core::ops::RangeInclusive<SegmentId>>,
segment_range: Vec<std::ops::RangeInclusive<SegmentId>>,
fill: Vec<FillId>,
}
@@ -476,7 +478,7 @@ impl RegionDomain {
/// Like [`Self::retain`] but also gives the function access to the segment range.
///
/// Note that this function requires an allocation that `retain` avoids.
pub fn retain_with_region(&mut self, f: impl Fn(&RegionId, &core::ops::RangeInclusive<SegmentId>) -> bool) {
pub fn retain_with_region(&mut self, f: impl Fn(&RegionId, &std::ops::RangeInclusive<SegmentId>) -> bool) {
let keep = self.id.iter().zip(self.segment_range.iter()).map(|(id, range)| f(id, range)).collect::<Vec<_>>();
let mut iter = keep.iter().copied();
self.segment_range.retain(|_| iter.next().unwrap());
@@ -486,7 +488,7 @@ impl RegionDomain {
self.id.retain(|_| iter.next().unwrap());
}
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
pub fn push(&mut self, id: RegionId, segment_range: std::ops::RangeInclusive<SegmentId>, fill: FillId) {
if self.id.contains(&id) {
warn!("Duplicate region");
return;
@@ -504,7 +506,7 @@ impl RegionDomain {
self.id.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
}
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut core::ops::RangeInclusive<SegmentId>)> {
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut std::ops::RangeInclusive<SegmentId>)> {
self.id.iter().copied().zip(self.segment_range.iter_mut())
}
@@ -516,7 +518,7 @@ impl RegionDomain {
&self.id
}
pub fn segment_range(&self) -> &[core::ops::RangeInclusive<SegmentId>] {
pub fn segment_range(&self) -> &[std::ops::RangeInclusive<SegmentId>] {
&self.segment_range
}
@@ -545,7 +547,7 @@ impl RegionDomain {
/// Iterates over regions in the domain.
///
/// Tuple is: (id, segment_range, fill)
pub fn iter(&self) -> impl Iterator<Item = (RegionId, core::ops::RangeInclusive<SegmentId>, FillId)> + '_ {
pub fn iter(&self) -> impl Iterator<Item = (RegionId, std::ops::RangeInclusive<SegmentId>, FillId)> + '_ {
let ids = self.id.iter().copied();
let segment_range = self.segment_range.iter().cloned();
let fill = self.fill.iter().copied();
@@ -643,7 +645,7 @@ impl FoundSubpath {
impl VectorData {
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier {
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> bezier_rs::Bezier {
let start = self.point_domain.positions()[start];
let end = self.point_domain.positions()[end];
bezier_rs::Bezier { start, end, handles }
@@ -752,15 +754,15 @@ impl VectorData {
}
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut last: Option<(usize, bezier_rs::BezierHandles)> = None;
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
first_point = Some(first_point.unwrap_or(start));
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
@@ -776,7 +778,7 @@ impl VectorData {
if closed {
groups[0].in_handle = last_handle.end();
} else {
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
@@ -789,10 +791,10 @@ impl VectorData {
}
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
fn subpath_from_segments(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
fn subpath_from_segments(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut last: Option<(usize, bezier_rs::BezierHandles)> = None;
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
if last.is_some_and(|(previous_end, _)| previous_end != start) {
@@ -801,7 +803,7 @@ impl VectorData {
}
first_point = Some(first_point.unwrap_or(start));
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
@@ -817,7 +819,7 @@ impl VectorData {
if closed {
groups[0].in_handle = last_handle.end();
} else {
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
@@ -849,7 +851,7 @@ impl VectorData {
})
}
pub fn build_stroke_path_iter(&self) -> StrokePathIter {
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> {
let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()];
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
@@ -908,13 +910,13 @@ impl VectorData {
})
}
/// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke.
pub fn manipulator_groups(&self) -> impl Iterator<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
/// Construct an iterator [`ManipulatorGroup`] for stroke.
pub fn manipulator_groups(&self) -> impl Iterator<Item = ManipulatorGroup<PointId>> + '_ {
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
}
/// Get manipulator by id
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<bezier_rs::ManipulatorGroup<PointId>> {
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<ManipulatorGroup<PointId>> {
let id = id.into();
self.manipulator_groups().find(|group| group.id == id)
}
@@ -994,7 +996,7 @@ pub struct StrokePathIter<'a> {
}
impl Iterator for StrokePathIter<'_> {
type Item = (Vec<bezier_rs::ManipulatorGroup<PointId>>, bool);
type Item = (Vec<ManipulatorGroup<PointId>>, bool);
fn next(&mut self) -> Option<Self::Item> {
let current_start = if let Some((index, _)) = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1) {
@@ -1016,7 +1018,7 @@ impl Iterator for StrokePathIter<'_> {
loop {
let Some(val) = self.points[point_index].take_first() else {
// Dead end
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.vector_data.point_domain.positions()[point_index],
in_handle,
out_handle: None,
@@ -1035,7 +1037,7 @@ impl Iterator for StrokePathIter<'_> {
} else {
self.vector_data.segment_domain.end_point()[val.segment_index]
};
groups.push(bezier_rs::ManipulatorGroup {
groups.push(ManipulatorGroup {
anchor: self.vector_data.point_domain.positions()[point_index],
in_handle,
out_handle: handles.start(),

View File

@@ -3,14 +3,13 @@ use crate::Ctx;
use crate::instances::Instance;
use crate::uuid::generate_uuid;
use bezier_rs::BezierHandles;
use core::hash::BuildHasher;
use dyn_any::DynAny;
use kurbo::{BezPath, PathEl, Point};
use std::collections::{HashMap, HashSet};
use std::hash::BuildHasher;
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PointModification {
add: Vec<PointId>,
remove: HashSet<PointId>,
@@ -81,8 +80,7 @@ impl PointModification {
}
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SegmentModification {
add: Vec<SegmentId>,
remove: HashSet<SegmentId>,
@@ -254,13 +252,12 @@ impl SegmentModification {
}
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RegionModification {
add: Vec<RegionId>,
remove: HashSet<RegionId>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
segment_range: HashMap<RegionId, core::ops::RangeInclusive<SegmentId>>,
segment_range: HashMap<RegionId, std::ops::RangeInclusive<SegmentId>>,
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
fill: HashMap<RegionId, FillId>,
}
@@ -299,8 +296,7 @@ impl RegionModification {
}
/// Represents a procedural change to the [`VectorData`].
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct VectorModification {
points: PointModification,
segments: SegmentModification,
@@ -416,8 +412,8 @@ impl VectorModification {
}
}
impl core::hash::Hash for VectorModification {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
impl Hash for VectorModification {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
generate_uuid().hash(state)
}
}
@@ -638,6 +634,33 @@ impl<'a> AppendBezpath<'a> {
}
}
pub trait VectorDataExt {
/// Appends a Kurbo BezPath to the vector data.
fn append_bezpath(&mut self, bezpath: BezPath);
}
impl VectorDataExt for VectorData {
fn append_bezpath(&mut self, bezpath: BezPath) {
AppendBezpath::append_bezpath(self, bezpath);
}
}
pub trait HandleExt {
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
#[must_use]
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType;
}
impl HandleExt for HandleId {
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
let Self { ty, segment } = self;
match ty {
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,35 +1,36 @@
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
use super::algorithms::offset_subpath::offset_subpath;
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use super::misc::{CentroidType, point_to_dvec2};
use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataExt, VectorDataTable};
use crate::bounds::BoundingBox;
use crate::instances::{Instance, InstanceMut, Instances};
use crate::raster_types::{CPU, RasterDataTable};
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, ReferencePoint, Transform};
use crate::vector::misc::dvec2_to_point;
use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType};
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use crate::vector::{FillId, PointDomain, RegionId};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use bezier_rs::{Join, ManipulatorGroup, Subpath};
use core::f64::consts::PI;
use core::hash::{Hash, Hasher};
use glam::{DAffine2, DVec2};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Point, Shape};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::PI;
use std::f64::consts::TAU;
use std::hash::{Hash, Hasher};
/// Implemented for types that can be converted to an iterator of vector data.
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
trait VectorDataTableIterMut {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>>;
}
impl VectorDataTableIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
// Grab only the direct children
self.instance_mut_iter()
.filter_map(|element| element.instance.as_vector_data_mut())
@@ -38,7 +39,7 @@ impl VectorDataTableIterMut for GraphicGroupTable {
}
impl VectorDataTableIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
self.instance_mut_iter()
}
}
@@ -210,7 +211,7 @@ where
vector_data
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
@@ -220,10 +221,7 @@ async fn repeat<I: 'n + Send + Clone>(
direction: PixelSize,
angle: Angle,
#[default(4)] instances: IntegerCount,
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
) -> Instances<I> {
let angle = angle.to_radians();
let count = instances.max(1);
let total = (count - 1) as f64;
@@ -249,7 +247,7 @@ where
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
@@ -257,10 +255,7 @@ async fn circular_repeat<I: 'n + Send + Clone>(
angle_offset: Angle,
#[default(5)] radius: f64,
#[default(5)] instances: IntegerCount,
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
) -> Instances<I> {
let count = instances.max(1);
let mut result_table = Instances::<I>::default();
@@ -284,7 +279,7 @@ where
result_table
}
#[node_macro::node(name("Copy to Points"), category("Vector"), path(graphene_core::vector))]
#[node_macro::node(name("Copy to Points"), category("Instancing"), path(graphene_core::vector))]
async fn copy_to_points<I: 'n + Send + Clone>(
_: impl Ctx,
points: VectorDataTable,
@@ -312,10 +307,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
random_rotation: Angle,
/// Seed to determine unique variations on all the randomized instance angles.
random_rotation_seed: SeedValue,
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
{
) -> Instances<I> {
let mut result_table = Instances::<I>::default();
let random_scale_difference = random_scale_max - random_scale_min;
@@ -366,7 +358,7 @@ where
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn mirror<I: 'n + Send + Clone>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
@@ -376,7 +368,7 @@ async fn mirror<I: 'n + Send + Clone>(
#[default(true)] keep_original: bool,
) -> Instances<I>
where
Instances<I>: GraphicElementRendered,
Instances<I>: BoundingBox,
{
let mut result_table = Instances::default();
@@ -424,7 +416,7 @@ where
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn round_corners(
_: impl Ctx,
source: VectorDataTable,
@@ -551,141 +543,36 @@ async fn round_corners(
result_table
}
#[node_macro::node(name("Spatial Merge by Distance"), category("Debug"), path(graphene_core::vector))]
async fn spatial_merge_by_distance(
#[node_macro::node(name("Merge by Distance"), category("Vector: Modifier"), path(graphene_core::vector))]
pub fn merge_by_distance(
_: impl Ctx,
vector_data: VectorDataTable,
#[default(0.1)]
#[hard_min(0.0001)]
distance: f64,
distance: PixelLength,
algorithm: MergeByDistanceAlgorithm,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() {
let vector_data_transform = vector_data_instance.transform;
let vector_data = vector_data_instance.instance;
let point_count = vector_data.point_domain.positions().len();
// Find min x and y for grid cell normalization
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
// Calculate mins without collecting all positions
for &pos in vector_data.point_domain.positions() {
let transformed_pos = vector_data_transform.transform_point2(pos);
min_x = min_x.min(transformed_pos.x);
min_y = min_y.min(transformed_pos.y);
}
// Create a spatial grid with cell size of 'distance'
use std::collections::HashMap;
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
// Add points to grid cells without collecting all positions first
for i in 0..point_count {
let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
grid.entry((grid_x, grid_y)).or_default().push(i);
}
// Create point index mapping for merged points
let mut point_index_map = vec![None; point_count];
let mut merged_positions = Vec::new();
let mut merged_indices = Vec::new();
// Process each point
for i in 0..point_count {
// Skip points that have already been processed
if point_index_map[i].is_some() {
continue;
}
let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
let mut group = vec![i];
// Check only neighboring cells (3x3 grid around current cell)
for dx in -1..=1 {
for dy in -1..=1 {
let neighbor_cell = (grid_x + dx, grid_y + dy);
if let Some(indices) = grid.get(&neighbor_cell) {
for &j in indices {
if j > i && point_index_map[j].is_none() {
let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]);
if pos_i.distance(pos_j) <= distance {
group.push(j);
}
}
}
}
}
}
// Create merged point - calculate positions as needed
let merged_position = group
.iter()
.map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx]))
.fold(DVec2::ZERO, |sum, pos| sum + pos)
/ group.len() as f64;
let merged_position = vector_data_transform.inverse().transform_point2(merged_position);
let merged_index = merged_positions.len();
merged_positions.push(merged_position);
merged_indices.push(vector_data.point_domain.ids()[group[0]]);
// Update mapping for all points in the group
for &idx in &group {
point_index_map[idx] = Some(merged_index);
match algorithm {
MergeByDistanceAlgorithm::Spatial => {
for mut vector_data_instance in vector_data.instance_iter() {
vector_data_instance.instance.merge_by_distance_spatial(vector_data_instance.transform, distance);
result_table.push(vector_data_instance);
}
}
// Create new point domain with merged points
let mut new_point_domain = PointDomain::new();
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
new_point_domain.push(idx, pos);
}
// Update segment domain
let mut new_segment_domain = SegmentDomain::new();
for segment_idx in 0..vector_data.segment_domain.ids().len() {
let id = vector_data.segment_domain.ids()[segment_idx];
let start = vector_data.segment_domain.start_point()[segment_idx];
let end = vector_data.segment_domain.end_point()[segment_idx];
let handles = vector_data.segment_domain.handles()[segment_idx];
let stroke = vector_data.segment_domain.stroke()[segment_idx];
// Get new indices for start and end points
let new_start = point_index_map[start].unwrap();
let new_end = point_index_map[end].unwrap();
// Skip segments where start and end points were merged
if new_start != new_end {
new_segment_domain.push(id, new_start, new_end, handles, stroke);
MergeByDistanceAlgorithm::Topological => {
for mut vector_data_instance in vector_data.instance_iter() {
vector_data_instance.instance.merge_by_distance_topological(distance);
result_table.push(vector_data_instance);
}
}
// Create new vector data
let mut result = vector_data.clone();
result.point_domain = new_point_domain;
result.segment_domain = new_segment_domain;
// Create and return the result
vector_data_instance.instance = result;
vector_data_instance.source_node_id = None;
result_table.push(vector_data_instance);
}
result_table
}
#[node_macro::node(category("Debug"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle: VectorDataTable) -> VectorDataTable {
let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.instance, rect.transform)) else {
return vector_data;
@@ -774,7 +661,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
}
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
#[node_macro::node(category("Vector"), name("Auto-Tangents"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(graphene_core::vector))]
async fn auto_tangents(
_: impl Ctx,
source: VectorDataTable,
@@ -1011,7 +898,7 @@ async fn auto_tangents(
// result_table
// }
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1038,7 +925,7 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn dimensions(_: impl Ctx, vector_data: VectorDataTable) -> DVec2 {
vector_data
.instance_ref_iter()
@@ -1095,7 +982,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: VectorDataTable, #[default(
points
}
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1139,7 +1026,7 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1198,7 +1085,7 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable)] graphic_group_input: Instances<I>) -> VectorDataTable
where
Instances<I>: GraphicElementRendered,
GraphicElement: From<Instances<I>>,
{
// A node based solution to support passing through vector data could be a network node with a cache node connected to
// a Flatten Path connected to an if else node, another connection from the cache directly
@@ -1243,18 +1130,26 @@ where
};
// Flatten the graphic group input into the output VectorData instance
let base_graphic_group = GraphicGroupTable::new(graphic_group_input.to_graphic_element());
let base_graphic_group = GraphicGroupTable::new(GraphicElement::from(graphic_group_input));
flatten_group(&base_graphic_group, &mut output);
// Return the single-row VectorDataTable containing the flattened VectorData subpaths
output_table
}
/// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category(""), path(graphene_core::vector))]
async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, subpath_segment_lengths: Vec<f64>) -> VectorDataTable {
// Limit the smallest spacing to something sensible to avoid freezing the application.
let spacing = spacing.max(0.01);
async fn sample_polyline(
_: impl Ctx,
vector_data: VectorDataTable,
spacing: PointSpacingType,
separation: f64,
quantity: f64,
start_offset: f64,
stop_offset: f64,
adaptive_spacing: bool,
subpath_segment_lengths: Vec<f64>,
) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
for mut vector_data_instance in vector_data.instance_iter() {
@@ -1289,7 +1184,11 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
next_segment_index += segment_count;
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
let amount = match spacing {
PointSpacingType::Separation => separation,
PointSpacingType::Quantity => quantity,
};
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
continue;
};
@@ -1307,9 +1206,109 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
result_table
}
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
/// Splits a path at a given progress from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed).
///
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Position on Path"), category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn split_path(_: impl Ctx, mut vector_data: VectorDataTable, progress: Fraction, parameterized_distance: bool, reverse: bool) -> VectorDataTable {
let euclidian = !parameterized_distance;
let bezpaths = vector_data
.instance_ref_iter()
.enumerate()
.flat_map(|(instance_row_index, vector_data)| vector_data.instance.stroke_bezpath_iter().map(|bezpath| (instance_row_index, bezpath)).collect::<Vec<_>>())
.collect::<Vec<_>>();
let bezpath_count = bezpaths.len() as f64;
let t_value = progress.clamp(0., bezpath_count);
let t_value = if reverse { bezpath_count - t_value } else { t_value };
let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize };
if let Some((instance_row_index, bezpath)) = bezpaths.get(index).cloned() {
let mut result_vector_data = VectorData {
style: vector_data.get(instance_row_index).unwrap().instance.style.clone(),
..Default::default()
};
for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == instance_row_index) {
result_vector_data.append_bezpath(bezpath.clone());
}
let t = if t_value == bezpath_count { 1. } else { t_value.fract() };
if let Some((first, second)) = split_bezpath(&bezpath, t, euclidian) {
result_vector_data.append_bezpath(first);
result_vector_data.append_bezpath(second);
} else {
result_vector_data.append_bezpath(bezpath);
}
*vector_data.get_mut(instance_row_index).unwrap().instance = result_vector_data;
}
vector_data
}
/// Splits path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn split_segments(_: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
for vector_data_instance in vector_data.instance_mut_iter() {
let points_count = vector_data_instance.instance.point_domain.ids().len();
let segments_count = vector_data_instance.instance.segment_domain.ids().len();
let mut point_usages = vec![0_usize; points_count];
// Count how many times each point is used as an endpoint of the segments
let start_points = vector_data_instance.instance.segment_domain.start_point().iter();
let end_points = vector_data_instance.instance.segment_domain.end_point().iter();
for (&start, &end) in start_points.zip(end_points) {
point_usages[start] += 1;
point_usages[end] += 1;
}
let mut new_points = PointDomain::new();
let mut offset_sum: usize = 0;
let mut points_with_new_offsets = Vec::with_capacity(points_count);
// Build a new point domain with the original points, but with duplications based on their extra usages by the segments
for (index, (point_id, point)) in vector_data_instance.instance.point_domain.iter().enumerate() {
// Ensure at least one usage to preserve free-floating points not connected to any segments
let usage_count = point_usages[index].max(1);
new_points.push_unchecked(point_id, point);
for i in 1..usage_count {
new_points.push_unchecked(point_id.generate_from_hash(i as u64), point);
}
points_with_new_offsets.push(offset_sum);
offset_sum += usage_count;
}
// Reconcile the segment domain with the new points
vector_data_instance.instance.point_domain = new_points;
for original_segment_index in 0..segments_count {
let original_point_start_index = vector_data_instance.instance.segment_domain.start_point()[original_segment_index];
let original_point_end_index = vector_data_instance.instance.segment_domain.end_point()[original_segment_index];
point_usages[original_point_start_index] -= 1;
point_usages[original_point_end_index] -= 1;
let start_usage = points_with_new_offsets[original_point_start_index] + point_usages[original_point_start_index];
let end_usage = points_with_new_offsets[original_point_end_index] + point_usages[original_point_end_index];
vector_data_instance.instance.segment_domain.set_start_point(original_segment_index, start_usage);
vector_data_instance.instance.segment_domain.set_end_point(original_segment_index, end_usage);
}
}
vector_data
}
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
///
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn position_on_path(
_: impl Ctx,
/// The path to traverse.
@@ -1344,8 +1343,9 @@ async fn position_on_path(
}
/// Determines the angle of the tangent at a point on the path, given by its progress from 0 to 1 along the path.
///
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Tangent on Path"), category("Vector"), path(graphene_core::vector))]
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn tangent_on_path(
_: impl Ctx,
/// The path to traverse.
@@ -1409,7 +1409,6 @@ async fn poisson_disk_points(
.stroke_bezpath_iter()
.map(|mut bezpath| {
// TODO: apply transform to points instead of modifying the paths
bezpath.apply_affine(Affine::new(vector_data_instance.transform.to_cols_array()));
bezpath.close_path();
let bbox = bezpath.bounding_box();
(bezpath, bbox)
@@ -1421,16 +1420,9 @@ async fn poisson_disk_points(
continue;
}
let mut poisson_disk_bezpath = BezPath::new();
for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation_disk_diameter, || rng.random::<f64>()) {
if poisson_disk_bezpath.elements().is_empty() {
poisson_disk_bezpath.move_to(dvec2_to_point(point));
} else {
poisson_disk_bezpath.line_to(dvec2_to_point(point));
}
result.point_domain.push(PointId::generate(), point);
}
result.append_bezpath(poisson_disk_bezpath);
}
// Transfer the style from the input vector data to the result.
@@ -1463,7 +1455,7 @@ async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> V
.collect()
}
#[node_macro::node(name("Spline"), category("Vector"), path(graphene_core::vector))]
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(graphene_core::vector))]
async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1509,7 +1501,7 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1562,7 +1554,7 @@ async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)]
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDataTable, #[default(0.5)] time: Fraction) -> VectorDataTable {
/// Subdivides the last segment of the bezpath to until it appends 'count' number of segments.
fn make_new_segments(bezpath: &mut BezPath, count: usize) {
@@ -1595,6 +1587,9 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
}
for segment in new_segments {
if bezpath.elements().is_empty() {
bezpath.move_to(segment.start())
}
bezpath.push(segment.as_path_el());
}
@@ -1670,7 +1665,10 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
for mut source_path in source_paths {
source_path.apply_affine(Affine::new(source_transform.to_cols_array()));
let end: Point = source_path.elements().last().and_then(|element| element.end_point()).unwrap_or_default();
// Skip if the path has no segments else get the point at the end of the path.
let Some(end) = source_path.segments().last().and_then(|element| Some(element.end())) else {
continue;
};
for element in source_path.elements_mut() {
match element {
@@ -1694,20 +1692,23 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
for mut target_path in target_paths {
target_path.apply_affine(Affine::new(source_transform.to_cols_array()));
let end: Point = target_path.elements().last().and_then(|element| element.end_point()).unwrap_or_default();
// Skip if the path has no segments else get the point at the start of the path.
let Some(start) = target_path.segments().next().and_then(|element| Some(element.start())) else {
continue;
};
for element in target_path.elements_mut() {
match element {
PathEl::MoveTo(point) => *point = point.lerp(end, time),
PathEl::LineTo(point) => *point = point.lerp(end, time),
PathEl::MoveTo(point) => *point = start.lerp(*point, time),
PathEl::LineTo(point) => *point = start.lerp(*point, time),
PathEl::QuadTo(point, point1) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
}
PathEl::CurveTo(point, point1, point2) => {
*point = point.lerp(end, time);
*point1 = point1.lerp(end, time);
*point2 = point2.lerp(end, time);
*point = start.lerp(*point, time);
*point1 = start.lerp(*point1, time);
*point2 = start.lerp(*point2, time);
}
PathEl::ClosePath => {}
}
@@ -1832,7 +1833,7 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2,
vector_data
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1846,7 +1847,7 @@ fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length)
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
fn close_path(_: impl Ctx, source: VectorDataTable) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
@@ -1858,24 +1859,35 @@ fn close_path(_: impl Ctx, source: VectorDataTable) -> VectorDataTable {
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point))
}
#[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))]
fn merge_by_distance(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
let mut result_table = VectorDataTable::default();
for mut source_instance in source.instance_iter() {
source_instance.instance.merge_by_distance(distance);
result_table.push(source_instance);
}
result_table
#[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 {
source.instance_iter().count() as u64
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn path_length(_: impl Ctx, source: VectorDataTable) -> f64 {
source
.instance_iter()
.map(|vector_data_instance| {
let transform = vector_data_instance.transform;
vector_data_instance
.instance
.stroke_bezpath_iter()
.map(|mut bezpath| {
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
bezpath.perimeter(DEFAULT_ACCURACY)
})
.sum::<f64>()
})
.sum()
}
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector_data = vector_data.eval(new_ctx).await;
@@ -1889,7 +1901,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<
.sum()
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>, centroid_type: CentroidType) -> DVec2 {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector_data = vector_data.eval(new_ctx).await;
@@ -1947,13 +1959,14 @@ mod test {
use super::*;
use crate::Node;
use bezier_rs::Bezier;
use kurbo::Rect;
use std::pin::Pin;
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, T: 'i + Clone + Send> Node<'i, Footprint> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: Footprint) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
@@ -1964,6 +1977,24 @@ mod test {
VectorDataTable::new(VectorData::from_subpath(data))
}
fn create_vector_data_instance(bezpath: BezPath, transform: DAffine2) -> Instance<VectorData> {
let mut instance = VectorData::default();
instance.append_bezpath(bezpath);
Instance {
instance,
transform,
..Default::default()
}
}
fn vector_node_from_instances(data: Vec<Instance<VectorData>>) -> VectorDataTable {
let mut vector_data_table = VectorDataTable::default();
for instance in data {
vector_data_table.push(instance);
}
vector_data_table
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
@@ -2015,7 +2046,7 @@ mod test {
// Test a VectorData with non-zero rotation
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
let mut square = VectorDataTable::new(square);
*square.get_mut(0).unwrap().transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
*square.get_mut(0).unwrap().transform *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4);
let bounding_box = BoundingBoxNode {
vector_data: FutureWrapperNode(square),
}
@@ -2051,51 +2082,63 @@ mod test {
}
}
#[tokio::test]
async fn sample_points() {
async fn sample_polyline() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0., 0., 0., false, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[tokio::test]
async fn adaptive_spacing() {
async fn sample_polyline_adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0., 45., 10., true, vec![100.]).await;
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[tokio::test]
async fn poisson() {
let sample_points = super::poisson_disk_points(
let poisson_points = super::poisson_disk_points(
Footprint::default(),
vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)),
10. * std::f64::consts::SQRT_2,
0,
)
.await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
let poisson_points = poisson_points.instance_ref_iter().next().unwrap().instance;
assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()),
(20..=40).contains(&poisson_points.point_domain.positions().len()),
"actual len {}",
sample_points.point_domain.positions().len()
poisson_points.point_domain.positions().len()
);
for point in sample_points.point_domain.positions() {
for point in poisson_points.point_domain.positions() {
assert!(point.length() < 50. + 1., "Expected point in circle {point}")
}
}
#[tokio::test]
async fn lengths() {
async fn segment_lengths() {
let subpath = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let lengths = subpath_segment_lengths(Footprint::default(), vector_node(subpath)).await;
assert_eq!(lengths, vec![100.]);
}
#[tokio::test]
async fn path_length() {
let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY);
let transform = DAffine2::from_scale(DVec2::new(2., 2.));
let instance = create_vector_data_instance(bezpath, transform);
let instances = (0..5).map(|_| instance.clone()).collect::<Vec<Instance<VectorData>>>();
let length = super::path_length(Footprint::default(), vector_node_from_instances(instances)).await;
// 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows)
assert_eq!(length, 101. * 4. * 2. * 5.);
}
#[tokio::test]
async fn spline() {
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
let spline = spline.instance_ref_iter().next().unwrap().instance;
@@ -2106,16 +2149,16 @@ mod test {
async fn morph() {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
let morphed = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
let morphed = morphed.instance_ref_iter().next().unwrap().instance;
assert_eq!(
&sample_points.point_domain.positions()[..4],
&morphed.point_domain.positions()[..4],
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
);
}
#[track_caller]
fn contains_segment(vector: VectorData, target: bezier_rs::Bezier) {
fn contains_segment(vector: VectorData, target: Bezier) {
let segments = vector.segment_bezier_iter().map(|x| x.1);
let count = segments.filter(|bezier| bezier.abs_diff_eq(&target, 0.01) || bezier.reversed().abs_diff_eq(&target, 0.01)).count();
assert_eq!(
@@ -2136,16 +2179,16 @@ mod test {
assert_eq!(beveled.segment_domain.ids().len(), 8);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
// Joins
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
}
#[tokio::test]
@@ -2159,12 +2202,12 @@ mod test {
assert_eq!(beveled.segment_domain.ids().len(), 3);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
contains_segment(beveled.clone(), trimmed);
// Join
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
}
#[tokio::test]
@@ -2183,12 +2226,12 @@ mod test {
assert_eq!(beveled.segment_domain.ids().len(), 3);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-10., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-10., 0.)));
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
contains_segment(beveled.clone(), trimmed);
// Join
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
}
#[tokio::test]
@@ -2201,13 +2244,13 @@ mod test {
assert_eq!(beveled.segment_domain.ids().len(), 5);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
// Joins
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
}
#[tokio::test]
@@ -2222,11 +2265,11 @@ mod test {
assert_eq!(beveled.segment_domain.ids().len(), 5);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
contains_segment(beveled.clone(), point);
let [start, end] = curve.split(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(start.start, start.end));
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(start.start, start.end));
contains_segment(beveled.clone(), end);
}
}