Merge branch 'master' into fix-range

This commit is contained in:
mTvare
2025-08-02 17:12:36 +05:30
committed by GitHub
306 changed files with 13655 additions and 7691 deletions

View File

@@ -14,10 +14,12 @@ wgpu = ["dep:wgpu"]
dealloc_nodes = []
[dependencies]
# Local dependencies
graphene-core-shaders = { workspace = true, features = ["std"] }
# Workspace dependencies
bytemuck = { workspace = true }
node-macro = { workspace = true }
num-derive = { workspace = true }
num-traits = { workspace = true }
rand = { workspace = true }
glam = { workspace = true }
@@ -30,7 +32,6 @@ rand_chacha = { workspace = true }
bezier-rs = { workspace = true }
specta = { workspace = true }
image = { workspace = true }
half = { workspace = true }
tinyvec = { workspace = true }
parley = { workspace = true }
skrifa = { workspace = true }
@@ -46,9 +47,3 @@ wgpu = { workspace = true, optional = true }
# Workspace dependencies
tokio = { workspace = true }
serde_json = { workspace = true }
[lints.rust]
# the spirv target is not in the list of common cfgs so must be added manually
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(target_arch, values("spirv"))',
] }

View File

@@ -1,240 +0,0 @@
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"),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,205 +0,0 @@
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

@@ -1,178 +0,0 @@
#![allow(clippy::neg_cmp_op_on_partial_ord)]
//! Fast conversions between u8 sRGB and linear float.
// Inspired by https://gist.github.com/rygorous/2203834, but with a slightly
// modified method, custom derived constants and error correction for perfect
// accuracy in accordance with the D3D11 spec:
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB.
/// CRITICAL_POINTS[i] is the last float value such that it maps to i after
/// conversion to integer sRGB. So if x > CRITICAL_POINTS[i] you know you need
/// to increment i.
#[rustfmt::skip]
const CRITICAL_POINTS: [f32; 256] = [
0.00015176347, 0.00045529046, 0.0007588174, 0.0010623443, 0.0013658714, 0.0016693983, 0.0019729252, 0.0022764523,
0.0025799791, 0.0028835062, 0.0031883009, 0.003509259, 0.003848315, 0.004205748, 0.0045818323, 0.0049768374,
0.005391024, 0.00582465, 0.0062779686, 0.0067512267, 0.0072446675, 0.0077585294, 0.008293047, 0.008848451,
0.0094249705, 0.010022825, 0.010642236, 0.01128342, 0.011946591, 0.012631957, 0.013339729, 0.014070111,
0.0148233045, 0.015599505, 0.01639891, 0.017221717, 0.018068114, 0.018938294, 0.019832445, 0.020750746,
0.021693384, 0.022660539, 0.02365239, 0.024669115, 0.025710886, 0.026777886, 0.027870273, 0.028988222,
0.030131903, 0.03130148, 0.032497127, 0.033718992, 0.034967244, 0.03624204, 0.03754355, 0.03887192,
0.040227327, 0.041609894, 0.04301979, 0.044457167, 0.04592218, 0.04741497, 0.04893569, 0.050484486,
0.05206151, 0.053666897, 0.055300802, 0.056963358, 0.058654714, 0.060375024, 0.062124394, 0.06390298,
0.065710925, 0.06754836, 0.06941542, 0.07131224, 0.07323896, 0.07519571, 0.07718261, 0.07919981,
0.08124744, 0.08332562, 0.08543448, 0.08757417, 0.08974478, 0.091946445, 0.09417931, 0.09644348,
0.098739095, 0.10106628, 0.10342514, 0.105815805, 0.1082384, 0.110693045, 0.11317986, 0.11569896,
0.118250474, 0.12083454, 0.12345121, 0.12610064, 0.12878296, 0.13149826, 0.13424668, 0.1370283,
0.13984327, 0.14269169, 0.14557366, 0.1484893, 0.15143873, 0.15442204, 0.15743938, 0.16049084,
0.1635765, 0.16669647, 0.16985092, 0.1730399, 0.17626354, 0.17952198, 0.18281525, 0.1861435,
0.18950681, 0.19290532, 0.19633913, 0.19980833, 0.20331302, 0.20685332, 0.21042931, 0.21404111,
0.21768881, 0.22137253, 0.22509235, 0.22884844, 0.23264077, 0.23646952, 0.24033478, 0.24423665,
0.24817522, 0.25215057, 0.25616285, 0.26021212, 0.26429847, 0.26842204, 0.27258286, 0.27678108,
0.2810168, 0.28529006, 0.289601, 0.2939497, 0.29833627, 0.30276078, 0.30722332, 0.311724,
0.31626293, 0.32084015, 0.32545578, 0.33010995, 0.3348027, 0.3395341, 0.34430432, 0.34911346,
0.3539615, 0.35884857, 0.3637748, 0.36874023, 0.373745, 0.37878913, 0.38387278, 0.388996,
0.39415887, 0.39936152, 0.404604, 0.4098864, 0.41520882, 0.42057133, 0.425974, 0.431417,
0.43690032, 0.4424241, 0.44798836, 0.45359328, 0.45923886, 0.46492523, 0.47065246, 0.47642064,
0.48222986, 0.48808017, 0.4939718, 0.49990457, 0.5058787, 0.5118943, 0.5179514, 0.5240501,
0.5301905, 0.5363727, 0.5425967, 0.54886264, 0.5551706, 0.56152064, 0.5679129, 0.5743473,
0.5808241, 0.5873433, 0.593905, 0.60050917, 0.60715604, 0.61384565, 0.62057805, 0.6273533,
0.63417155, 0.6410328, 0.6479372, 0.65488476, 0.66187555, 0.6689097, 0.6759874, 0.68310845,
0.6902731, 0.6974814, 0.7047334, 0.71202916, 0.7193688, 0.7267524, 0.73418003, 0.7416518,
0.7491677, 0.7567278, 0.76433223, 0.7719811, 0.7796744, 0.7874122, 0.7951947, 0.80302185,
0.8108938, 0.81881046, 0.82677215, 0.8347787, 0.8428304, 0.8509272, 0.85906917, 0.8672564,
0.875489, 0.8837671, 0.89209044, 0.9004596, 0.9088741, 0.91733456, 0.9258405, 0.9343926,
0.94299024, 0.95163417, 0.96032387, 0.96906, 0.977842, 0.9866705, 0.9955452, 1.,
];
#[rustfmt::skip]
const FLOAT_SRGB_LERP: [u32; 27] = [
0x66f, 0x66f063b, 0xcaa0515, 0x11c00773, 0x193305dc, 0x1f1004f3, 0x24030481, 0x28850773,
0x2ff9065e, 0x365805a1, 0x3bfa0547, 0x414108f7, 0x4a3907d8, 0x52110709, 0x591b06aa, 0x5fc50b70,
0x6b350a18, 0x754e091c, 0x7e6b08aa, 0x87160ef1, 0x96070d3e, 0xa3460bfc, 0xaf430b6c, 0xbaaf13bd,
0xce6d1187, 0xdff40fe3, 0xefd70f28,
];
#[inline]
pub fn float_to_srgb_u8(mut f: f32) -> u8 {
// Clamp f to [0, 1], with a negated condition to handle NaNs as 0.
if !(f >= 0.) {
f = 0.;
} else if f > 1. {
f = 1.;
}
// Shift away slightly from 0.0 to reduce exponent range.
const C: f32 = 0.009842521f32;
let u = (f + C).to_bits() - C.to_bits();
if u > (1. + C).to_bits() - C.to_bits() {
// 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 { std::hint::unreachable_unchecked() }
}
// Compute a piecewise linear interpolation that is always
// the correct answer, or one less than it.
let u16mask = (1 << 16) - 1;
let lut_idx = u >> 21;
let lerp_idx = (u >> 5) & u16mask;
let bias_mult = FLOAT_SRGB_LERP[lut_idx as usize];
let bias = (bias_mult >> 16) << 16;
let mult = bias_mult & u16mask;
// I don't believe this wraps, but since we test in release mode,
// better make sure debug mode behaves the same.
let lerp = bias.wrapping_add(mult * lerp_idx) >> 24;
// Adjust linear interpolation to the correct value.
if f > CRITICAL_POINTS[lerp as usize] { lerp as u8 + 1 } else { lerp as u8 }
}
#[rustfmt::skip]
const FROM_SRGB_U8: [f32; 256] = [
0., 0.000303527, 0.000607054, 0.00091058103, 0.001214108, 0.001517635, 0.0018211621, 0.002124689,
0.002428216, 0.002731743, 0.00303527, 0.0033465356, 0.003676507, 0.004024717, 0.004391442,
0.0047769533, 0.005181517, 0.0056053917, 0.0060488326, 0.006512091, 0.00699541, 0.0074990317,
0.008023192, 0.008568125, 0.009134057, 0.009721218, 0.010329823, 0.010960094, 0.011612245,
0.012286487, 0.012983031, 0.013702081, 0.014443844, 0.015208514, 0.015996292, 0.016807375,
0.017641952, 0.018500218, 0.019382361, 0.020288562, 0.02121901, 0.022173883, 0.023153365,
0.02415763, 0.025186857, 0.026241222, 0.027320892, 0.028426038, 0.029556843, 0.03071345, 0.03189604,
0.033104774, 0.03433981, 0.035601325, 0.036889452, 0.038204376, 0.039546248, 0.04091521, 0.042311423,
0.043735042, 0.045186214, 0.046665095, 0.048171833, 0.049706575, 0.051269468, 0.052860655, 0.05448028,
0.056128494, 0.057805434, 0.05951124, 0.06124607, 0.06301003, 0.06480328, 0.06662595, 0.06847818,
0.07036011, 0.07227186, 0.07421358, 0.07618539, 0.07818743, 0.08021983, 0.082282715, 0.084376216,
0.086500466, 0.088655606, 0.09084173, 0.09305898, 0.095307484, 0.09758736, 0.09989874, 0.10224175,
0.10461649, 0.10702311, 0.10946172, 0.111932434, 0.11443538, 0.116970696, 0.11953845, 0.12213881,
0.12477186, 0.12743773, 0.13013652, 0.13286836, 0.13563336, 0.13843165, 0.14126332, 0.1441285,
0.1470273, 0.14995982, 0.15292618, 0.1559265, 0.15896086, 0.16202943, 0.16513224, 0.16826946,
0.17144115, 0.17464745, 0.17788847, 0.1811643, 0.18447503, 0.1878208, 0.19120172, 0.19461787,
0.19806935, 0.2015563, 0.20507877, 0.2086369, 0.21223079, 0.21586053, 0.21952623, 0.22322798,
0.22696589, 0.23074007, 0.23455065, 0.23839766, 0.2422812, 0.2462014, 0.25015837, 0.25415218,
0.2581829, 0.26225072, 0.26635566, 0.27049786, 0.27467737, 0.27889434, 0.2831488, 0.2874409,
0.2917707, 0.29613832, 0.30054384, 0.30498737, 0.30946895, 0.31398875, 0.31854683, 0.32314324,
0.32777813, 0.33245158, 0.33716366, 0.34191445, 0.3467041, 0.3515327, 0.35640025, 0.36130688,
0.3662527, 0.37123778, 0.37626222, 0.3813261, 0.38642952, 0.39157256, 0.3967553, 0.40197787,
0.4072403, 0.4125427, 0.41788515, 0.42326775, 0.42869055, 0.4341537, 0.43965724, 0.44520125,
0.45078585, 0.45641106, 0.46207705, 0.46778384, 0.47353154, 0.47932023, 0.48514998, 0.4910209,
0.49693304, 0.5028866, 0.50888145, 0.5149178, 0.5209957, 0.52711535, 0.5332766, 0.5394797,
0.5457247, 0.5520116, 0.5583406, 0.5647117, 0.57112503, 0.57758063, 0.5840786, 0.590619, 0.597202,
0.60382754, 0.61049575, 0.61720675, 0.62396055, 0.63075733, 0.637597, 0.6444799, 0.6514058,
0.65837497, 0.66538745, 0.67244333, 0.6795426, 0.68668544, 0.69387203, 0.70110214, 0.70837605,
0.7156938, 0.72305536, 0.730461, 0.7379107, 0.7454045, 0.75294244, 0.76052475, 0.7681514, 0.77582246,
0.78353804, 0.79129815, 0.79910296, 0.8069525, 0.8148468, 0.822786, 0.8307701, 0.83879924, 0.84687346,
0.8549928, 0.8631574, 0.87136734, 0.8796226, 0.8879232, 0.89626956, 0.90466136, 0.913099, 0.92158204,
0.93011117, 0.9386859, 0.9473069, 0.9559735, 0.9646866, 0.9734455, 0.98225087, 0.9911022, 1.,
];
#[inline]
pub fn srgb_u8_to_float(c: u8) -> f32 {
FROM_SRGB_U8[c as usize]
}
#[cfg(test)]
mod tests {
use super::*;
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB
fn float_to_srgb_ref(f: f32) -> f32 {
if !(f > 0_f32) {
0_f32
} else if f <= 0.0031308f32 {
12.92_f32 * f
} else if f < 1_f32 {
1.055f32 * f.powf(1_f32 / 2.4_f32) - 0.055f32
} else {
1_f32
}
}
fn float_to_srgb_u8_ref(f: f32) -> u8 {
(float_to_srgb_ref(f) * 255_f32 + 0.5_f32) as u8
}
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#SRGBtoFLOAT
fn srgb_to_float_ref(f: f32) -> f32 {
if f <= 0.04045f32 { f / 12.92f32 } else { ((f + 0.055f32) / 1.055f32).powf(2.4_f32) }
}
fn srgb_u8_to_float_ref(c: u8) -> f32 {
srgb_to_float_ref(c as f32 * (1_f32 / 255_f32))
}
#[test]
fn test_float_to_srgb_u8() {
for u in 0..=u8::MAX {
assert!(srgb_u8_to_float(u) == srgb_u8_to_float_ref(u));
}
}
#[ignore = "expensive, test in release mode"]
#[test]
fn test_srgb_u8_to_float() {
// Simply... check all float values.
for u in 0..=u32::MAX {
let f = f32::from_bits(u);
assert!(float_to_srgb_u8(f) == float_to_srgb_u8_ref(f));
}
}
}

View File

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

View File

@@ -1,11 +1,10 @@
use crate::transform::Footprint;
pub use graphene_core_shaders::context::{ArcCtx, Ctx};
use std::any::Any;
use std::borrow::Borrow;
use std::panic::Location;
use std::sync::Arc;
pub trait Ctx: Clone + Send {}
pub trait ExtractFootprint {
#[track_caller]
fn try_footprint(&self) -> Option<&Footprint>;
@@ -27,7 +26,7 @@ pub trait ExtractAnimationTime {
}
pub trait ExtractIndex {
fn try_index(&self) -> Option<usize>;
fn try_index(&self) -> Option<Vec<usize>>;
}
// Consider returning a slice or something like that
@@ -51,9 +50,6 @@ pub enum VarArgsResult {
IndexOutOfBounds,
NoVarArgs,
}
impl<T: Ctx> Ctx for Option<T> {}
impl<T: Ctx + Sync> Ctx for &T {}
impl Ctx for () {}
impl Ctx for Footprint {}
impl ExtractFootprint for () {
fn try_footprint(&self) -> Option<&Footprint> {
@@ -91,7 +87,7 @@ impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Option<T> {
}
}
impl<T: ExtractIndex> ExtractIndex for Option<T> {
fn try_index(&self) -> Option<usize> {
fn try_index(&self) -> Option<Vec<usize>> {
self.as_ref().and_then(|x| x.try_index())
}
}
@@ -122,7 +118,7 @@ impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Arc<T> {
}
}
impl<T: ExtractIndex> ExtractIndex for Arc<T> {
fn try_index(&self) -> Option<usize> {
fn try_index(&self) -> Option<Vec<usize>> {
(**self).try_index()
}
}
@@ -157,7 +153,7 @@ impl<T: CloneVarArgs + Sync> CloneVarArgs for Arc<T> {
}
impl Ctx for ContextImpl<'_> {}
impl Ctx for Arc<OwnedContextImpl> {}
impl ArcCtx for OwnedContextImpl {}
impl ExtractFootprint for ContextImpl<'_> {
fn try_footprint(&self) -> Option<&Footprint> {
@@ -170,8 +166,8 @@ impl ExtractTime for ContextImpl<'_> {
}
}
impl ExtractIndex for ContextImpl<'_> {
fn try_index(&self) -> Option<usize> {
self.index
fn try_index(&self) -> Option<Vec<usize>> {
self.index.clone()
}
}
impl ExtractVarArgs for ContextImpl<'_> {
@@ -202,8 +198,8 @@ impl ExtractAnimationTime for OwnedContextImpl {
}
}
impl ExtractIndex for OwnedContextImpl {
fn try_index(&self) -> Option<usize> {
self.index
fn try_index(&self) -> Option<Vec<usize>> {
self.index.clone()
}
}
impl ExtractVarArgs for OwnedContextImpl {
@@ -244,7 +240,7 @@ pub struct OwnedContextImpl {
varargs: Option<Arc<[DynBox]>>,
parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
// This could be converted into a single enum to save extra bytes
index: Option<usize>,
index: Option<Vec<usize>>,
real_time: Option<f64>,
animation_time: Option<f64>,
}
@@ -334,7 +330,11 @@ impl OwnedContextImpl {
self
}
pub fn with_index(mut self, index: usize) -> Self {
self.index = Some(index);
if let Some(current_index) = &mut self.index {
current_index.push(index);
} else {
self.index = Some(vec![index]);
}
self
}
pub fn into_context(self) -> Option<Arc<Self>> {
@@ -346,12 +346,12 @@ impl OwnedContextImpl {
}
}
#[derive(Default, Clone, Copy, dyn_any::DynAny)]
#[derive(Default, Clone, dyn_any::DynAny)]
pub struct ContextImpl<'a> {
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>,
index: Option<Vec<usize>>,
time: Option<f64>,
}
@@ -363,6 +363,7 @@ impl<'a> ContextImpl<'a> {
ContextImpl {
footprint: Some(new_footprint),
varargs: varargs.map(|x| x.borrow()),
index: self.index.clone(),
..*self
}
}

View File

@@ -2,9 +2,9 @@ use crate::Ctx;
use dyn_any::DynAny;
use glam::{DVec2, IVec2, UVec2};
/// Obtains the X or Y component of a coordinate point.
/// Obtains the X or Y component of a vec2.
///
/// The inverse of this node is "Coordinate Value", which can have either or both its X and Y exposed as graph inputs.
/// The inverse of this node is "Vec2 Value", which can have either or both its X and Y parameters exposed as graph inputs.
#[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 {
@@ -13,7 +13,7 @@ fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
}
}
/// The X or Y component of a coordinate.
/// The X or Y component of a vec2.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
#[widget(Dropdown)]
pub enum XY {

View File

@@ -100,6 +100,11 @@ impl From<RasterDataTable<GPU>> for GraphicGroupTable {
Self::new(GraphicElement::RasterDataGPU(raster_data_table))
}
}
impl From<DAffine2> for GraphicGroupTable {
fn from(_: DAffine2) -> Self {
GraphicGroupTable::default()
}
}
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
@@ -118,6 +123,12 @@ impl Default for GraphicElement {
}
}
impl From<DAffine2> for GraphicElement {
fn from(_: DAffine2) -> Self {
GraphicElement::default()
}
}
impl GraphicElement {
pub fn as_group(&self) -> Option<&GraphicGroupTable> {
match self {
@@ -201,41 +212,6 @@ impl BoundingBox for GraphicGroupTable {
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Raster::new_cpu(Image::deserialize(deserializer)?))
}
}
impl serde::Serialize for Raster<CPU> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.data().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for Raster<GPU> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
unimplemented!()
}
}
impl serde::Serialize for Raster<GPU> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
unimplemented!()
}
}
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct Artboard {
@@ -351,6 +327,7 @@ async fn to_element<Data: Into<GraphicElement> + 'n>(
VectorDataTable,
RasterDataTable<CPU>,
RasterDataTable<GPU>,
DAffine2,
)]
data: Data,
) -> GraphicElement {
@@ -463,14 +440,18 @@ async fn to_artboard<Data: Into<GraphicGroupTable> + 'n>(
Context -> VectorDataTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> DAffine2,
)]
contents: impl Node<Context<'static>, Output = Data>,
label: String,
location: IVec2,
dimensions: IVec2,
location: DVec2,
dimensions: DVec2,
background: Color,
clip: bool,
) -> Artboard {
let location = location.as_ivec2();
let dimensions = dimensions.as_ivec2().max(IVec2::ONE);
let footprint = ctx.try_footprint().copied();
let mut new_ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {

View File

@@ -1,4 +1,5 @@
use crate::AlphaBlending;
use crate::transform::ApplyTransform;
use crate::uuid::NodeId;
use dyn_any::StaticType;
use glam::DAffine2;
@@ -26,6 +27,24 @@ impl<T> Instances<T> {
}
}
pub fn new_instance(instance: Instance<T>) -> Self {
Self {
instance: vec![instance.instance],
transform: vec![instance.transform],
alpha_blending: vec![instance.alpha_blending],
source_node_id: vec![instance.source_node_id],
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
instance: Vec::with_capacity(capacity),
transform: Vec::with_capacity(capacity),
alpha_blending: Vec::with_capacity(capacity),
source_node_id: Vec::with_capacity(capacity),
}
}
pub fn push(&mut self, instance: Instance<T>) {
self.instance.push(instance.instance);
self.transform.push(instance.transform);
@@ -136,6 +155,20 @@ impl<T: Hash> Hash for Instances<T> {
}
}
impl<T> ApplyTransform for Instances<T> {
fn apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform *= *modification;
}
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
for transform in &mut self.transform {
*transform = *modification * *transform;
}
}
}
impl<T: PartialEq> PartialEq for Instances<T> {
fn eq(&self, other: &Self) -> bool {
self.instance.len() == other.instance.len() && { self.instance.iter().zip(other.instance.iter()).all(|(a, b)| a == b) }
@@ -146,6 +179,18 @@ unsafe impl<T: StaticType + 'static> StaticType for Instances<T> {
type Static = Instances<T>;
}
impl<T> FromIterator<Instance<T>> for Instances<T> {
fn from_iter<I: IntoIterator<Item = Instance<T>>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut instances = Self::with_capacity(lower);
for instance in iter {
instances.push(instance);
}
instances
}
}
fn one_daffine2_default() -> Vec<DAffine2> {
vec![DAffine2::IDENTITY]
}

View File

@@ -2,10 +2,8 @@
extern crate log;
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;
@@ -33,13 +31,17 @@ pub mod vector;
pub use crate as graphene_core;
pub use blending::*;
pub use color::Color;
pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphene_core_shaders::AsU32;
pub use graphene_core_shaders::blending;
pub use graphene_core_shaders::choice_type;
pub use graphene_core_shaders::color;
pub use graphic_element::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable};
pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color;
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
@@ -165,12 +167,3 @@ pub trait NodeInputDecleration {
fn identifier() -> ProtoNodeIdentifier;
type Result;
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}

View File

@@ -10,10 +10,18 @@ use crate::{Context, Ctx};
use glam::{DAffine2, DVec2};
#[node_macro::node(category("Text"))]
fn to_string<T: std::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, DAffine2, VectorDataTable)] value: T) -> String {
format!("{:?}", value)
}
#[node_macro::node(category("Text"))]
fn serialize<T: serde::Serialize>(
_: impl Ctx,
#[implementations(String, bool, f64, u32, u64, DVec2, DAffine2, Color, Option<Color>, GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] value: T,
) -> String {
serde_json::to_string(&value).unwrap_or_else(|_| "Serialization Error".to_string())
}
#[node_macro::node(category("Text"))]
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
first.clone() + &second
@@ -33,8 +41,8 @@ fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f
}
#[node_macro::node(category("Text"))]
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize {
string.len()
fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> u32 {
string.chars().count() as u32
}
#[node_macro::node(category("Math: Logic"))]

View File

@@ -1,12 +1,3 @@
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
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::*;
@@ -15,6 +6,11 @@ pub mod color {
pub mod image;
pub use self::image::Image;
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::vector::VectorDataTable;
use std::fmt::Debug;
pub trait Bitmap {
type Pixel: Pixel;

View File

@@ -50,6 +50,13 @@ pub struct Image<P: Pixel> {
// 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).
}
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct TransformImage(pub DAffine2);
impl Hash for TransformImage {
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
}
impl<P: Pixel + Debug> Debug for Image<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length = self.data.len();

View File

@@ -6,136 +6,202 @@ use crate::raster::Image;
use core::ops::Deref;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[cfg(feature = "wgpu")]
use std::sync::Arc;
use std::fmt::Debug;
use std::ops::DerefMut;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct CPU;
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
pub struct GPU;
mod __private {
pub trait Sealed {}
}
trait Storage: 'static {}
impl Storage for CPU {}
impl Storage for GPU {}
pub trait Storage: __private::Sealed + Clone + Debug + 'static {
fn is_empty(&self) -> bool;
}
#[derive(Clone, Debug, Hash, PartialEq)]
#[allow(private_bounds)]
pub struct Raster<T: Storage> {
data: RasterStorage,
#[derive(Clone, Debug, PartialEq, Hash, Default)]
pub struct Raster<T>
where
Raster<T>: Storage,
{
storage: T,
}
unsafe impl<T: Storage> dyn_any::StaticType for Raster<T> {
unsafe impl<T> dyn_any::StaticType for Raster<T>
where
Raster<T>: Storage,
{
type Static = Raster<T>;
}
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
pub enum RasterStorage {
Cpu(Image<Color>),
#[cfg(feature = "wgpu")]
Gpu(Arc<wgpu::Texture>),
#[cfg(not(feature = "wgpu"))]
Gpu(()),
impl<T> Raster<T>
where
Raster<T>: Storage,
{
pub fn new(t: T) -> Self {
Self { storage: t }
}
}
impl RasterStorage {}
impl Raster<CPU> {
pub fn new_cpu(image: Image<Color>) -> Self {
Self {
data: RasterStorage::Cpu(image),
storage: CPU,
}
}
pub fn data(&self) -> &Image<Color> {
let RasterStorage::Cpu(cpu) = &self.data else { unreachable!() };
cpu
}
pub fn data_mut(&mut self) -> &mut Image<Color> {
let RasterStorage::Cpu(cpu) = &mut self.data else { unreachable!() };
cpu
}
pub fn into_data(self) -> Image<Color> {
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 {
Self {
data: RasterStorage::Cpu(Image::default()),
storage: CPU,
}
}
}
impl Deref for Raster<CPU> {
type Target = Image<Color>;
impl<T> Deref for Raster<T>
where
Raster<T>: Storage,
{
type Target = T;
fn deref(&self) -> &Self::Target {
self.data()
}
}
#[cfg(feature = "wgpu")]
impl Raster<GPU> {
pub fn new_gpu(image: Arc<wgpu::Texture>) -> Self {
Self {
data: RasterStorage::Gpu(image),
storage: GPU,
}
}
pub fn data(&self) -> &wgpu::Texture {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu
}
pub fn data_mut(&mut self) -> &mut Arc<wgpu::Texture> {
let RasterStorage::Gpu(gpu) = &mut self.data else { unreachable!() };
gpu
}
pub fn data_owned(&self) -> Arc<wgpu::Texture> {
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
gpu.clone()
&self.storage
}
}
impl Raster<GPU> {
#[cfg(feature = "wgpu")]
pub fn is_empty(&self) -> bool {
let data = self.data();
data.width() == 0 || data.height() == 0
}
#[cfg(not(feature = "wgpu"))]
pub fn is_empty(&self) -> bool {
true
}
}
#[cfg(feature = "wgpu")]
impl Deref for Raster<GPU> {
type Target = wgpu::Texture;
fn deref(&self) -> &Self::Target {
self.data()
impl<T> DerefMut for Raster<T>
where
Raster<T>: Storage,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.storage
}
}
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)
pub use cpu::CPU;
mod cpu {
use super::*;
use crate::raster_types::__private::Sealed;
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
pub struct CPU(Image<Color>);
impl Sealed for Raster<CPU> {}
impl Storage for Raster<CPU> {
fn is_empty(&self) -> bool {
self.0.height == 0 || self.0.width == 0
}
}
impl Raster<CPU> {
pub fn new_cpu(image: Image<Color>) -> Self {
Self::new(CPU(image))
}
pub fn data(&self) -> &Image<Color> {
self
}
pub fn data_mut(&mut self) -> &mut Image<Color> {
self
}
pub fn into_data(self) -> Image<Color> {
self.storage.0
}
}
impl Deref for CPU {
type Target = Image<Color>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CPU {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Raster::new_cpu(Image::deserialize(deserializer)?))
}
}
impl serde::Serialize for Raster<CPU> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
}
impl BoundingBox for RasterDataTable<GPU> {
pub use gpu::GPU;
#[cfg(feature = "wgpu")]
mod gpu {
use super::*;
use crate::raster_types::__private::Sealed;
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct GPU {
texture: wgpu::Texture,
}
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
self.texture.width() == 0 || self.texture.height() == 0
}
}
impl Raster<GPU> {
pub fn new_gpu(texture: wgpu::Texture) -> Self {
Self::new(GPU { texture })
}
pub fn data(&self) -> &wgpu::Texture {
&self.texture
}
}
}
#[cfg(not(feature = "wgpu"))]
mod gpu {
use super::*;
#[derive(Clone, Debug)]
pub struct GPU;
impl Storage for Raster<GPU> {
fn is_empty(&self) -> bool {
true
}
}
}
mod gpu_common {
use super::*;
impl<'de> serde::Deserialize<'de> for Raster<GPU> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
unimplemented!()
}
}
impl serde::Serialize for Raster<GPU> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
unimplemented!()
}
}
}
impl<T> BoundingBox for RasterDataTable<T>
where
Raster<T>: Storage,
{
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

View File

@@ -1,38 +1,12 @@
use crate::{Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
use dyn_any::{DynAny, StaticType};
use std::borrow::Cow;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::{LazyLock, Mutex};
pub mod types {
/// 0% - 100%
pub type Percentage = f64;
/// -100% - 100%
pub type SignedPercentage = f64;
/// -180° - 180°
pub type Angle = f64;
/// Ends in the unit of x
pub type Multiplier = f64;
/// Non-negative integer with px unit
pub type PixelLength = f64;
/// Non-negative
pub type Length = f64;
/// 0 to 1
pub type Fraction = f64;
/// Unsigned integer
pub type IntegerCount = u32;
/// Unsigned integer to be used for random seeds
pub type SeedValue = u32;
/// Non-negative integer coordinate with px unit
pub type Resolution = glam::UVec2;
/// DVec2 with px unit
pub type PixelSize = glam::DVec2;
/// String with one or more than one line
pub type TextArea = String;
}
pub use graphene_core_shaders::registry::types;
// Translation struct between macro and definition
#[derive(Clone)]
@@ -63,33 +37,6 @@ pub struct FieldMetadata {
pub unit: Option<&'static str>,
}
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
}
pub enum ChoiceWidgetHint {
Dropdown,
RadioButtons,
}
/// Translation struct between macro and definition.
#[derive(Clone, Debug)]
pub struct VariantMetadata {
/// Name as declared in source code.
pub name: Cow<'static, str>,
/// Name to be displayed in UI.
pub label: Cow<'static, str>,
/// User-facing documentation text.
pub docstring: Option<Cow<'static, str>>,
/// Name of icon to display in radio buttons and such.
pub icon: Option<Cow<'static, str>>,
}
#[derive(Clone, Debug)]
pub enum RegistryWidgetOverride {
None,

View File

@@ -1,5 +1,31 @@
mod font_cache;
mod to_path;
use dyn_any::DynAny;
pub use font_cache::*;
pub use to_path::*;
/// Alignment of lines of type within a text block.
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
#[label("Justify")]
JustifyLeft,
// TODO: JustifyCenter, JustifyRight, JustifyAll
}
impl From<TextAlign> for parley::Alignment {
fn from(val: TextAlign) -> Self {
match val {
TextAlign::Left => parley::Alignment::Left,
TextAlign::Center => parley::Alignment::Middle,
TextAlign::Right => parley::Alignment::Right,
TextAlign::JustifyLeft => parley::Alignment::Justified,
}
}
}

View File

@@ -1,9 +1,11 @@
use crate::vector::PointId;
use super::TextAlign;
use crate::instances::Instance;
use crate::vector::{PointId, VectorData, VectorDataTable};
use bezier_rs::{ManipulatorGroup, Subpath};
use core::cell::RefCell;
use glam::{DAffine2, DVec2};
use parley::fontique::Blob;
use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
use skrifa::GlyphId;
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen};
@@ -20,24 +22,20 @@ thread_local! {
struct PathBuilder {
current_subpath: Subpath<PointId>,
glyph_subpaths: Vec<Subpath<PointId>>,
other_subpaths: Vec<Subpath<PointId>>,
origin: DVec2,
glyph_subpaths: Vec<Subpath<PointId>>,
vector_table: VectorDataTable,
scale: f64,
id: PointId,
}
impl PathBuilder {
fn point(&self, x: f32, y: f32) -> DVec2 {
// Y-axis inversion converts from font coordinate system (Y-up) to graphics coordinate system (Y-down)
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
}
fn set_origin(&mut self, x: f64, y: f64) {
self.origin = DVec2::new(x, y);
}
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], style_skew: Option<DAffine2>, skew: DAffine2) {
#[allow(clippy::too_many_arguments)]
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
let location_ref = LocationRef::new(normalized_coords);
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
glyph.draw(settings, self).unwrap();
@@ -52,8 +50,17 @@ impl PathBuilder {
glyph_subpath.apply_transform(skew);
}
if !self.glyph_subpaths.is_empty() {
self.other_subpaths.extend(core::mem::take(&mut self.glyph_subpaths));
if per_glyph_instances {
self.vector_table.push(Instance {
instance: VectorData::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
transform: DAffine2::from_translation(glyph_offset),
..Default::default()
});
} else {
for subpath in self.glyph_subpaths.drain(..) {
// Unwrapping here is ok because `self.vector_table` is initialized with a single `VectorData`
self.vector_table.get_mut(0).unwrap().instance.append_subpath(subpath, false);
}
}
}
}
@@ -97,6 +104,7 @@ pub struct TypesettingConfig {
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
}
impl Default for TypesettingConfig {
@@ -108,11 +116,12 @@ impl Default for TypesettingConfig {
max_width: None,
max_height: None,
tilt: 0.,
align: TextAlign::default(),
}
}
}
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64) {
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64, per_glyph_instances: bool) {
let mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline();
@@ -120,18 +129,26 @@ fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder
// User-requested tilt applied around baseline to avoid vertical displacement
// Translation ensures rotation point is at the baseline, not origin
let skew = DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64));
let skew = if per_glyph_instances {
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
} else {
DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
};
let synthesis = run.synthesis();
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
// This preserves the distinction between font styling and user transformations
let style_skew = synthesis.skew().map(|angle| {
DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
if per_glyph_instances {
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
} else {
DAffine2::from_translation(DVec2::new(0., run_y as f64))
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
}
});
let font = run.font();
@@ -145,14 +162,15 @@ fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder
let outlines = font_ref.outline_glyphs();
for glyph in glyph_run.glyphs() {
let glyph_x = run_x + glyph.x;
let glyph_y = run_y - glyph.y;
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id);
if let Some(glyph_outline) = outlines.get(glyph_id) {
path_builder.set_origin(glyph_x as f64, glyph_y as f64);
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, style_skew, skew);
if !per_glyph_instances {
path_builder.origin = glyph_offset;
}
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
}
}
}
@@ -172,7 +190,7 @@ fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingC
})?;
const DISPLAY_SCALE: f32 = 1.;
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, true);
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, false);
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
@@ -182,32 +200,42 @@ fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingC
let mut layout: Layout<()> = builder.build(str);
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default());
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
Some(layout)
}
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Vec<Subpath<PointId>> {
let Some(layout) = layout_text(str, font_data, typesetting) else { return Vec::new() };
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> VectorDataTable {
let Some(layout) = layout_text(str, font_data, typesetting) else {
return VectorDataTable::new(VectorData::default());
};
let mut path_builder = PathBuilder {
current_subpath: Subpath::new(Vec::new(), false),
glyph_subpaths: Vec::new(),
other_subpaths: Vec::new(),
origin: DVec2::ZERO,
vector_table: if per_glyph_instances {
VectorDataTable::default()
} else {
VectorDataTable::new(VectorData::default())
},
scale: layout.scale() as f64,
id: PointId::ZERO,
origin: DVec2::default(),
};
for line in layout.lines() {
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt);
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt, per_glyph_instances);
}
}
}
path_builder.other_subpaths
if path_builder.vector_table.is_empty() {
path_builder.vector_table = VectorDataTable::new(VectorData::default());
}
path_builder.vector_table
}
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {

View File

@@ -6,14 +6,20 @@ use glam::{DAffine2, DMat2, DVec2};
pub trait Transform {
fn transform(&self) -> DAffine2;
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
pivot
}
fn decompose_scale(&self) -> DVec2 {
DVec2::new(
self.transform().transform_vector2((1., 0.).into()).length(),
self.transform().transform_vector2((0., 1.).into()).length(),
)
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
}
/// Requires that the transform does not contain any skew.
fn decompose_rotation(&self) -> f64 {
let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2;
let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X);
if rotation == -0. { 0. } else { rotation }
}
}
@@ -141,12 +147,21 @@ impl std::hash::Hash for Footprint {
pub trait ApplyTransform {
fn apply_transform(&mut self, modification: &DAffine2);
fn left_apply_transform(&mut self, modification: &DAffine2);
}
impl<T: TransformMut> ApplyTransform for T {
fn apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = self.transform() * modification
}
fn left_apply_transform(&mut self, &modification: &DAffine2) {
*self.transform_mut() = modification * self.transform()
}
}
impl ApplyTransform for () {
fn apply_transform(&mut self, &_modification: &DAffine2) {}
impl ApplyTransform for DVec2 {
fn apply_transform(&mut self, modification: &DAffine2) {
*self = modification.transform_point2(*self);
}
fn left_apply_transform(&mut self, modification: &DAffine2) {
*self = modification.inverse().transform_point2(*self);
}
}

View File

@@ -7,20 +7,22 @@ use core::f64;
use glam::{DAffine2, DVec2};
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
async fn transform<T: ApplyTransform + 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> DAffine2,
Context -> DVec2,
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
value: impl Node<Context<'static>, Output = T>,
translate: DVec2,
rotate: f64,
scale: DVec2,
skew: DVec2,
) -> Instances<T> {
) -> T {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
@@ -31,11 +33,9 @@ async fn transform<T: 'n + 'static>(
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
let mut transform_target = value.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target.left_apply_transform(&matrix);
transform_target
}
@@ -52,6 +52,40 @@ fn replace_transform<Data, TransformInput: Transform>(
data
}
#[node_macro::node(category("Math: Transform"), path(graphene_core::vector))]
async fn extract_transform<T>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
RasterDataTable<GPU>,
)]
vector_data: Instances<T>,
) -> DAffine2 {
vector_data.instance_ref_iter().next().map(|vector_data| *vector_data.transform).unwrap_or_default()
}
#[node_macro::node(category("Math: Transform"))]
fn invert_transform(_: impl Ctx, transform: DAffine2) -> DAffine2 {
transform.inverse()
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_translation(_: impl Ctx, transform: DAffine2) -> DVec2 {
transform.translation
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_rotation(_: impl Ctx, transform: DAffine2) -> f64 {
transform.decompose_rotation()
}
#[node_macro::node(category("Math: Transform"))]
fn decompose_scale(_: impl Ctx, transform: DAffine2) -> DVec2 {
transform.decompose_scale()
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,

View File

@@ -120,7 +120,6 @@ impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
#[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval");

View File

@@ -1,17 +1,20 @@
use super::intersection::bezpath_intersections;
use super::poisson_disk::poisson_disk_sample;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
use super::util::segment_tangent;
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
use glam::{DMat2, DVec2};
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
use std::f64::consts::{FRAC_PI_2, PI};
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
/// Splits the [`BezPath`] at segment index 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)> {
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> 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.
@@ -52,14 +55,27 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
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);
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments.
pub fn split_bezpath(bezpath: &BezPath, t_value: TValue) -> Option<(BezPath, BezPath)> {
if bezpath.segments().count() == 0 {
return None;
}
// Get the segment which lies at the split.
let (segment_index, t) = eval_bezpath(bezpath, t_value, None);
split_bezpath_at_segment(bezpath, segment_index, t)
}
pub fn evaluate_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
}
pub fn tangent_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);
pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
match segment {
PathSeg::Line(line) => line.deriv().eval(t),
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
@@ -165,23 +181,35 @@ pub fn sample_polyline_on_bezpath(
Some(sample_bezpath)
}
pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> (usize, f64) {
if euclidian {
let (segment_index, t) = bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalEuclidean(t), segments_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
return (segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY));
#[derive(Debug, Clone, Copy)]
pub enum TValue {
Parametric(f64),
Euclidean(f64),
}
/// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`.
pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option<PathSeg> {
let t1 = eval_pathseg(segment, t1);
let t2 = eval_pathseg(segment, t2);
if t1 > t2 { None } else { Some(segment.subsegment(t1..t2)) }
}
pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 {
match t_value {
TValue::Parametric(t) => t,
TValue::Euclidean(t) => eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY),
}
bezpath_t_value_to_parametric(bezpath, BezPathTValue::GlobalParametric(t), segments_length)
}
/// 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: PathSeg, distance: f64, accuracy: f64) -> f64 {
pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
let mut low_t = 0.;
let mut mid_t = 0.5;
let mut high_t = 1.;
let total_length = path_segment.perimeter(accuracy);
let total_length = segment.perimeter(accuracy);
if !total_length.is_finite() || total_length <= f64::EPSILON {
return 0.;
@@ -190,7 +218,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
let distance = distance.clamp(0., 1.);
while high_t - low_t > accuracy {
let current_length = path_segment.subsegment(0.0..mid_t).perimeter(accuracy);
let current_length = segment.subsegment(0.0..mid_t).perimeter(accuracy);
let current_distance = current_length / total_length;
if current_distance > distance {
@@ -207,7 +235,7 @@ pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f6
/// 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: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
fn eval_bazpath_to_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;
@@ -219,19 +247,14 @@ fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths
(bezpath.segments().count() - 1, 1.)
}
enum BezPathTValue {
GlobalEuclidean(f64),
GlobalParametric(f64),
}
/// 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: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
/// Convert a [TValue] to a parametric `(segment_index, t)` tuple.
/// - Asserts that `t` values contained within the `TValue` argument lie in the range [0, 1].
fn eval_bezpath(bezpath: &BezPath, t: TValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
let segment_count = bezpath.segments().count();
assert!(segment_count >= 1);
match t {
BezPathTValue::GlobalEuclidean(t) => {
TValue::Euclidean(t) => {
let computed_segments_length;
let segments_length = if let Some(segments_length) = precomputed_segments_length {
@@ -243,16 +266,18 @@ fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precompute
let total_length = segments_length.iter().sum();
global_euclidean_to_local_euclidean(bezpath, t, segments_length, total_length)
let (segment_index, t) = eval_bazpath_to_euclidean(bezpath, t, segments_length, total_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
(segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY))
}
BezPathTValue::GlobalParametric(global_t) => {
assert!((0.0..=1.).contains(&global_t));
TValue::Parametric(t) => {
assert!((0.0..=1.).contains(&t));
if global_t == 1. {
if t == 1. {
return (segment_count - 1, 1.);
}
let scaled_t = global_t * segment_count as f64;
let scaled_t = t * segment_count as f64;
let segment_index = scaled_t.floor() as usize;
let t = scaled_t - segment_index as f64;
@@ -314,3 +339,130 @@ pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], s
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
}
/// Returns true if the Bezier curve is equivalent to a line.
///
/// **NOTE**: This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
pub fn is_linear(segment: &PathSeg) -> bool {
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
match *segment {
PathSeg::Line(_) => true,
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
}
}
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
// Split the first subpath at its last intersection
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
if subpath_1_intersections.is_empty() {
return None;
}
let (segment_index, t) = *subpath_1_intersections.last()?;
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;
// Split the second subpath at its first intersection
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
if subpath_2_intersections.is_empty() {
return None;
}
let (segment_index, t) = subpath_2_intersections[0];
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;
Some((clipped_subpath1, clipped_subpath2))
}
/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
///
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
/// When the limit is exceeded, no [`PathEl`] will be returned.
/// This value should be greater than 0. If not, the default of 4 will be used.
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
let miter_limit = match miter_limit {
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
_ => 4.,
};
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
let in_segment = bezpath1.segments().last()?;
let out_segment = bezpath2.segments().next()?;
let in_tangent = segment_tangent(in_segment, 1.);
let out_tangent = segment_tangent(out_segment, 0.);
if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
// Avoid panic from normalizing zero vectors
// TODO: Besides returning None, is there a more appropriate way to handle this?
return None;
}
let angle = (in_tangent * -1.).angle_to(out_tangent).abs();
if angle.to_degrees() < miter_limit {
return None;
}
let p1 = in_segment.end();
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
let line1 = Line::new(p1, dvec2_to_point(p2));
let p1 = out_segment.start();
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
let line2 = Line::new(p1, dvec2_to_point(p2));
// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
// Otherwise, we return the element to create the join.
let intersection = line1.crossing_point(line2)?;
Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
}
/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
/// By default, the angle is assumed to be 180 degrees.
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
let center_to_arc_point = arc_point - center;
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
let p3 = dvec2_to_point(arc_point);
let first_half = PathEl::CurveTo(p1, p2, p3);
let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
let p3 = dvec2_to_point(right);
let second_half = PathEl::CurveTo(p1, p2, p3);
[first_half, second_half]
}
/// Returns two [`PathEl`] to create a round join with the provided center.
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());
let center_to_right = right - center;
let center_to_left = left - center;
let in_segment = bezpath1.segments().last();
let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.));
let mut angle = center_to_right.angle_to(center_to_left) / 2.;
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
}
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
}

View File

@@ -0,0 +1,6 @@
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
/// Constant used to determine if `f64`s are equivalent.
#[cfg(test)]
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;

View File

@@ -88,12 +88,10 @@ async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
// TODO: Make this return a u32 instead of an f64, but we ned to improve math-related compatibility with integer types first.
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
match ctx.try_index() {
Some(index) => return index as f64,
None => warn!("Extracted value of incorrect type"),
}
0.
async fn instance_index(ctx: impl Ctx + ExtractIndex, _primary: (), loop_level: u32) -> f64 {
ctx.try_index()
.and_then(|indexes| indexes.get(indexes.len().wrapping_sub(1).wrapping_sub(loop_level as usize)).copied())
.unwrap_or_default() as f64
}
#[cfg(test)]

View File

@@ -0,0 +1,365 @@
use super::contants::MIN_SEPARATION_VALUE;
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples,
/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to
/// that segment where the intersection occurred.
///
/// `minimum_separation` is the minimum difference that two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
pub fn bezpath_and_segment_intersections(bezpath: &BezPath, segment: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
bezpath
.segments()
.enumerate()
.flat_map(|(index, this_segment)| {
filtered_segment_intersections(this_segment, segment, accuracy, minimum_separation)
.into_iter()
.map(|t| (index, t))
.collect::<Vec<(usize, f64)>>()
})
.collect()
}
/// Calculates the intersection points the bezpath has with another given bezpath and returns a list of parametric `t`-values.
pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
let mut intersection_t_values: Vec<(usize, f64)> = bezpath2
.segments()
.flat_map(|bezier| bezpath_and_segment_intersections(bezpath1, bezier, accuracy, minimum_separation))
.collect();
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersection_t_values
}
/// Calculates the intersection points the segment has with another given segment and returns a list of parametric `t`-values with given accuracy.
pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>) -> Vec<(f64, f64)> {
let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY);
match (segment1, segment2) {
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
(segment1, segment2) => {
let mut intersections = Vec::new();
segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections);
intersections
}
}
}
/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments
/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy.
#[allow(clippy::too_many_arguments)]
fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) {
let bbox1 = segment1.bounding_box();
let bbox2 = segment2.bounding_box();
let mid_t1 = (min_t1 + max_t1) / 2.;
let mid_t2 = (min_t2 + max_t2) / 2.;
// Check if the bounding boxes overlap
if bbox1.overlaps(bbox2) {
// If bounding boxes overlap and they are small enough, we have found an intersection
if bbox1.width() < accuracy && bbox1.height() < accuracy && bbox2.width() < accuracy && bbox2.height() < accuracy {
// Use the middle `t` value, append the corresponding `t` value
intersections.push((mid_t1, mid_t2));
return;
}
// Split curves in half
let (seg11, seg12) = segment1.subdivide();
let (seg21, seg22) = segment2.subdivide();
// Repeat checking the intersection with the combinations of the two halves of each curve
segment_intersections_inner(seg11, min_t1, mid_t1, seg21, min_t2, mid_t2, accuracy, intersections);
segment_intersections_inner(seg11, min_t1, mid_t1, seg22, mid_t2, max_t2, accuracy, intersections);
segment_intersections_inner(seg12, mid_t1, max_t1, seg21, min_t2, mid_t2, accuracy, intersections);
segment_intersections_inner(seg12, mid_t1, max_t1, seg22, mid_t2, max_t2, accuracy, intersections);
}
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier segment and the provided one
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference
/// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
/// The returned `t` values are with respect to the current bezier segment, not the provided parameter.
/// If the provided segment is linear, then zero intersection points will be returned along colinear segments.
///
/// `accuracy` defines, for intersections where the provided bezier segment is non-linear, the maximum size of the bounding boxes to be considered an intersection point.
///
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order.
pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
intersection_t_values.iter().map(|x| x.0).fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) {
accumulator.pop();
}
accumulator.push(t);
accumulator
})
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Returns a list of pairs of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided
/// one such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference between
/// two adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
/// The first value in pair is with respect to the current bezier and the second value in pair is with respect to the provided parameter.
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
///
/// `error`, for intersections where the provided bezier is non-linear, defines the threshold for bounding boxes to be considered an intersection point.
///
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order
pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(f64, f64)> {
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap());
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
if !accumulator.is_empty()
&& (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
&& (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
{
accumulator.pop();
}
accumulator.push(*t);
accumulator
})
}
#[cfg(test)]
mod tests {
use super::{bezpath_and_segment_intersections, filtered_segment_intersections};
use crate::vector::algorithms::{
contants::MAX_ABSOLUTE_DIFFERENCE,
util::{compare_points, compare_vec_of_points, dvec2_compare},
};
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez};
#[test]
fn test_intersect_line_segment_quadratic() {
let p1 = Point::new(30., 50.);
let p2 = Point::new(140., 30.);
let p3 = Point::new(160., 170.);
// Intersection at edge of curve
let bezier = PathSeg::Quad(QuadBez::new(p1, p2, p3));
let line1 = PathSeg::Line(Line::new(Point::new(20., 50.), Point::new(40., 50.)));
let intersections1 = filtered_segment_intersections(bezier, line1, None, None);
assert!(intersections1.len() == 1);
assert!(compare_points(bezier.eval(intersections1[0]), p1));
// Intersection in the middle of curve
let line2 = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(30., 30.)));
let intersections2 = filtered_segment_intersections(bezier, line2, None, None);
assert!(compare_points(bezier.eval(intersections2[0]), Point::new(47.77355, 47.77354)));
}
#[test]
fn test_intersect_curve_cubic_edge_case() {
// M34 107 C40 40 120 120 102 29
let p1 = Point::new(34., 107.);
let p2 = Point::new(40., 40.);
let p3 = Point::new(120., 120.);
let p4 = Point::new(102., 29.);
let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4));
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
let intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
assert_eq!(intersections.len(), 1);
}
#[test]
fn test_intersect_curve() {
let p0 = Point::new(30., 30.);
let p1 = Point::new(60., 140.);
let p2 = Point::new(150., 30.);
let p3 = Point::new(160., 160.);
let cubic_segment = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3));
let p0 = Point::new(175., 140.);
let p1 = Point::new(20., 20.);
let p2 = Point::new(120., 20.);
let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2));
let intersections1 = filtered_segment_intersections(cubic_segment, quadratic_segment, None, None);
let intersections2 = filtered_segment_intersections(quadratic_segment, cubic_segment, None, None);
let intersections1_points: Vec<Point> = intersections1.iter().map(|&t| cubic_segment.eval(t)).collect();
let intersections2_points: Vec<Point> = intersections2.iter().map(|&t| quadratic_segment.eval(t)).rev().collect();
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_one() {
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
let cubic_start = Point::new(35., 125.);
let cubic_handle_1 = Point::new(40., 40.);
let cubic_handle_2 = Point::new(120., 120.);
let cubic_end = Point::new(43., 43.);
let quadratic_1_handle = Point::new(175., 90.);
let quadratic_end = Point::new(145., 150.);
let quadratic_2_handle = Point::new(70., 185.);
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
let bezpath = BezPath::from_vec(vec![
PathEl::MoveTo(cubic_start),
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
PathEl::QuadTo(quadratic_2_handle, cubic_start),
PathEl::ClosePath,
]);
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
let cubic_intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, linear_segment, None, None);
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, linear_segment, None, None);
assert!(
dvec2_compare(
cubic_segment.eval(cubic_intersections[0]),
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
assert!(
dvec2_compare(
quadratic_segment.eval(quadratic_1_intersections[0]),
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
assert!(
dvec2_compare(
quadratic_segment.eval(quadratic_1_intersections[1]),
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_two() {
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
// M150 150 L 20 20
let cubic_start = Point::new(34., 107.);
let cubic_handle_1 = Point::new(40., 40.);
let cubic_handle_2 = Point::new(120., 120.);
let cubic_end = Point::new(102., 29.);
let quadratic_1_handle = Point::new(175., 90.);
let quadratic_end = Point::new(129., 171.);
let quadratic_2_handle = Point::new(70., 185.);
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
let bezpath = BezPath::from_vec(vec![
PathEl::MoveTo(cubic_start),
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
PathEl::QuadTo(quadratic_2_handle, cubic_start),
PathEl::ClosePath,
]);
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
assert!(
dvec2_compare(
cubic_segment.eval(cubic_intersections[0]),
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
assert!(
dvec2_compare(
quadratic_segment.eval(quadratic_1_intersections[0]),
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
}
#[test]
fn intersection_linear_multiple_subpath_curves_test_three() {
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
let cubic_start = Point::new(35., 125.);
let cubic_handle_1 = Point::new(40., 40.);
let cubic_handle_2 = Point::new(120., 120.);
let cubic_end = Point::new(44., 44.);
let quadratic_1_handle = Point::new(175., 90.);
let quadratic_end = Point::new(145., 150.);
let quadratic_2_handle = Point::new(70., 185.);
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
let bezpath = BezPath::from_vec(vec![
PathEl::MoveTo(cubic_start),
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
PathEl::QuadTo(quadratic_2_handle, cubic_start),
PathEl::ClosePath,
]);
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
assert!(
dvec2_compare(
cubic_segment.eval(cubic_intersections[0]),
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
assert!(
dvec2_compare(
quadratic_segment.eval(quadratic_1_intersections[0]),
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
assert!(
dvec2_compare(
quadratic_segment.eval(quadratic_1_intersections[1]),
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
MAX_ABSOLUTE_DIFFERENCE
)
.all()
);
}
}

View File

@@ -1,6 +1,9 @@
pub mod bezpath_algorithms;
mod contants;
pub mod instance;
pub mod intersection;
pub mod merge_by_distance;
pub mod offset_subpath;
pub mod poisson_disk;
pub mod spline;
pub mod util;

View File

@@ -1,173 +1,137 @@
use crate::vector::PointId;
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join};
use crate::vector::misc::point_to_dvec2;
use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg};
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
/// Accuracy of fitting offset curve to Bezier paths.
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;
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7;
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),
kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates(
cubic_bez.p0.x,
cubic_bez.p0.y,
cubic_bez.p1.x,
cubic_bez.p1.y,
cubic_bez.p2.x,
cubic_bez.p2.y,
cubic_bez.p3.x,
cubic_bez.p3.y,
),
}
}
// TODO: Replace the implementation to use only Kurbo API.
/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away.
/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away.
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: Option<f64>) -> BezPath {
// An offset at a distance 0 from the curve is simply the same curve.
// An offset of a single point is not defined.
if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 {
return subpath.clone();
if distance == 0. || bezpath.get_seg(1).is_none() {
return bezpath.clone();
}
let mut subpaths = subpath
.iter()
.filter(|bezier| !bezier.is_point())
let mut bezpaths = bezpath
.segments()
.map(|bezier| bezier.to_cubic())
.map(|cubic| {
let Bezier { start, end, handles } = cubic;
let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()};
let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y));
.map(|cubic_bez| {
let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY);
let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY);
let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| {
acc.push(segment_to_bezier(seg));
acc
});
Subpath::from_beziers(&beziers, false)
kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY)
})
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
.collect::<Vec<Subpath<PointId>>>();
let mut drop_common_point = vec![true; subpath.len()];
.filter(|bezpath| bezpath.get_seg(1).is_some()) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
.collect::<Vec<BezPath>>();
// Clip or join consecutive Subpaths
for i in 0..subpaths.len() - 1 {
for i in 0..bezpaths.len() - 1 {
let j = i + 1;
let subpath1 = &subpaths[i];
let subpath2 = &subpaths[j];
let bezpath1 = &bezpaths[i];
let bezpath2 = &bezpaths[j];
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
let first_segment = subpath2.get_segment(0).unwrap();
let last_segment_end = point_to_dvec2(bezpath1.segments().last().unwrap().end());
let first_segment_start = point_to_dvec2(bezpath2.segments().next().unwrap().start());
// If the anchors are approximately equal, there is no need to clip / join the segments
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
if last_segment_end.abs_diff_eq(first_segment_start, MAX_ABSOLUTE_DIFFERENCE) {
continue;
}
// Calculate the angle formed between two consecutive Subpaths
let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
let angle = out_tangent.angle_to(in_tangent);
// The angle is concave. The Subpath overlap and must be clipped
let mut apply_join = true;
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
// the points as being on top of one another.
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
subpaths[i] = clipped_subpath1;
subpaths[j] = clipped_subpath2;
apply_join = false;
}
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
bezpaths[i] = clipped_subpath1;
bezpaths[j] = clipped_subpath2;
apply_join = false;
}
// The angle is convex. The Subpath must be joined using the specified join type
if apply_join {
drop_common_point[j] = false;
match join {
Join::Bevel => {}
Join::Miter(miter_limit) => {
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
if let Some(miter_manipulator_group) = miter_manipulator_group {
subpaths[i].manipulator_groups_mut().push(miter_manipulator_group);
Join::Bevel => {
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
bezpaths[i].push(element);
}
Join::Miter => {
let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit);
if let Some(element) = element {
bezpaths[i].push(element[0]);
bezpaths[i].push(element[1]);
} else {
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
bezpaths[i].push(element);
}
}
Join::Round => {
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor);
let last_index = subpaths[i].manipulator_groups().len() - 1;
subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
subpaths[i].manipulator_groups_mut().push(round_point);
subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle);
let center = point_to_dvec2(bezpath.get_seg(i + 1).unwrap().end());
let elements = round_line_join(&bezpaths[i], &bezpaths[j], center);
bezpaths[i].push(elements[0]);
bezpaths[i].push(elements[1]);
}
}
}
}
// Clip any overlap in the last segment
if subpath.closed {
let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
let angle = out_tangent.angle_to(in_tangent);
let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath);
if is_bezpath_closed {
let mut apply_join = true;
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
// Merge the clipped subpaths
let last_index = subpaths.len() - 1;
subpaths[last_index] = clipped_subpath1;
subpaths[0] = clipped_subpath2;
apply_join = false;
}
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) {
// Merge the clipped subpaths
let last_index = bezpaths.len() - 1;
bezpaths[last_index] = clipped_subpath1;
bezpaths[0] = clipped_subpath2;
apply_join = false;
}
if apply_join {
drop_common_point[0] = false;
match join {
Join::Bevel => {}
Join::Miter(miter_limit) => {
let last_subpath_index = subpaths.len() - 1;
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
if let Some(miter_manipulator_group) = miter_manipulator_group {
subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group);
Join::Bevel => {
let last_subpath_index = bezpaths.len() - 1;
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
bezpaths[last_subpath_index].push(element);
}
Join::Miter => {
let last_subpath_index = bezpaths.len() - 1;
let element = miter_line_join(&bezpaths[last_subpath_index], &bezpaths[0], miter_limit);
if let Some(element) = element {
bezpaths[last_subpath_index].push(element[0]);
bezpaths[last_subpath_index].push(element[1]);
} else {
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
bezpaths[last_subpath_index].push(element);
}
}
Join::Round => {
let last_subpath_index = subpaths.len() - 1;
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor);
let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1;
subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
subpaths[last_subpath_index].manipulator_groups_mut().push(round_point);
subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle);
let last_subpath_index = bezpaths.len() - 1;
let center = point_to_dvec2(bezpath.get_seg(1).unwrap().start());
let elements = round_line_join(&bezpaths[last_subpath_index], &bezpaths[0], center);
bezpaths[last_subpath_index].push(elements[0]);
bezpaths[last_subpath_index].push(elements[1]);
}
}
}
}
// Merge the subpaths. Drop points which overlap with one another.
let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec();
for i in 1..subpaths.len() {
if drop_common_point[i] {
let last_group = manipulator_groups.pop().unwrap();
let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec();
manipulators_copy[0].in_handle = last_group.in_handle;
manipulator_groups.append(&mut manipulators_copy);
} else {
manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec());
// Merge the bezpaths and its segments. Drop points which overlap with one another.
let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::<Vec<PathSeg>>()).collect::<Vec<PathSeg>>();
let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| {
if acc.elements().is_empty() {
acc.move_to(segment.start());
}
}
if subpath.closed && drop_common_point[0] {
let last_group = manipulator_groups.pop().unwrap();
manipulator_groups[0].in_handle = last_group.in_handle;
acc.push(segment.as_path_el());
acc
});
if is_bezpath_closed {
offset_bezpath.close_path();
}
Subpath::new(manipulator_groups, subpath.closed)
offset_bezpath
}

View File

@@ -182,7 +182,7 @@ where
A::Item: Clone,
B::Item: Clone,
{
a.flat_map(move |i| (b.clone().map(move |j| (i.clone(), j))))
a.flat_map(move |i| b.clone().map(move |j| (i.clone(), j)))
}
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.

View File

@@ -0,0 +1,44 @@
use glam::DVec2;
use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg};
pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 {
// NOTE: .deriv() method gives inaccurate result when it is 1.
let t = if t == 1. { 1. - f64::EPSILON } else { t };
let tangent = match segment {
PathSeg::Line(line) => line.deriv().eval(t),
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
};
DVec2::new(tangent.x, tangent.y)
}
// Compare two f64s with some maximum absolute difference to account for floating point errors
#[cfg(test)]
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
(f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE
}
/// Compare points by allowing some maximum absolute difference to account for floating point errors
#[cfg(test)]
pub fn compare_points(p1: kurbo::Point, p2: kurbo::Point) -> bool {
let (p1, p2) = (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2));
p1.abs_diff_eq(p2, super::contants::MAX_ABSOLUTE_DIFFERENCE)
}
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
#[cfg(test)]
pub fn compare_vec_of_points(a: Vec<kurbo::Point>, b: Vec<kurbo::Point>, max_absolute_difference: f64) -> bool {
a.len() == b.len()
&& a.into_iter()
.zip(b)
.map(|(p1, p2)| (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2)))
.all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
}
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
#[cfg(test)]
pub fn dvec2_compare(a: kurbo::Point, b: kurbo::Point, max_abs_diff: f64) -> glam::BVec2 {
glam::BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff)
}

View File

@@ -1,6 +1,10 @@
use super::PointId;
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
use dyn_any::DynAny;
use glam::DVec2;
use kurbo::Point;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
use std::ops::Sub;
/// Represents different ways of calculating the centroid.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
@@ -64,7 +68,7 @@ pub enum GridType {
#[widget(Radio)]
pub enum ArcType {
#[default]
Open,
Open = 0,
Closed,
PieSlice,
}
@@ -96,3 +100,140 @@ pub fn point_to_dvec2(point: Point) -> DVec2 {
pub fn dvec2_to_point(value: DVec2) -> Point {
Point { x: value.x, y: value.y }
}
pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles {
match *segment {
PathSeg::Line(_) => BezierHandles::Linear,
PathSeg::Quad(QuadBez { p0: _, p1, p2: _ }) => BezierHandles::Quadratic { handle: point_to_dvec2(p1) },
PathSeg::Cubic(CubicBez { p0: _, p1, p2, p3: _ }) => BezierHandles::Cubic {
handle_start: point_to_dvec2(p1),
handle_end: point_to_dvec2(p2),
},
}
}
pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg {
match handles {
bezier_rs::BezierHandles::Linear => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(end);
PathSeg::Line(Line::new(p0, p1))
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(handle);
let p2 = dvec2_to_point(end);
PathSeg::Quad(QuadBez::new(p0, p1, p2))
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(handle_start);
let p2 = dvec2_to_point(handle_end);
let p3 = dvec2_to_point(end);
PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3))
}
}
}
pub fn subpath_to_kurbo_bezpath(subpath: Subpath<PointId>) -> BezPath {
let maniputor_groups = subpath.manipulator_groups();
let closed = subpath.closed();
bezpath_from_manipulator_groups(maniputor_groups, closed)
}
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
let Some(first) = manipulator_groups.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
for manipulator in manipulator_groups.iter().skip(1) {
match (out_handle, manipulator.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
}
out_handle = manipulator.out_handle;
}
if closed {
match (out_handle, first.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
}
bezpath.close_path();
}
bezpath
}
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup<PointId>>, bool) {
let mut manipulator_groups = Vec::<ManipulatorGroup<PointId>>::new();
let mut is_closed = false;
for element in bezpath.elements() {
let manipulator_group = match *element {
kurbo::PathEl::MoveTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
kurbo::PathEl::LineTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
kurbo::PathEl::QuadTo(point, point1) => ManipulatorGroup::new(point_to_dvec2(point1), Some(point_to_dvec2(point)), None),
kurbo::PathEl::CurveTo(point, point1, point2) => {
if let Some(last_maipulator_group) = manipulator_groups.last_mut() {
last_maipulator_group.out_handle = Some(point_to_dvec2(point));
}
ManipulatorGroup::new(point_to_dvec2(point2), Some(point_to_dvec2(point1)), None)
}
kurbo::PathEl::ClosePath => {
if let Some(last_group) = manipulator_groups.pop() {
if let Some(first_group) = manipulator_groups.first_mut() {
first_group.out_handle = last_group.in_handle;
}
}
is_closed = true;
break;
}
};
manipulator_groups.push(manipulator_group);
}
(manipulator_groups, is_closed)
}
/// Returns true if the [`PathSeg`] is equivalent to a line.
///
/// This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
pub fn is_linear(segment: PathSeg) -> bool {
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
match segment {
PathSeg::Line(_) => true,
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
}
}
/// Get an iterator over the coordinates of all points in a path segment.
pub fn get_segment_points(segment: PathSeg) -> Vec<Point> {
match segment {
PathSeg::Line(line) => [line.p0, line.p1].to_vec(),
PathSeg::Quad(quad_bez) => [quad_bez.p0, quad_bez.p1, quad_bez.p2].to_vec(),
PathSeg::Cubic(cubic_bez) => [cubic_bez.p0, cubic_bez.p1, cubic_bez.p2, cubic_bez.p3].to_vec(),
}
}
/// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other.
pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool {
let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 };
let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 };
let seg1_points = get_segment_points(seg1);
let seg2_points = get_segment_points(seg2);
let cmp = |a: f64, b: f64| a.sub(b).abs() < max_abs_diff;
seg1_points.len() == seg2_points.len() && seg1_points.into_iter().zip(seg2_points).all(|(a, b)| cmp(a.x, b.x) && cmp(a.y, b.y))
}

View File

@@ -17,7 +17,7 @@ use core::hash::Hash;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
pub use indexed::VectorDataIndex;
use kurbo::{Affine, Rect, Shape};
use kurbo::{Affine, BezPath, Rect, Shape};
pub use modification::*;
use std::collections::HashMap;
@@ -195,6 +195,13 @@ impl VectorData {
Self::from_subpaths([subpath], false)
}
/// Construct some new vector data from a single [`BezPath`] with an identity transform and black fill.
pub fn from_bezpath(bezpath: BezPath) -> Self {
let mut vector_data = Self::default();
vector_data.append_bezpath(bezpath);
vector_data
}
/// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
let mut vector_data = Self::default();
@@ -226,10 +233,10 @@ impl VectorData {
pub fn close_subpaths(&mut self) {
let segments_to_add: Vec<_> = self
.stroke_bezier_paths()
.filter(|subpath| !subpath.closed)
.filter_map(|subpath| {
let (first, last) = subpath.manipulator_groups().first().zip(subpath.manipulator_groups().last())?;
.build_stroke_path_iter()
.filter(|(_, closed)| !closed)
.filter_map(|(manipulator_groups, _)| {
let (first, last) = manipulator_groups.first().zip(manipulator_groups.last())?;
let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?;
Some((start, end))
})
@@ -337,7 +344,7 @@ impl VectorData {
/// 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)))
.filter(|(_, bez, start, end)| (*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear))
.count()
}
@@ -370,7 +377,7 @@ impl VectorData {
}
pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool {
let bez_paths: Vec<_> = self
let number = self
.stroke_bezpath_iter()
.map(|mut bezpath| {
// TODO: apply transform to points instead of modifying the paths
@@ -379,19 +386,9 @@ impl VectorData {
let bbox = bezpath.bounding_box();
(bezpath, bbox)
})
.collect();
// Check against all paths the point is contained in to compute the correct winding number
let mut number = 0;
for (shape, bbox) in bez_paths {
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
continue;
}
let winding = shape.winding(dvec2_to_point(point));
number += winding;
}
.filter(|(_, bbox)| bbox.contains(dvec2_to_point(point)))
.map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point)))
.sum::<i32>();
// Non-zero fill rule
number != 0
@@ -571,6 +568,30 @@ impl ManipulatorPointId {
}
}
/// Finds all the connected handles of a point.
/// For an anchor it is all the connected handles.
/// For a handle it is all the handles connected to its corresponding anchor other than the current handle.
pub fn get_all_connected_handles(self, vector_data: &VectorData) -> Option<Vec<HandleId>> {
match self {
ManipulatorPointId::Anchor(point) => {
let connected = vector_data.all_connected(point).collect::<Vec<_>>();
Some(connected)
}
ManipulatorPointId::PrimaryHandle(segment) => {
let point = vector_data.segment_domain.segment_start_from_id(segment)?;
let current = HandleId::primary(segment);
let connected = vector_data.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
Some(connected)
}
ManipulatorPointId::EndHandle(segment) => {
let point = vector_data.segment_domain.segment_end_from_id(segment)?;
let current = HandleId::end(segment);
let connected = vector_data.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
Some(connected)
}
}
}
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
#[must_use]
pub fn get_anchor(self, vector_data: &VectorData) -> Option<PointId> {

View File

@@ -3,6 +3,7 @@ use crate::vector::vector_data::{HandleId, VectorData};
use bezier_rs::{BezierHandles, ManipulatorGroup};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::iter::zip;
@@ -440,6 +441,35 @@ impl SegmentDomain {
let handles = self.handles.iter_mut();
zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles))
}
pub(crate) fn pair_handles_and_points_mut_by_index(
&mut self,
index1: usize,
index2: usize,
) -> (&mut bezier_rs::BezierHandles, &mut usize, &mut usize, &mut bezier_rs::BezierHandles, &mut usize, &mut usize) {
// Use split_at_mut to avoid multiple mutable borrows of the same slice
let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1));
let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1));
let (end_first, end_second) = self.end_point.split_at_mut(index2.max(index1));
let (h1, h2) = if index1 < index2 {
(&mut handles_first[index1], &mut handles_second[0])
} else {
(&mut handles_second[0], &mut handles_first[index2])
};
let (sp1, sp2) = if index1 < index2 {
(&mut start_first[index1], &mut start_second[0])
} else {
(&mut start_second[0], &mut start_first[index2])
};
let (ep1, ep2) = if index1 < index2 {
(&mut end_first[index1], &mut end_second[0])
} else {
(&mut end_second[0], &mut end_first[index2])
};
(h1, sp1, ep1, h2, sp2, ep2)
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
@@ -644,6 +674,18 @@ impl FoundSubpath {
}
impl VectorData {
/// Construct a [`kurbo::PathSeg`] by resolving the points from their ids.
fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg {
let start = dvec2_to_point(self.point_domain.positions()[start]);
let end = dvec2_to_point(self.point_domain.positions()[end]);
match handles {
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new(start, dvec2_to_point(handle), end)),
BezierHandles::Cubic { handle_start, handle_end } => PathSeg::Cubic(CubicBez::new(start, dvec2_to_point(handle_start), dvec2_to_point(handle_end), end)),
}
}
/// 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: BezierHandles) -> bezier_rs::Bezier {
let start = self.point_domain.positions()[start];
@@ -670,6 +712,19 @@ impl VectorData {
(start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index]))
}
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_iter(&self) -> impl Iterator<Item = (SegmentId, PathSeg, PointId, PointId)> {
let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
self.segment_domain
.handles
.iter()
.zip(&self.segment_domain.id)
.zip(self.segment_domain.start_point())
.zip(self.segment_domain.end_point())
.map(to_segment)
}
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, bezier_rs::Bezier, PointId, PointId)> + '_ {
let to_bezier = |(((&handles, &id), &start), &end)| (id, self.segment_to_bezier_with_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
@@ -790,48 +845,8 @@ impl VectorData {
Some(bezier_rs::Subpath::new(groups, closed))
}
/// 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 = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
if last.is_some_and(|(previous_end, _)| previous_end != start) {
warn!("subpath_from_segments that were not continuous");
return None;
}
first_point = Some(first_point.unwrap_or(start));
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
id: self.point_domain.ids()[start],
});
last = Some((end, handle));
}
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
if let Some((end, last_handle)) = last {
if closed {
groups[0].in_handle = last_handle.end();
} else {
groups.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
id: self.point_domain.ids()[end],
});
}
}
Some(bezier_rs::Subpath::new(groups, closed))
}
/// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
pub fn region_bezier_paths(&self) -> impl Iterator<Item = (RegionId, bezier_rs::Subpath<PointId>)> + '_ {
pub fn region_manipulator_groups(&self) -> impl Iterator<Item = (RegionId, Vec<ManipulatorGroup<PointId>>)> + '_ {
self.region_domain
.id
.iter()
@@ -847,7 +862,29 @@ impl VectorData {
.zip(self.segment_domain.end_point.get(range)?)
.map(|((&handles, &start), &end)| (handles, start, end));
self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
let mut manipulator_groups = Vec::new();
let mut in_handle = None;
for segment in segments_iter {
let (handles, start_point_index, _end_point_index) = segment;
let start_point_id = self.point_domain.id[start_point_index];
let start_point = self.point_domain.position[start_point_index];
let (manipulator_group, next_in_handle) = match handles {
BezierHandles::Linear => (ManipulatorGroup::new_with_id(start_point, in_handle, None, start_point_id), None),
BezierHandles::Quadratic { handle } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle), start_point_id), None),
BezierHandles::Cubic { handle_start, handle_end } => (ManipulatorGroup::new_with_id(start_point, in_handle, Some(handle_start), start_point_id), Some(handle_end)),
};
in_handle = next_in_handle;
manipulator_groups.push(manipulator_group);
}
if let Some(first) = manipulator_groups.first_mut() {
first.in_handle = in_handle;
}
Some((id, manipulator_groups))
})
}

View File

@@ -418,7 +418,7 @@ impl Hash for VectorModification {
}
}
/// A node that applies a procedural modification to some [`VectorData`].
/// Applies a diff modification to a vector path.
#[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>, node_path: Vec<NodeId>) -> VectorDataTable {
if vector_data.is_empty() {
@@ -437,6 +437,23 @@ async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modificat
vector_data
}
/// Applies the vector path's local transformation to its geometry and resets it to the identity.
#[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
for vector_data_instance in vector_data.instance_mut_iter() {
let vector_data = vector_data_instance.instance;
let transform = *vector_data_instance.transform;
for (_, point) in vector_data.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
*vector_data_instance.transform = DAffine2::IDENTITY;
}
vector_data
}
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
// TODO: Eventually remove this document upgrade code
use serde::de::{SeqAccess, Visitor};

File diff suppressed because it is too large Load Diff