mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Merge branch 'master' into fix-range
This commit is contained in:
@@ -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"))',
|
||||
] }
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod color;
|
||||
mod color_traits;
|
||||
mod discrete_srgb;
|
||||
|
||||
pub use color::*;
|
||||
pub use color_traits::*;
|
||||
pub use discrete_srgb::*;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
6
node-graph/gcore/src/vector/algorithms/contants.rs
Normal file
6
node-graph/gcore/src/vector/algorithms/contants.rs
Normal 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;
|
||||
@@ -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)]
|
||||
|
||||
365
node-graph/gcore/src/vector/algorithms/intersection.rs
Normal file
365
node-graph/gcore/src/vector/algorithms/intersection.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
44
node-graph/gcore/src/vector/algorithms/util.rs
Normal file
44
node-graph/gcore/src/vector/algorithms/util.rs
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user