mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 19:08:05 +08:00
merge
This commit is contained in:
@@ -2,8 +2,7 @@ use crate::{Ctx, ExtractAnimationTime, ExtractTime};
|
||||
|
||||
const DAY: f64 = 1000. * 3600. * 24.;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RealTimeMode {
|
||||
#[label("UTC")]
|
||||
Utc,
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
use crate::text::FontCache;
|
||||
use crate::transform::Footprint;
|
||||
use crate::vector::style::ViewMode;
|
||||
use alloc::sync::Arc;
|
||||
use core::fmt::Debug;
|
||||
use core::future::Future;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use core::pin::Pin;
|
||||
use core::ptr::addr_of;
|
||||
use core::time::Duration;
|
||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||
use glam::{DAffine2, UVec2};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SurfaceId(pub u64);
|
||||
|
||||
impl core::fmt::Display for SurfaceId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SurfaceFrame {
|
||||
pub surface_id: SurfaceId,
|
||||
pub resolution: UVec2,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Hash for SurfaceFrame {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.surface_id.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for SurfaceFrame {
|
||||
type Static = SurfaceFrame;
|
||||
}
|
||||
|
||||
pub trait Size {
|
||||
fn size(&self) -> UVec2;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Size for web_sys::HtmlCanvasElement {
|
||||
fn size(&self) -> UVec2 {
|
||||
UVec2::new(self.width(), self.height())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub texture: Arc<wgpu::Texture>,
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
pub texture: (),
|
||||
}
|
||||
|
||||
impl Hash for ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.texture.hash(state);
|
||||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
impl PartialEq for ImageTexture {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
#[cfg(feature = "wgpu")]
|
||||
{
|
||||
self.texture == other.texture
|
||||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
{
|
||||
self.texture == other.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for ImageTexture {
|
||||
type Static = ImageTexture;
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl Size for ImageTexture {
|
||||
fn size(&self) -> UVec2 {
|
||||
UVec2::new(self.texture.width(), self.texture.height())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
|
||||
fn from(x: SurfaceHandleFrame<S>) -> Self {
|
||||
Self {
|
||||
surface_id: x.surface_handle.window_id,
|
||||
transform: x.transform,
|
||||
resolution: x.surface_handle.surface.size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SurfaceHandle<Surface> {
|
||||
pub window_id: SurfaceId,
|
||||
pub surface: Surface,
|
||||
}
|
||||
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// unsafe impl<T: dyn_any::WasmNotSync> Sync for SurfaceHandle<T> {}
|
||||
|
||||
impl<S: Size> Size for SurfaceHandle<S> {
|
||||
fn size(&self) -> UVec2 {
|
||||
self.surface.size()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: 'static> StaticType for SurfaceHandle<T> {
|
||||
type Static = SurfaceHandle<T>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SurfaceHandleFrame<Surface> {
|
||||
pub surface_handle: Arc<SurfaceHandle<Surface>>,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
unsafe impl<T: 'static> StaticType for SurfaceHandleFrame<T> {
|
||||
type Static = SurfaceHandleFrame<T>;
|
||||
}
|
||||
|
||||
// TODO: think about how to automatically clean up memory
|
||||
/*
|
||||
impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> {
|
||||
fn drop(&mut self) {
|
||||
self.application_io.destroy_surface(self.surface_id)
|
||||
}
|
||||
}*/
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
|
||||
|
||||
pub trait ApplicationIo {
|
||||
type Surface;
|
||||
type Executor;
|
||||
fn window(&self) -> Option<SurfaceHandle<Self::Surface>>;
|
||||
fn create_window(&self) -> SurfaceHandle<Self::Surface>;
|
||||
fn destroy_window(&self, surface_id: SurfaceId);
|
||||
fn gpu_executor(&self) -> Option<&Self::Executor> {
|
||||
None
|
||||
}
|
||||
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError>;
|
||||
}
|
||||
|
||||
impl<T: ApplicationIo> ApplicationIo for &T {
|
||||
type Surface = T::Surface;
|
||||
type Executor = T::Executor;
|
||||
|
||||
fn window(&self) -> Option<SurfaceHandle<Self::Surface>> {
|
||||
(**self).window()
|
||||
}
|
||||
|
||||
fn create_window(&self) -> SurfaceHandle<T::Surface> {
|
||||
(**self).create_window()
|
||||
}
|
||||
|
||||
fn destroy_window(&self, surface_id: SurfaceId) {
|
||||
(**self).destroy_window(surface_id)
|
||||
}
|
||||
|
||||
fn gpu_executor(&self) -> Option<&T::Executor> {
|
||||
(**self).gpu_executor()
|
||||
}
|
||||
|
||||
fn load_resource<'a>(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
|
||||
(**self).load_resource(url)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ApplicationError {
|
||||
NotFound,
|
||||
InvalidUrl,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NodeGraphUpdateMessage {
|
||||
// ImaginateStatusUpdate,
|
||||
}
|
||||
|
||||
pub trait NodeGraphUpdateSender {
|
||||
fn send(&self, message: NodeGraphUpdateMessage);
|
||||
}
|
||||
|
||||
impl<T: NodeGraphUpdateSender> NodeGraphUpdateSender for std::sync::Mutex<T> {
|
||||
fn send(&self, message: NodeGraphUpdateMessage) {
|
||||
self.lock().as_mut().unwrap().send(message)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GetEditorPreferences {
|
||||
// fn hostname(&self) -> &str;
|
||||
fn use_vello(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ExportFormat {
|
||||
#[default]
|
||||
Svg,
|
||||
Png {
|
||||
transparent: bool,
|
||||
},
|
||||
Jpeg,
|
||||
Canvas,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TimingInformation {
|
||||
pub time: f64,
|
||||
pub animation_time: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RenderConfig {
|
||||
pub viewport: Footprint,
|
||||
pub export_format: ExportFormat,
|
||||
pub time: TimingInformation,
|
||||
pub view_mode: ViewMode,
|
||||
pub hide_artboards: bool,
|
||||
pub for_export: bool,
|
||||
}
|
||||
|
||||
struct Logger;
|
||||
|
||||
impl NodeGraphUpdateSender for Logger {
|
||||
fn send(&self, message: NodeGraphUpdateMessage) {
|
||||
log::warn!("dispatching message with fallback node graph update sender {:?}", message);
|
||||
}
|
||||
}
|
||||
|
||||
struct DummyPreferences;
|
||||
|
||||
impl GetEditorPreferences for DummyPreferences {
|
||||
// fn hostname(&self) -> &str {
|
||||
// "dummy_endpoint"
|
||||
// }
|
||||
|
||||
fn use_vello(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorApi<Io> {
|
||||
/// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`].
|
||||
pub font_cache: FontCache,
|
||||
/// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web).
|
||||
pub application_io: Option<Arc<Io>>,
|
||||
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
|
||||
/// Editor preferences made available to the graph through the [`WasmEditorApi`].
|
||||
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<Io> Eq for EditorApi<Io> {}
|
||||
|
||||
impl<Io: Default> Default for EditorApi<Io> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_cache: FontCache::default(),
|
||||
application_io: None,
|
||||
node_graph_message_sender: Box::new(Logger),
|
||||
editor_preferences: Box::new(DummyPreferences),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> Hash for EditorApi<Io> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.font_cache.hash(state);
|
||||
self.application_io.as_ref().map_or(0, |io| io.as_ref() as *const _ as usize).hash(state);
|
||||
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
|
||||
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> PartialEq for EditorApi<Io> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.font_cache == other.font_cache
|
||||
&& self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize)
|
||||
&& std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _)
|
||||
&& std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for EditorApi<T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("EditorApi").field("font_cache", &self.font_cache).finish()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: StaticTypeSized> StaticType for EditorApi<T> {
|
||||
type Static = EditorApi<T::Static>;
|
||||
}
|
||||
240
node-graph/gcore/src/blending.rs
Normal file
240
node-graph/gcore/src/blending.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use dyn_any::DynAny;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AlphaBlending {
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f32,
|
||||
pub fill: f32,
|
||||
pub clip: bool,
|
||||
}
|
||||
impl Default for AlphaBlending {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl Hash for AlphaBlending {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.opacity.to_bits().hash(state);
|
||||
self.fill.to_bits().hash(state);
|
||||
self.blend_mode.hash(state);
|
||||
self.clip.hash(state);
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AlphaBlending {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
||||
write!(
|
||||
f,
|
||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
||||
self.blend_mode,
|
||||
round(self.opacity * 100.),
|
||||
round(self.fill * 100.),
|
||||
if self.clip { "Yes" } else { "No" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AlphaBlending {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
opacity: 1.,
|
||||
fill: 1.,
|
||||
blend_mode: BlendMode::Normal,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, t: f32) -> Self {
|
||||
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
|
||||
|
||||
AlphaBlending {
|
||||
opacity: lerp(self.opacity, other.opacity, t),
|
||||
fill: lerp(self.fill, other.fill, t),
|
||||
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
|
||||
clip: if t < 0.5 { self.clip } else { other.clip },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
|
||||
#[repr(i32)]
|
||||
pub enum BlendMode {
|
||||
// Basic group
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
// Darken group
|
||||
Darken,
|
||||
Multiply,
|
||||
ColorBurn,
|
||||
LinearBurn,
|
||||
DarkerColor,
|
||||
|
||||
// Lighten group
|
||||
Lighten,
|
||||
Screen,
|
||||
ColorDodge,
|
||||
LinearDodge,
|
||||
LighterColor,
|
||||
|
||||
// Contrast group
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
VividLight,
|
||||
LinearLight,
|
||||
PinLight,
|
||||
HardMix,
|
||||
|
||||
// Inversion group
|
||||
Difference,
|
||||
Exclusion,
|
||||
Subtract,
|
||||
Divide,
|
||||
|
||||
// Component group
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
|
||||
// Other stuff
|
||||
Erase,
|
||||
Restore,
|
||||
MultiplyAlpha,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// All standard blend modes ordered by group.
|
||||
pub fn list() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion, Subtract, Divide],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
/// The subset of [`BlendMode::list()`] that is supported by SVG.
|
||||
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn index_in_list(&self) -> Option<usize> {
|
||||
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
|
||||
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => Some("normal"),
|
||||
// Darken group
|
||||
BlendMode::Darken => Some("darken"),
|
||||
BlendMode::Multiply => Some("multiply"),
|
||||
BlendMode::ColorBurn => Some("color-burn"),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => Some("lighten"),
|
||||
BlendMode::Screen => Some("screen"),
|
||||
BlendMode::ColorDodge => Some("color-dodge"),
|
||||
// Contrast group
|
||||
BlendMode::Overlay => Some("overlay"),
|
||||
BlendMode::SoftLight => Some("soft-light"),
|
||||
BlendMode::HardLight => Some("hard-light"),
|
||||
// Inversion group
|
||||
BlendMode::Difference => Some("difference"),
|
||||
BlendMode::Exclusion => Some("exclusion"),
|
||||
// Component group
|
||||
BlendMode::Hue => Some("hue"),
|
||||
BlendMode::Saturation => Some("saturation"),
|
||||
BlendMode::Color => Some("color"),
|
||||
BlendMode::Luminosity => Some("luminosity"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the blend mode CSS style declaration.
|
||||
pub fn render(&self) -> String {
|
||||
format!(
|
||||
r#" mix-blend-mode: {};"#,
|
||||
self.to_svg_style_name().unwrap_or_else(|| {
|
||||
warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BlendMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => write!(f, "Normal"),
|
||||
// Darken group
|
||||
BlendMode::Darken => write!(f, "Darken"),
|
||||
BlendMode::Multiply => write!(f, "Multiply"),
|
||||
BlendMode::ColorBurn => write!(f, "Color Burn"),
|
||||
BlendMode::LinearBurn => write!(f, "Linear Burn"),
|
||||
BlendMode::DarkerColor => write!(f, "Darker Color"),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => write!(f, "Lighten"),
|
||||
BlendMode::Screen => write!(f, "Screen"),
|
||||
BlendMode::ColorDodge => write!(f, "Color Dodge"),
|
||||
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
|
||||
BlendMode::LighterColor => write!(f, "Lighter Color"),
|
||||
// Contrast group
|
||||
BlendMode::Overlay => write!(f, "Overlay"),
|
||||
BlendMode::SoftLight => write!(f, "Soft Light"),
|
||||
BlendMode::HardLight => write!(f, "Hard Light"),
|
||||
BlendMode::VividLight => write!(f, "Vivid Light"),
|
||||
BlendMode::LinearLight => write!(f, "Linear Light"),
|
||||
BlendMode::PinLight => write!(f, "Pin Light"),
|
||||
BlendMode::HardMix => write!(f, "Hard Mix"),
|
||||
// Inversion group
|
||||
BlendMode::Difference => write!(f, "Difference"),
|
||||
BlendMode::Exclusion => write!(f, "Exclusion"),
|
||||
BlendMode::Subtract => write!(f, "Subtract"),
|
||||
BlendMode::Divide => write!(f, "Divide"),
|
||||
// Component group
|
||||
BlendMode::Hue => write!(f, "Hue"),
|
||||
BlendMode::Saturation => write!(f, "Saturation"),
|
||||
BlendMode::Color => write!(f, "Color"),
|
||||
BlendMode::Luminosity => write!(f, "Luminosity"),
|
||||
// Other utility blend modes (hidden from the normal list)
|
||||
BlendMode::Erase => write!(f, "Erase"),
|
||||
BlendMode::Restore => write!(f, "Restore"),
|
||||
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
|
||||
}
|
||||
}
|
||||
}
|
||||
175
node-graph/gcore/src/blending_nodes.rs
Normal file
175
node-graph/gcore/src/blending_nodes.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use crate::raster::Image;
|
||||
use crate::raster_types::{CPU, RasterDataTable};
|
||||
use crate::registry::types::Percentage;
|
||||
use crate::vector::VectorDataTable;
|
||||
use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable};
|
||||
|
||||
pub(super) trait MultiplyAlpha {
|
||||
fn multiply_alpha(&mut self, factor: f64);
|
||||
}
|
||||
|
||||
impl MultiplyAlpha for Color {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for VectorDataTable {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for GraphicGroupTable {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for RasterDataTable<CPU>
|
||||
where
|
||||
GraphicElement: From<Image<Color>>,
|
||||
{
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait MultiplyFill {
|
||||
fn multiply_fill(&mut self, factor: f64);
|
||||
}
|
||||
impl MultiplyFill for Color {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for VectorDataTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for GraphicGroupTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for RasterDataTable<CPU> {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait SetBlendMode {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode);
|
||||
}
|
||||
|
||||
impl SetBlendMode for VectorDataTable {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for GraphicGroupTable {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for RasterDataTable<CPU> {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait SetClip {
|
||||
fn set_clip(&mut self, clip: bool);
|
||||
}
|
||||
|
||||
impl SetClip for VectorDataTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for GraphicGroupTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for RasterDataTable<CPU> {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blend_mode<T: SetBlendMode>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn opacity<T: MultiplyAlpha>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
#[default(100.)] fill: Percentage,
|
||||
#[default(false)] clip: bool,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value.multiply_fill(fill / 100.);
|
||||
value.set_clip(clip);
|
||||
value
|
||||
}
|
||||
24
node-graph/gcore/src/bounds.rs
Normal file
24
node-graph/gcore/src/bounds.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::Color;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
pub trait BoundingBox {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>;
|
||||
}
|
||||
|
||||
macro_rules! none_impl {
|
||||
($t:path) => {
|
||||
impl BoundingBox for $t {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
none_impl!(String);
|
||||
none_impl!(bool);
|
||||
none_impl!(f32);
|
||||
none_impl!(f64);
|
||||
none_impl!(DVec2);
|
||||
none_impl!(Option<Color>);
|
||||
none_impl!(Vec<Color>);
|
||||
@@ -1,18 +1,16 @@
|
||||
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
|
||||
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
|
||||
use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core::hash::Hash;
|
||||
use dyn_any::DynAny;
|
||||
use half::f16;
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::Euclid;
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RGBA16F {
|
||||
red: f16,
|
||||
green: f16,
|
||||
@@ -84,8 +82,7 @@ impl Alpha for RGBA16F {
|
||||
impl Pixel for RGBA16F {}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SRGBA8 {
|
||||
red: u8,
|
||||
green: u8,
|
||||
@@ -165,8 +162,7 @@ impl Alpha for SRGBA8 {
|
||||
impl Pixel for SRGBA8 {}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Luma(pub f32);
|
||||
|
||||
impl Luminance for Luma {
|
||||
@@ -206,8 +202,7 @@ impl Pixel for Luma {}
|
||||
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
|
||||
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Color {
|
||||
red: f32,
|
||||
green: f32,
|
||||
@@ -217,7 +212,7 @@ pub struct Color {
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl Hash for Color {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.red.to_bits().hash(state);
|
||||
self.green.to_bits().hash(state);
|
||||
self.blue.to_bits().hash(state);
|
||||
@@ -350,7 +345,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
|
||||
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
|
||||
///
|
||||
@@ -388,7 +383,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
|
||||
/// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
|
||||
/// assert_eq!(color, color2)
|
||||
@@ -403,7 +398,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
@@ -421,7 +416,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
|
||||
/// ```
|
||||
pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
|
||||
@@ -463,7 +458,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.r() == 0.114);
|
||||
/// ```
|
||||
@@ -476,7 +471,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.g() == 0.103);
|
||||
/// ```
|
||||
@@ -489,7 +484,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.b() == 0.98);
|
||||
/// ```
|
||||
@@ -502,7 +497,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert!(color.a() == 0.97);
|
||||
/// ```
|
||||
@@ -778,7 +773,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97));
|
||||
/// ```
|
||||
@@ -791,7 +786,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
@@ -808,7 +803,7 @@ impl Color {
|
||||
|
||||
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
@@ -818,7 +813,7 @@ impl Color {
|
||||
|
||||
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
@@ -830,7 +825,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
|
||||
/// // TODO: Add test
|
||||
/// ```
|
||||
@@ -845,7 +840,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
|
||||
/// ```
|
||||
pub fn to_hsla(&self) -> [f32; 4] {
|
||||
@@ -881,7 +876,7 @@ impl Color {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgba_str("7C67FA61").unwrap();
|
||||
/// ```
|
||||
pub fn from_rgba_str(color_str: &str) -> Option<Color> {
|
||||
@@ -899,7 +894,7 @@ impl Color {
|
||||
/// Creates a color from a 6-character RGB hex string (without a # prefix).
|
||||
///
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// use graphene_core::color::Color;
|
||||
/// let color = Color::from_rgb_str("7C67FA").unwrap();
|
||||
/// ```
|
||||
pub fn from_rgb_str(color_str: &str) -> Option<Color> {
|
||||
205
node-graph/gcore/src/color/color_traits.rs
Normal file
205
node-graph/gcore/src/color/color_traits.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::DVec2;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
|
||||
pub use crate::blending::*;
|
||||
|
||||
pub trait Linear {
|
||||
fn from_f32(x: f32) -> Self;
|
||||
fn to_f32(self) -> f32;
|
||||
fn from_f64(x: f64) -> Self;
|
||||
fn to_f64(self) -> f64;
|
||||
fn lerp(self, other: Self, value: Self) -> Self
|
||||
where
|
||||
Self: Sized + Copy,
|
||||
Self: std::ops::Sub<Self, Output = Self>,
|
||||
Self: std::ops::Mul<Self, Output = Self>,
|
||||
Self: std::ops::Add<Self, Output = Self>,
|
||||
{
|
||||
self + (other - self) * value
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl Linear for f32 {
|
||||
#[inline(always)] fn from_f32(x: f32) -> Self { x }
|
||||
#[inline(always)] fn to_f32(self) -> f32 { self }
|
||||
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
|
||||
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl Linear for f64 {
|
||||
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
|
||||
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
|
||||
#[inline(always)] fn from_f64(x: f64) -> Self { x }
|
||||
#[inline(always)] fn to_f64(self) -> f64 { self }
|
||||
}
|
||||
|
||||
pub trait Channel: Copy + Debug {
|
||||
fn to_linear<Out: Linear>(self) -> Out;
|
||||
fn from_linear<In: Linear>(linear: In) -> Self;
|
||||
}
|
||||
|
||||
pub trait LinearChannel: Channel {
|
||||
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
|
||||
Out::from_linear(self.to_linear::<f64>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Linear + Debug + Copy> Channel for T {
|
||||
#[inline(always)]
|
||||
fn to_linear<Out: Linear>(self) -> Out {
|
||||
Out::from_f64(self.to_f64())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_linear<In: Linear>(linear: In) -> Self {
|
||||
Self::from_f64(linear.to_f64())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Linear + Debug + Copy> LinearChannel for T {}
|
||||
|
||||
use num_derive::*;
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
|
||||
pub struct SRGBGammaFloat(f32);
|
||||
|
||||
impl Channel for SRGBGammaFloat {
|
||||
#[inline(always)]
|
||||
fn to_linear<Out: Linear>(self) -> Out {
|
||||
let x = self.0;
|
||||
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_linear<In: Linear>(linear: In) -> Self {
|
||||
let x = linear.to_f32();
|
||||
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
|
||||
}
|
||||
}
|
||||
pub trait RGBPrimaries {
|
||||
const RED: DVec2;
|
||||
const GREEN: DVec2;
|
||||
const BLUE: DVec2;
|
||||
const WHITE: DVec2;
|
||||
}
|
||||
pub trait Rec709Primaries {}
|
||||
impl<T: Rec709Primaries> RGBPrimaries for T {
|
||||
const RED: DVec2 = DVec2::new(0.64, 0.33);
|
||||
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
|
||||
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
|
||||
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
|
||||
}
|
||||
|
||||
pub trait SRGB: Rec709Primaries {}
|
||||
|
||||
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
pub trait Serde {}
|
||||
|
||||
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
impl<T> Serde for T {}
|
||||
|
||||
// TODO: Come up with a better name for this trait
|
||||
pub trait Pixel: Clone + Pod + Zeroable + Default {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
bytemuck::bytes_of(self).to_vec()
|
||||
}
|
||||
// TODO: use u8 for Color
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
|
||||
}
|
||||
|
||||
fn byte_size() -> usize {
|
||||
size_of::<Self>()
|
||||
}
|
||||
}
|
||||
pub trait RGB: Pixel {
|
||||
type ColorChannel: Channel;
|
||||
|
||||
fn red(&self) -> Self::ColorChannel;
|
||||
fn r(&self) -> Self::ColorChannel {
|
||||
self.red()
|
||||
}
|
||||
fn green(&self) -> Self::ColorChannel;
|
||||
fn g(&self) -> Self::ColorChannel {
|
||||
self.green()
|
||||
}
|
||||
fn blue(&self) -> Self::ColorChannel;
|
||||
fn b(&self) -> Self::ColorChannel {
|
||||
self.blue()
|
||||
}
|
||||
}
|
||||
pub trait RGBMut: RGB {
|
||||
fn set_red(&mut self, red: Self::ColorChannel);
|
||||
fn set_green(&mut self, green: Self::ColorChannel);
|
||||
fn set_blue(&mut self, blue: Self::ColorChannel);
|
||||
}
|
||||
|
||||
pub trait AssociatedAlpha: RGB + Alpha {
|
||||
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
|
||||
}
|
||||
|
||||
pub trait UnassociatedAlpha: RGB + Alpha {
|
||||
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
|
||||
}
|
||||
|
||||
pub trait Alpha {
|
||||
type AlphaChannel: LinearChannel;
|
||||
const TRANSPARENT: Self;
|
||||
fn alpha(&self) -> Self::AlphaChannel;
|
||||
fn a(&self) -> Self::AlphaChannel {
|
||||
self.alpha()
|
||||
}
|
||||
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
|
||||
}
|
||||
pub trait AlphaMut: Alpha {
|
||||
fn set_alpha(&mut self, value: Self::AlphaChannel);
|
||||
}
|
||||
|
||||
pub trait Depth {
|
||||
type DepthChannel: Channel;
|
||||
fn depth(&self) -> Self::DepthChannel;
|
||||
fn d(&self) -> Self::DepthChannel {
|
||||
self.depth()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtraChannels<const NUM: usize> {
|
||||
type ChannelType: Channel;
|
||||
fn extra_channels(&self) -> [Self::ChannelType; NUM];
|
||||
}
|
||||
|
||||
pub trait Luminance {
|
||||
type LuminanceChannel: LinearChannel;
|
||||
fn luminance(&self) -> Self::LuminanceChannel;
|
||||
fn l(&self) -> Self::LuminanceChannel {
|
||||
self.luminance()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LuminanceMut: Luminance {
|
||||
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
|
||||
}
|
||||
|
||||
// TODO: We might rename this to Raster at some point
|
||||
pub trait Sample {
|
||||
type Pixel: Pixel;
|
||||
// TODO: Add an area parameter
|
||||
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
|
||||
}
|
||||
|
||||
impl<T: Sample> Sample for &T {
|
||||
type Pixel = T::Pixel;
|
||||
|
||||
#[inline(always)]
|
||||
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
|
||||
(**self).sample(pos, area)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ pub fn float_to_srgb_u8(mut f: f32) -> u8 {
|
||||
// We clamped f to [0, 1], and the integer representations
|
||||
// of the positive finite non-NaN floats are monotonic.
|
||||
// This makes the later LUT lookup panicless.
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
unsafe { std::hint::unreachable_unchecked() }
|
||||
}
|
||||
|
||||
// Compute a piecewise linear interpolation that is always
|
||||
7
node-graph/gcore/src/color/mod.rs
Normal file
7
node-graph/gcore/src/color/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod color;
|
||||
mod color_traits;
|
||||
mod discrete_srgb;
|
||||
|
||||
pub use color::*;
|
||||
pub use color_traits::*;
|
||||
pub use discrete_srgb::*;
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::transform::Footprint;
|
||||
use core::any::Any;
|
||||
use core::borrow::Borrow;
|
||||
use core::panic::Location;
|
||||
use std::any::Any;
|
||||
use std::borrow::Borrow;
|
||||
use std::panic::Location;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Ctx: Clone + Send {}
|
||||
@@ -240,7 +240,7 @@ type DynBox = Box<dyn Any + Send + Sync>;
|
||||
|
||||
#[derive(dyn_any::DynAny)]
|
||||
pub struct OwnedContextImpl {
|
||||
footprint: Option<crate::transform::Footprint>,
|
||||
footprint: Option<Footprint>,
|
||||
varargs: Option<Arc<[DynBox]>>,
|
||||
parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
|
||||
// This could be converted into a single enum to save extra bytes
|
||||
@@ -249,8 +249,8 @@ pub struct OwnedContextImpl {
|
||||
animation_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for OwnedContextImpl {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
impl std::fmt::Debug for OwnedContextImpl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OwnedContextImpl")
|
||||
.field("footprint", &self.footprint)
|
||||
.field("varargs", &self.varargs)
|
||||
@@ -269,8 +269,8 @@ impl Default for OwnedContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for OwnedContextImpl {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for OwnedContextImpl {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.footprint.hash(state);
|
||||
self.varargs.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
|
||||
self.parent.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
|
||||
@@ -348,7 +348,7 @@ impl OwnedContextImpl {
|
||||
|
||||
#[derive(Default, Clone, Copy, dyn_any::DynAny)]
|
||||
pub struct ContextImpl<'a> {
|
||||
pub(crate) footprint: Option<&'a crate::transform::Footprint>,
|
||||
pub(crate) footprint: Option<&'a Footprint>,
|
||||
varargs: Option<&'a [DynRef<'a>]>,
|
||||
// This could be converted into a single enum to save extra bytes
|
||||
index: Option<usize>,
|
||||
|
||||
26
node-graph/gcore/src/debug.rs
Normal file
26
node-graph/gcore/src/debug.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use crate::raster_types::{CPU, RasterDataTable};
|
||||
use crate::{Color, Ctx};
|
||||
|
||||
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
|
||||
ty.size()
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
|
||||
Some(input)
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
|
||||
input.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Clones the input value.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
|
||||
value.clone()
|
||||
}
|
||||
21
node-graph/gcore/src/extract_xy.rs
Normal file
21
node-graph/gcore/src/extract_xy.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::Ctx;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
|
||||
/// Obtain the X or Y component of a coordinate.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
||||
match axis {
|
||||
XY::X => vector.into().x,
|
||||
XY::Y => vector.into().y,
|
||||
}
|
||||
}
|
||||
|
||||
/// The X or Y component of a coordinate.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum XY {
|
||||
#[default]
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::Node;
|
||||
use core::marker::PhantomData;
|
||||
use std::marker::PhantomData;
|
||||
#[derive(Clone)]
|
||||
pub struct FnNode<T: Fn(I) -> O, I, O>(T, PhantomData<(I, O)>);
|
||||
|
||||
|
||||
248
node-graph/gcore/src/gradient.rs
Normal file
248
node-graph/gcore/src/gradient.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use crate::Color;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum GradientType {
|
||||
#[default]
|
||||
Linear,
|
||||
Radial,
|
||||
}
|
||||
|
||||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||
// TODO: Use linear not gamma colors
|
||||
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct GradientStops(pub Vec<(f64, Color)>);
|
||||
|
||||
impl std::hash::Hash for GradientStops {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.len().hash(state);
|
||||
self.0.iter().for_each(|(position, color)| {
|
||||
position.to_bits().hash(state);
|
||||
color.hash(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GradientStops {
|
||||
fn default() -> Self {
|
||||
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a GradientStops {
|
||||
type Item = &'a (f64, Color);
|
||||
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for GradientStops {
|
||||
type Output = (f64, Color);
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for GradientStops {
|
||||
type Target = Vec<(f64, Color)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for GradientStops {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops {
|
||||
pub fn new(stops: Vec<(f64, Color)>) -> Self {
|
||||
let mut stops = Self(stops);
|
||||
stops.sort();
|
||||
stops
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.0.is_empty() {
|
||||
return Color::BLACK;
|
||||
}
|
||||
|
||||
if t <= self.0[0].0 {
|
||||
return self.0[0].1;
|
||||
}
|
||||
if t >= self.0[self.0.len() - 1].0 {
|
||||
return self.0[self.0.len() - 1].1;
|
||||
}
|
||||
|
||||
for i in 0..self.0.len() - 1 {
|
||||
let (t1, c1) = self.0[i];
|
||||
let (t2, c2) = self.0[i + 1];
|
||||
if t >= t1 && t <= t2 {
|
||||
let normalized_t = (t - t1) / (t2 - t1);
|
||||
return c1.lerp(&c2, normalized_t as f32);
|
||||
}
|
||||
}
|
||||
|
||||
Color::BLACK
|
||||
}
|
||||
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
|
||||
}
|
||||
|
||||
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
|
||||
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient fill.
|
||||
///
|
||||
/// Contains the start and end points, along with the colors at varying points along the length.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct Gradient {
|
||||
pub stops: GradientStops,
|
||||
pub gradient_type: GradientType,
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Default for Gradient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stops: GradientStops::default(),
|
||||
gradient_type: GradientType::Linear,
|
||||
start: DVec2::new(0., 0.5),
|
||||
end: DVec2::new(1., 0.5),
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Gradient {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stops.0.len().hash(state);
|
||||
[].iter()
|
||||
.chain(self.start.to_array().iter())
|
||||
.chain(self.end.to_array().iter())
|
||||
.chain(self.transform.to_cols_array().iter())
|
||||
.chain(self.stops.0.iter().map(|(position, _)| position))
|
||||
.for_each(|x| x.to_bits().hash(state));
|
||||
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
|
||||
self.gradient_type.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Gradient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
|
||||
transform,
|
||||
gradient_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let start = self.start + (other.start - self.start) * time;
|
||||
let end = self.end + (other.end - self.end) * time;
|
||||
let transform = self.transform;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.stops.0.iter())
|
||||
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
||||
let position = a_pos + (b_pos - a_pos) * time;
|
||||
let color = a_color.lerp(b_color, time as f32);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = GradientStops::new(stops);
|
||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
stops,
|
||||
gradient_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a stop into the gradient, the index if successful
|
||||
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
|
||||
// Transform the start and end positions to the same coordinate space as the mouse.
|
||||
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
|
||||
|
||||
// Calculate the new position by finding the closest point on the line
|
||||
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||
|
||||
// Don't insert point past end of line
|
||||
if !(0. ..=1.).contains(&new_position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute the color of the inserted stop
|
||||
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
|
||||
// Lerp between the nearest colors if applicable
|
||||
(a, Some(b)) => a.lerp(
|
||||
&b,
|
||||
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
|
||||
),
|
||||
// Use the start or the end color if applicable
|
||||
(v, _) => v,
|
||||
};
|
||||
|
||||
// Compute the correct index to keep the positions in order
|
||||
let mut index = 0;
|
||||
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let new_color = get_color(index - 1, new_position);
|
||||
|
||||
// Insert the new stop
|
||||
self.stops.0.insert(index, (new_position, new_color));
|
||||
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::blending::AlphaBlending;
|
||||
use crate::bounds::BoundingBox;
|
||||
use crate::instances::{Instance, Instances};
|
||||
use crate::raster::BlendMode;
|
||||
use crate::math::quad::Quad;
|
||||
use crate::raster::image::Image;
|
||||
use crate::raster_types::{CPU, GPU, Raster, RasterDataTable};
|
||||
use crate::transform::TransformMut;
|
||||
@@ -7,82 +9,20 @@ use crate::uuid::NodeId;
|
||||
use crate::vector::{VectorData, VectorDataTable};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use std::hash::Hash;
|
||||
|
||||
pub mod renderer;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[serde(default)]
|
||||
pub struct AlphaBlending {
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f32,
|
||||
pub fill: f32,
|
||||
pub clip: bool,
|
||||
}
|
||||
impl Default for AlphaBlending {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl core::hash::Hash for AlphaBlending {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.opacity.to_bits().hash(state);
|
||||
self.fill.to_bits().hash(state);
|
||||
self.blend_mode.hash(state);
|
||||
self.clip.hash(state);
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AlphaBlending {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
||||
write!(
|
||||
f,
|
||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
||||
self.blend_mode,
|
||||
round(self.opacity * 100.),
|
||||
round(self.fill * 100.),
|
||||
if self.clip { "Yes" } else { "No" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AlphaBlending {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
opacity: 1.,
|
||||
fill: 1.,
|
||||
blend_mode: BlendMode::Normal,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, t: f32) -> Self {
|
||||
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
|
||||
|
||||
AlphaBlending {
|
||||
opacity: lerp(self.opacity, other.opacity, t),
|
||||
fill: lerp(self.fill, other.fill, t),
|
||||
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
|
||||
clip: if t < 0.5 { self.clip } else { other.clip },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GraphicGroupTable, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldGraphicGroup {
|
||||
elements: Vec<(GraphicElement, Option<NodeId>)>,
|
||||
transform: DAffine2,
|
||||
alpha_blending: AlphaBlending,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GraphicGroup {
|
||||
elements: Vec<(GraphicElement, Option<NodeId>)>,
|
||||
}
|
||||
@@ -164,8 +104,7 @@ impl From<RasterDataTable<GPU>> for GraphicGroupTable {
|
||||
}
|
||||
|
||||
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GraphicElement {
|
||||
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
|
||||
GraphicGroup(GraphicGroupTable),
|
||||
@@ -245,6 +184,25 @@ impl GraphicElement {
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for GraphicElement {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform, include_stroke),
|
||||
GraphicElement::RasterDataCPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
GraphicElement::RasterDataGPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for GraphicGroupTable {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.filter_map(|element| element.instance.bounding_box(transform * *element.transform, include_stroke))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
@@ -281,8 +239,7 @@ impl serde::Serialize for Raster<GPU> {
|
||||
}
|
||||
|
||||
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Artboard {
|
||||
pub graphic_group: GraphicGroupTable,
|
||||
pub label: String,
|
||||
@@ -311,12 +268,25 @@ impl Artboard {
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Artboard {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
|
||||
if self.clip {
|
||||
Some(artboard_bounds)
|
||||
} else {
|
||||
[self.graphic_group.bounding_box(transform, include_stroke), Some(artboard_bounds)]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<ArtboardGroupTable, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ArtboardGroup {
|
||||
pub artboards: Vec<(Artboard, Option<NodeId>)>,
|
||||
}
|
||||
@@ -348,8 +318,21 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
|
||||
|
||||
pub type ArtboardGroupTable = Instances<Artboard>;
|
||||
|
||||
impl BoundingBox for ArtboardGroupTable {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.filter_map(|instance| instance.instance.bounding_box(transform, include_stroke))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable {
|
||||
async fn layer<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] mut stack: Instances<I>,
|
||||
#[implementations(GraphicElement, VectorData, Raster<CPU>, Raster<GPU>)] element: I,
|
||||
node_path: Vec<NodeId>,
|
||||
) -> Instances<I> {
|
||||
// Get the penultimate element of the node path, or None if the path is too short
|
||||
let source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
|
||||
|
||||
@@ -432,7 +415,7 @@ async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: boo
|
||||
output
|
||||
}
|
||||
|
||||
#[node_macro::node(category("General"))]
|
||||
#[node_macro::node(category("Vector"))]
|
||||
async fn flatten_vector(_: impl Ctx, group: GraphicGroupTable) -> VectorDataTable {
|
||||
// TODO: Avoid mutable reference, instead return a new GraphicGroupTable?
|
||||
fn flatten_group(output_group_table: &mut VectorDataTable, current_group_table: GraphicGroupTable) {
|
||||
@@ -571,3 +554,7 @@ impl From<GraphicGroupTable> for GraphicElement {
|
||||
GraphicElement::GraphicGroup(graphic_group)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToGraphicElement {
|
||||
fn to_graphic_element(&self) -> GraphicElement;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ impl<T> Instances<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn instance_ref_iter(&self) -> impl DoubleEndedIterator<Item = InstanceRef<T>> + Clone {
|
||||
pub fn instance_ref_iter(&self) -> impl DoubleEndedIterator<Item = InstanceRef<'_, T>> + Clone {
|
||||
self.instance
|
||||
.iter()
|
||||
.zip(self.mask.iter())
|
||||
@@ -78,7 +78,7 @@ impl<T> Instances<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn instance_mut_iter(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<T>> {
|
||||
pub fn instance_mut_iter(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<'_, T>> {
|
||||
self.instance
|
||||
.iter_mut()
|
||||
.zip(self.mask.iter_mut())
|
||||
@@ -94,7 +94,7 @@ impl<T> Instances<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<InstanceRef<T>> {
|
||||
pub fn get(&self, index: usize) -> Option<InstanceRef<'_, T>> {
|
||||
if index >= self.instance.len() {
|
||||
return None;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ impl<T> Instances<T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<InstanceMut<T>> {
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<InstanceMut<'_, T>> {
|
||||
if index >= self.instance.len() {
|
||||
return None;
|
||||
}
|
||||
@@ -143,8 +143,8 @@ impl<T> Default for Instances<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> core::hash::Hash for Instances<T> {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl<T: Hash> Hash for Instances<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
for instance in &self.instance {
|
||||
instance.hash(state);
|
||||
}
|
||||
@@ -230,7 +230,7 @@ impl<T> Instance<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_instance_ref(&self) -> InstanceRef<T> {
|
||||
pub fn to_instance_ref(&self) -> InstanceRef<'_, T> {
|
||||
InstanceRef {
|
||||
instance: &self.instance,
|
||||
mask: &self.mask,
|
||||
@@ -240,7 +240,7 @@ impl<T> Instance<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_instance_mut(&mut self) -> InstanceMut<T> {
|
||||
pub fn to_instance_mut(&mut self) -> InstanceMut<'_, T> {
|
||||
InstanceMut {
|
||||
instance: &mut self.instance,
|
||||
mask: &mut self.mask,
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
extern crate alloc;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
pub use crate as graphene_core;
|
||||
pub use num_traits;
|
||||
|
||||
pub use ctor;
|
||||
|
||||
pub mod animation;
|
||||
pub mod blending;
|
||||
pub mod blending_nodes;
|
||||
pub mod bounds;
|
||||
pub mod color;
|
||||
pub mod consts;
|
||||
pub mod context;
|
||||
pub mod debug;
|
||||
pub mod extract_xy;
|
||||
pub mod generic;
|
||||
pub mod gradient;
|
||||
mod graphic_element;
|
||||
pub mod instances;
|
||||
pub mod logic;
|
||||
pub mod math;
|
||||
pub mod memo;
|
||||
pub mod misc;
|
||||
pub mod ops;
|
||||
pub mod raster;
|
||||
pub mod raster_types;
|
||||
pub mod registry;
|
||||
pub mod structural;
|
||||
pub mod text;
|
||||
pub mod transform;
|
||||
pub mod transform_nodes;
|
||||
pub mod uuid;
|
||||
pub mod value;
|
||||
|
||||
pub mod memo;
|
||||
|
||||
pub mod raster;
|
||||
pub mod transform;
|
||||
|
||||
mod graphic_element;
|
||||
pub use graphic_element::*;
|
||||
pub mod vector;
|
||||
|
||||
pub mod application_io;
|
||||
|
||||
pub mod registry;
|
||||
|
||||
pub use crate as graphene_core;
|
||||
pub use blending::*;
|
||||
pub use context::*;
|
||||
use core::any::TypeId;
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
pub use ctor;
|
||||
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
|
||||
pub use graphic_element::*;
|
||||
pub use memo::MemoHash;
|
||||
pub use num_traits;
|
||||
pub use raster::Color;
|
||||
use std::any::TypeId;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
pub use types::Cow;
|
||||
|
||||
// pub trait Node: for<'n> NodeIO<'n> {
|
||||
@@ -54,11 +55,11 @@ pub trait Node<'i, Input> {
|
||||
fn reset(&self) {}
|
||||
/// Returns the name of the node for diagnostic purposes.
|
||||
fn node_name(&self) -> &'static str {
|
||||
core::any::type_name::<Self>()
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
/// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes.
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
log::warn!("Node::serialize not implemented for {}", core::any::type_name::<Self>());
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
log::warn!("Node::serialize not implemented for {}", std::any::type_name::<Self>());
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -75,13 +76,13 @@ where
|
||||
TypeId::of::<Input::Static>()
|
||||
}
|
||||
fn input_type_name(&self) -> &'static str {
|
||||
core::any::type_name::<Input>()
|
||||
std::any::type_name::<Input>()
|
||||
}
|
||||
fn output_type(&self) -> core::any::TypeId {
|
||||
fn output_type(&self) -> TypeId {
|
||||
TypeId::of::<<Self::Output as StaticTypeSized>::Static>()
|
||||
}
|
||||
fn output_type_name(&self) -> &'static str {
|
||||
core::any::type_name::<Self::Output>()
|
||||
std::any::type_name::<Self::Output>()
|
||||
}
|
||||
fn to_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes {
|
||||
NodeIOTypes {
|
||||
@@ -122,7 +123,7 @@ impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for Box<
|
||||
(**self).eval(input)
|
||||
}
|
||||
}
|
||||
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for alloc::sync::Arc<N> {
|
||||
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for std::sync::Arc<N> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> O {
|
||||
(**self).eval(input)
|
||||
@@ -142,13 +143,7 @@ impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)>
|
||||
}
|
||||
}
|
||||
|
||||
pub use crate::application_io::{SurfaceFrame, SurfaceId};
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandle = application_io::SurfaceHandle<web_sys::HtmlCanvasElement>;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
|
||||
|
||||
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + core::fmt::Debug {
|
||||
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug {
|
||||
fn get_input(&'a self, index: usize) -> Option<&'a T>;
|
||||
fn set_input(&'a mut self, index: usize, value: T);
|
||||
}
|
||||
@@ -169,3 +164,12 @@ pub trait NodeInputDecleration {
|
||||
fn identifier() -> &'static str;
|
||||
type Result;
|
||||
}
|
||||
|
||||
pub trait AsU32 {
|
||||
fn as_u32(&self) -> u32;
|
||||
}
|
||||
impl AsU32 for u32 {
|
||||
fn as_u32(&self) -> u32 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,15 @@ use crate::vector::VectorDataTable;
|
||||
use crate::{Color, Context, Ctx};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
#[node_macro::node(category("Debug"), name("Log to Console"))]
|
||||
fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
|
||||
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
|
||||
log::debug!("{:#?}", value);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Text"))]
|
||||
fn to_string<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
|
||||
fn to_string<T: std::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
|
||||
format!("{:?}", value)
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@ fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usiz
|
||||
string.len()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Text"))]
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
async fn switch<T, C: Send + 'n + Clone>(
|
||||
#[implementations(Context)] ctx: C,
|
||||
condition: bool,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
|
||||
#[derive(Clone, DynAny)]
|
||||
#[derive(Clone, Debug, DynAny)]
|
||||
pub struct AxisAlignedBbox {
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
@@ -60,8 +59,7 @@ impl From<(DVec2, DVec2)> for AxisAlignedBbox {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Bbox {
|
||||
pub top_left: DVec2,
|
||||
pub top_right: DVec2,
|
||||
25
node-graph/gcore/src/math/math_ext.rs
Normal file
25
node-graph/gcore/src/math/math_ext.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::math::quad::Quad;
|
||||
use crate::math::rect::Rect;
|
||||
use bezier_rs::Bezier;
|
||||
|
||||
pub trait QuadExt {
|
||||
/// Get all the edges in the rect as linear bezier curves
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
|
||||
}
|
||||
|
||||
impl QuadExt for Quad {
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
|
||||
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RectExt {
|
||||
/// Get all the edges in the quad as linear bezier curves
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
|
||||
}
|
||||
|
||||
impl RectExt for Rect {
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
|
||||
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
4
node-graph/gcore/src/math/mod.rs
Normal file
4
node-graph/gcore/src/math/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod bbox;
|
||||
pub mod math_ext;
|
||||
pub mod quad;
|
||||
pub mod rect;
|
||||
@@ -58,11 +58,6 @@ impl Quad {
|
||||
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
|
||||
}
|
||||
|
||||
/// Get all the edges in the quad as linear bezier curves
|
||||
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
|
||||
self.all_edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
|
||||
/// Generates the axis aligned bounding box of the quad
|
||||
pub fn bounding_box(&self) -> [DVec2; 2] {
|
||||
[
|
||||
@@ -143,7 +138,7 @@ impl Quad {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Mul<Quad> for DAffine2 {
|
||||
impl std::ops::Mul<Quad> for DAffine2 {
|
||||
type Output = Quad;
|
||||
|
||||
fn mul(self, rhs: Quad) -> Self::Output {
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::Quad;
|
||||
use crate::math::quad::Quad;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq)]
|
||||
@@ -43,11 +43,6 @@ impl Rect {
|
||||
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
|
||||
}
|
||||
|
||||
/// Get all the edges in the rect as linear bezier curves
|
||||
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
|
||||
self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
|
||||
/// Gets the center of a rect
|
||||
#[must_use]
|
||||
pub fn center(&self) -> DVec2 {
|
||||
@@ -97,21 +92,21 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Mul<Rect> for DAffine2 {
|
||||
type Output = super::Quad;
|
||||
impl std::ops::Mul<Rect> for DAffine2 {
|
||||
type Output = Quad;
|
||||
|
||||
fn mul(self, rhs: Rect) -> Self::Output {
|
||||
self * super::Quad::from_box(rhs.0)
|
||||
self * Quad::from_box(rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Index<usize> for Rect {
|
||||
impl std::ops::Index<usize> for Rect {
|
||||
type Output = DVec2;
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
impl core::ops::IndexMut<usize> for Rect {
|
||||
impl std::ops::IndexMut<usize> for Rect {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
&mut self.0[index]
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{Node, WasmNotSend};
|
||||
use alloc::sync::Arc;
|
||||
use core::future::Future;
|
||||
use core::ops::Deref;
|
||||
use dyn_any::DynFuture;
|
||||
use std::future::Future;
|
||||
use std::hash::DefaultHasher;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Caches the output of a given Node and acts as a proxy
|
||||
@@ -15,7 +15,7 @@ pub struct MemoNode<T, CachedNode> {
|
||||
impl<'i, I: Hash + 'i, T: 'i + Clone + WasmNotSend, CachedNode: 'i> Node<'i, I> for MemoNode<T, CachedNode>
|
||||
where
|
||||
CachedNode: for<'any_input> Node<'any_input, I>,
|
||||
for<'a> <CachedNode as Node<'a, I>>::Output: core::future::Future<Output = T> + WasmNotSend,
|
||||
for<'a> <CachedNode as Node<'a, I>>::Output: Future<Output = T> + WasmNotSend,
|
||||
{
|
||||
// TODO: This should return a reference to the cached cached_value
|
||||
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
|
||||
@@ -63,7 +63,7 @@ pub struct ImpureMemoNode<I, T, CachedNode> {
|
||||
impl<'i, I: 'i, T: 'i + Clone + WasmNotSend, CachedNode: 'i> Node<'i, I> for ImpureMemoNode<I, T, CachedNode>
|
||||
where
|
||||
CachedNode: for<'any_input> Node<'any_input, I>,
|
||||
for<'a> <CachedNode as Node<'a, I>>::Output: core::future::Future<Output = T> + WasmNotSend,
|
||||
for<'a> <CachedNode as Node<'a, I>>::Output: Future<Output = T> + WasmNotSend,
|
||||
{
|
||||
// TODO: This should return a reference to the cached cached_value
|
||||
// but that requires a lot of lifetime magic <- This was suggested by copilot but is pretty accurate xD
|
||||
@@ -93,7 +93,7 @@ impl<T, I, CachedNode> ImpureMemoNode<I, T, CachedNode> {
|
||||
ImpureMemoNode {
|
||||
cache: Default::default(),
|
||||
node,
|
||||
_phantom: core::marker::PhantomData,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,9 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<Arc<dyn core::any::Any + Send + Sync>> {
|
||||
fn serialize(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
|
||||
let io = self.io.lock().unwrap();
|
||||
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any + Send + Sync>)
|
||||
(io).as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,14 +142,13 @@ impl<I, T, N> MonitorNode<I, T, N> {
|
||||
}
|
||||
}
|
||||
|
||||
use core::hash::{Hash, Hasher};
|
||||
use std::hash::{Hash, Hasher};
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct MemoHash<T: Hash> {
|
||||
hash: u64,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
@@ -159,7 +158,6 @@ impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHas
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -179,12 +177,12 @@ impl<T: Hash> MemoHash<T> {
|
||||
}
|
||||
|
||||
fn calc_hash(data: &T) -> u64 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
let mut hasher = DefaultHasher::new();
|
||||
data.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> MemoHashGuard<T> {
|
||||
pub fn inner_mut(&mut self) -> MemoHashGuard<'_, T> {
|
||||
MemoHashGuard { inner: self }
|
||||
}
|
||||
pub fn into_inner(self) -> T {
|
||||
@@ -206,7 +204,7 @@ impl<T: Hash> Hash for MemoHash<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> core::ops::Deref for MemoHash<T> {
|
||||
impl<T: Hash> Deref for MemoHash<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -218,14 +216,14 @@ pub struct MemoHashGuard<'a, T: Hash> {
|
||||
inner: &'a mut MemoHash<T>,
|
||||
}
|
||||
|
||||
impl<T: Hash> core::ops::Drop for MemoHashGuard<'_, T> {
|
||||
impl<T: Hash> Drop for MemoHashGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
let hash = MemoHash::<T>::calc_hash(&self.inner.value);
|
||||
self.inner.hash = hash;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> core::ops::Deref for MemoHashGuard<'_, T> {
|
||||
impl<T: Hash> Deref for MemoHashGuard<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -233,7 +231,7 @@ impl<T: Hash> core::ops::Deref for MemoHashGuard<'_, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> core::ops::DerefMut for MemoHashGuard<'_, T> {
|
||||
impl<T: Hash> std::ops::DerefMut for MemoHashGuard<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner.value
|
||||
}
|
||||
|
||||
@@ -1,546 +1,5 @@
|
||||
use crate::Ctx;
|
||||
use crate::raster::BlendMode;
|
||||
use crate::raster_types::{CPU, RasterDataTable};
|
||||
use crate::registry::types::{Fraction, Percentage};
|
||||
use crate::vector::style::GradientStops;
|
||||
use crate::{Color, Node};
|
||||
use core::marker::PhantomData;
|
||||
use core::ops::{Add, Div, Mul, Rem, Sub};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
use math_parser::ast;
|
||||
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
|
||||
use math_parser::value::{Number, Value};
|
||||
use num_traits::Pow;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
|
||||
/// The struct that stores the context for the maths parser.
|
||||
/// This is currently just limited to supplying `a` and `b` until we add better node graph support and UI for variadic inputs.
|
||||
struct MathNodeContext {
|
||||
a: f64,
|
||||
b: f64,
|
||||
}
|
||||
|
||||
impl ValueProvider for MathNodeContext {
|
||||
fn get_value(&self, name: &str) -> Option<Value> {
|
||||
if name.eq_ignore_ascii_case("a") {
|
||||
Some(Value::from_f64(self.a))
|
||||
} else if name.eq_ignore_ascii_case("b") {
|
||||
Some(Value::from_f64(self.b))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates a mathematical expression with input values "A" and "B"
|
||||
#[node_macro::node(category("General"), properties("math_properties"))]
|
||||
fn math<U: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
/// The value of "A" when calculating the expression
|
||||
#[implementations(f64, f32)]
|
||||
operand_a: U,
|
||||
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"
|
||||
#[default(A + B)]
|
||||
expression: String,
|
||||
/// The value of "B" when calculating the expression
|
||||
#[implementations(f64, f32)]
|
||||
#[default(1.)]
|
||||
operand_b: U,
|
||||
) -> U {
|
||||
let (node, _unit) = match ast::Node::try_parse_from_str(&expression) {
|
||||
Ok(expr) => expr,
|
||||
Err(e) => {
|
||||
warn!("Invalid expression: `{expression}`\n{e:?}");
|
||||
return U::from(0.).unwrap();
|
||||
}
|
||||
};
|
||||
let context = EvalContext::new(
|
||||
MathNodeContext {
|
||||
a: operand_a.to_f64().unwrap(),
|
||||
b: operand_b.to_f64().unwrap(),
|
||||
},
|
||||
NothingMap,
|
||||
);
|
||||
|
||||
let value = match node.eval(&context) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
warn!("Expression evaluation error: {e:?}");
|
||||
return U::from(0.).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
let Value::Number(num) = value;
|
||||
match num {
|
||||
Number::Real(val) => U::from(val).unwrap(),
|
||||
Number::Complex(c) => U::from(c.re).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The addition operation (+) calculates the sum of two numbers.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn add<U: Add<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] augend: U,
|
||||
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] addend: T,
|
||||
) -> <U as Add<T>>::Output {
|
||||
augend + addend
|
||||
}
|
||||
|
||||
/// The subtraction operation (-) calculates the difference between two numbers.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn subtract<U: Sub<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] minuend: U,
|
||||
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] subtrahend: T,
|
||||
) -> <U as Sub<T>>::Output {
|
||||
minuend - subtrahend
|
||||
}
|
||||
|
||||
/// The multiplication operation (×) calculates the product of two numbers.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn multiply<U: Mul<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] multiplier: U,
|
||||
#[default(1.)]
|
||||
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)]
|
||||
multiplicand: T,
|
||||
) -> <U as Mul<T>>::Output {
|
||||
multiplier * multiplicand
|
||||
}
|
||||
|
||||
/// The division operation (÷) calculates the quotient of two numbers.
|
||||
///
|
||||
/// Produces 0 if the denominator is 0.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, DVec2, f64)] numerator: U,
|
||||
#[default(1.)]
|
||||
#[implementations(f64, f64, f32, f32, u32, u32, DVec2, f64, DVec2)]
|
||||
denominator: T,
|
||||
) -> <U as Div<T>>::Output
|
||||
where
|
||||
<U as Div<T>>::Output: Default,
|
||||
{
|
||||
if denominator == T::default() {
|
||||
return <U as Div<T>>::Output::default();
|
||||
}
|
||||
numerator / denominator
|
||||
}
|
||||
|
||||
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
|
||||
modulus: T,
|
||||
always_positive: bool,
|
||||
) -> <U as Rem<T>>::Output {
|
||||
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
|
||||
}
|
||||
|
||||
/// The exponent operation (^) calculates the result of raising a number to a power.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn exponent<U: Pow<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32)] base: U,
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32)]
|
||||
power: T,
|
||||
) -> <U as num_traits::Pow<T>>::Output {
|
||||
base.pow(power)
|
||||
}
|
||||
|
||||
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn root<U: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32)]
|
||||
radicand: U,
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32)]
|
||||
degree: U,
|
||||
) -> U {
|
||||
if degree == U::from(2.).unwrap() {
|
||||
radicand.sqrt()
|
||||
} else if degree == U::from(3.).unwrap() {
|
||||
radicand.cbrt()
|
||||
} else {
|
||||
radicand.powf(U::from(1.).unwrap() / degree)
|
||||
}
|
||||
}
|
||||
|
||||
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
|
||||
#[node_macro::node(category("Math: Arithmetic"))]
|
||||
fn logarithm<U: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, f32)] value: U,
|
||||
#[default(2.)]
|
||||
#[implementations(f64, f32)]
|
||||
base: U,
|
||||
) -> U {
|
||||
if base == U::from(2.).unwrap() {
|
||||
value.log2()
|
||||
} else if base == U::from(10.).unwrap() {
|
||||
value.log10()
|
||||
} else if base - U::from(std::f64::consts::E).unwrap() < U::epsilon() * U::from(1e6).unwrap() {
|
||||
value.ln()
|
||||
} else {
|
||||
value.log(base)
|
||||
}
|
||||
}
|
||||
|
||||
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn sine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
|
||||
if radians { theta.sin() } else { theta.to_radians().sin() }
|
||||
}
|
||||
|
||||
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn cosine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
|
||||
if radians { theta.cos() } else { theta.to_radians().cos() }
|
||||
}
|
||||
|
||||
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn tangent<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
|
||||
if radians { theta.tan() } else { theta.to_radians().tan() }
|
||||
}
|
||||
|
||||
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn sine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
|
||||
if radians { value.asin() } else { value.asin().to_degrees() }
|
||||
}
|
||||
|
||||
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
|
||||
if radians { value.acos() } else { value.acos().to_degrees() }
|
||||
}
|
||||
|
||||
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
|
||||
/// atan: the angle whose tangent is the specified scalar number.
|
||||
/// atan2: the angle of a ray from the origin to the specified coordinate.
|
||||
#[node_macro::node(category("Math: Trig"))]
|
||||
fn tangent_inverse<U: TangentInverse>(_: impl Ctx, #[implementations(f64, f32, DVec2)] value: U, radians: bool) -> U::Output {
|
||||
value.atan(radians)
|
||||
}
|
||||
|
||||
pub trait TangentInverse {
|
||||
type Output: num_traits::float::Float;
|
||||
fn atan(self, radians: bool) -> Self::Output;
|
||||
}
|
||||
impl TangentInverse for f32 {
|
||||
type Output = f32;
|
||||
fn atan(self, radians: bool) -> Self::Output {
|
||||
if radians { self.atan() } else { self.atan().to_degrees() }
|
||||
}
|
||||
}
|
||||
impl TangentInverse for f64 {
|
||||
type Output = f64;
|
||||
fn atan(self, radians: bool) -> Self::Output {
|
||||
if radians { self.atan() } else { self.atan().to_degrees() }
|
||||
}
|
||||
}
|
||||
impl TangentInverse for glam::DVec2 {
|
||||
type Output = f64;
|
||||
fn atan(self, radians: bool) -> Self::Output {
|
||||
if radians { self.y.atan2(self.x) } else { self.y.atan2(self.x).to_degrees() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn random<U: num_traits::float::Float>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
seed: u64,
|
||||
#[implementations(f64, f32)]
|
||||
#[default(0.)]
|
||||
min: U,
|
||||
#[implementations(f64, f32)]
|
||||
#[default(1.)]
|
||||
max: U,
|
||||
) -> f64 {
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
|
||||
let result = rng.random::<f64>();
|
||||
let (min, max) = if min < max { (min, max) } else { (max, min) };
|
||||
let (min, max) = (min.to_f64().unwrap(), max.to_f64().unwrap());
|
||||
result * (max - min) + min
|
||||
}
|
||||
|
||||
/// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
|
||||
#[node_macro::node(name("To u32"), category("Math: Numeric"))]
|
||||
fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
|
||||
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
|
||||
value.to_u32().unwrap()
|
||||
}
|
||||
|
||||
/// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
|
||||
#[node_macro::node(name("To u64"), category("Math: Numeric"))]
|
||||
fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
|
||||
let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
|
||||
value.to_u64().unwrap()
|
||||
}
|
||||
|
||||
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
|
||||
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
|
||||
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
|
||||
value.to_f64().unwrap()
|
||||
}
|
||||
|
||||
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
|
||||
value.round()
|
||||
}
|
||||
|
||||
/// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn floor<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
|
||||
value.floor()
|
||||
}
|
||||
|
||||
/// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn ceiling<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
|
||||
value.ceil()
|
||||
}
|
||||
|
||||
/// The absolute value function (abs) removes the negative sign from an input value, if present.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
|
||||
value.abs()
|
||||
}
|
||||
|
||||
/// The minimum function (min) picks the smaller of two numbers.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn min<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
|
||||
if value < other_value { value } else { other_value }
|
||||
}
|
||||
|
||||
/// The maximum function (max) picks the larger of two numbers.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn max<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
|
||||
if value > other_value { value } else { other_value }
|
||||
}
|
||||
|
||||
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn clamp<T: core::cmp::PartialOrd>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T,
|
||||
) -> T {
|
||||
let (min, max) = if min < max { (min, max) } else { (max, min) };
|
||||
if value < min {
|
||||
min
|
||||
} else if value > max {
|
||||
max
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn equals<U: core::cmp::PartialEq<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
|
||||
) -> bool {
|
||||
other_value == value
|
||||
}
|
||||
|
||||
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn not_equals<U: core::cmp::PartialEq<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
|
||||
) -> bool {
|
||||
other_value != value
|
||||
}
|
||||
|
||||
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
|
||||
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn less_than<T: core::cmp::PartialOrd<T>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
|
||||
or_equal: bool,
|
||||
) -> bool {
|
||||
if or_equal { value <= other_value } else { value < other_value }
|
||||
}
|
||||
|
||||
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
|
||||
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn greater_than<T: core::cmp::PartialOrd<T>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32)] other_value: T,
|
||||
or_equal: bool,
|
||||
) -> bool {
|
||||
if or_equal { value >= other_value } else { value > other_value }
|
||||
}
|
||||
|
||||
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_or(_: impl Ctx, value: bool, other_value: bool) -> bool {
|
||||
value || other_value
|
||||
}
|
||||
|
||||
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_and(_: impl Ctx, value: bool, other_value: bool) -> bool {
|
||||
value && other_value
|
||||
}
|
||||
|
||||
/// The logical not operation (!) reverses true and false value of the input.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn logical_not(_: impl Ctx, input: bool) -> bool {
|
||||
!input
|
||||
}
|
||||
|
||||
/// Constructs a bool value which may be set to true or false.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool {
|
||||
bool_value
|
||||
}
|
||||
|
||||
/// Constructs a number value which may be set to any real number.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 {
|
||||
number
|
||||
}
|
||||
|
||||
/// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
|
||||
percentage
|
||||
}
|
||||
|
||||
/// Constructs a two-dimensional vector value which may be set to any XY coordinate.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn coordinate_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
|
||||
DVec2::new(x, y)
|
||||
}
|
||||
|
||||
/// Constructs a color value which may be set to any color, or no color.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
|
||||
color
|
||||
}
|
||||
|
||||
// // Aims for interoperable compatibility with:
|
||||
// // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
|
||||
// // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
|
||||
// #[node_macro::node(category("Raster: Adjustment"))]
|
||||
// async fn gradient_map<T: Adjust<Color>>(
|
||||
// _: impl Ctx,
|
||||
// #[implementations(
|
||||
// Color,
|
||||
// RasterDataTable<CPU>,
|
||||
// GradientStops,
|
||||
// )]
|
||||
// mut image: T,
|
||||
// gradient: GradientStops,
|
||||
// reverse: bool,
|
||||
// ) -> T {
|
||||
// image.adjust(|color| {
|
||||
// let intensity = color.luminance_srgb();
|
||||
// let intensity = if reverse { 1. - intensity } else { intensity };
|
||||
// gradient.evaluate(intensity as f64)
|
||||
// });
|
||||
|
||||
// image
|
||||
// }
|
||||
|
||||
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
|
||||
#[node_macro::node(category("General"))]
|
||||
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Color {
|
||||
let position = position.clamp(0., 1.);
|
||||
gradient.evaluate(position)
|
||||
}
|
||||
|
||||
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
|
||||
gradient
|
||||
}
|
||||
|
||||
/// Constructs a blend mode choice value which may be set to any of the available blend modes in order to tell another node which blending operation to use.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn blend_mode_value(_: impl Ctx, _primary: (), blend_mode: BlendMode) -> BlendMode {
|
||||
blend_mode
|
||||
}
|
||||
|
||||
/// Constructs a string value which may be set to any plain text.
|
||||
#[node_macro::node(category("Value"))]
|
||||
fn string_value(_: impl Ctx, _primary: (), string: String) -> String {
|
||||
string
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
|
||||
ty.size()
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
|
||||
Some(input)
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
|
||||
input.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Meant for debugging purposes, not general use. Clones the input value.
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&RasterDataTable<CPU>)] value: &'i T) -> T {
|
||||
value.clone()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Math: Vector"))]
|
||||
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
|
||||
vector_a.dot(vector_b)
|
||||
}
|
||||
|
||||
/// Obtain the X or Y component of a coordinate.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
||||
match axis {
|
||||
XY::X => vector.into().x,
|
||||
XY::Y => vector.into().y,
|
||||
}
|
||||
}
|
||||
|
||||
/// The X or Y component of a coordinate.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum XY {
|
||||
#[default]
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
use crate::Node;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// TODO: Rename to "Passthrough"
|
||||
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
|
||||
@@ -566,7 +25,7 @@ where
|
||||
self.0.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.0.serialize()
|
||||
}
|
||||
}
|
||||
@@ -586,7 +45,7 @@ impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Nod
|
||||
pub struct IntoNode<O>(PhantomData<O>);
|
||||
impl<O> IntoNode<O> {
|
||||
pub const fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
Self(PhantomData)
|
||||
}
|
||||
}
|
||||
impl<O> Default for IntoNode<O> {
|
||||
@@ -598,7 +57,7 @@ impl<'input, I: 'input, O: 'input> Node<'input, I> for IntoNode<O>
|
||||
where
|
||||
I: Into<O> + Sync + Send,
|
||||
{
|
||||
type Output = ::dyn_any::DynFuture<'input, O>;
|
||||
type Output = dyn_any::DynFuture<'input, O>;
|
||||
|
||||
#[inline]
|
||||
fn eval(&'input self, input: I) -> Self::Output {
|
||||
@@ -606,75 +65,82 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
|
||||
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
|
||||
pub trait Convert<T>: Sized {
|
||||
/// Converts this type into the (usually inferred) output type.
|
||||
#[must_use]
|
||||
fn convert(self) -> T;
|
||||
}
|
||||
|
||||
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
|
||||
macro_rules! impl_convert {
|
||||
($from:ty,$to:ty) => {
|
||||
impl Convert<$to> for $from {
|
||||
fn convert(self) -> $to {
|
||||
self as $to
|
||||
}
|
||||
}
|
||||
};
|
||||
($to:ty) => {
|
||||
impl_convert!(f32, $to);
|
||||
impl_convert!(f64, $to);
|
||||
impl_convert!(i8, $to);
|
||||
impl_convert!(u8, $to);
|
||||
impl_convert!(u16, $to);
|
||||
impl_convert!(i16, $to);
|
||||
impl_convert!(i32, $to);
|
||||
impl_convert!(u32, $to);
|
||||
impl_convert!(i64, $to);
|
||||
impl_convert!(u64, $to);
|
||||
impl_convert!(i128, $to);
|
||||
impl_convert!(u128, $to);
|
||||
impl_convert!(isize, $to);
|
||||
impl_convert!(usize, $to);
|
||||
};
|
||||
}
|
||||
impl_convert!(f32);
|
||||
impl_convert!(f64);
|
||||
impl_convert!(i8);
|
||||
impl_convert!(u8);
|
||||
impl_convert!(u16);
|
||||
impl_convert!(i16);
|
||||
impl_convert!(i32);
|
||||
impl_convert!(u32);
|
||||
impl_convert!(i64);
|
||||
impl_convert!(u64);
|
||||
impl_convert!(i128);
|
||||
impl_convert!(u128);
|
||||
impl_convert!(isize);
|
||||
impl_convert!(usize);
|
||||
|
||||
// Convert
|
||||
pub struct ConvertNode<O>(PhantomData<O>);
|
||||
impl<_O> ConvertNode<_O> {
|
||||
pub const fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
impl<_O> Default for ConvertNode<_O> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I> for ConvertNode<_O> {
|
||||
type Output = ::dyn_any::DynFuture<'input, _O>;
|
||||
|
||||
#[inline]
|
||||
fn eval(&'input self, input: I) -> Self::Output {
|
||||
Box::pin(async move { input.convert() })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::generic::*;
|
||||
|
||||
#[test]
|
||||
pub fn dot_product_function() {
|
||||
let vector_a = glam::DVec2::new(1., 2.);
|
||||
let vector_b = glam::DVec2::new(3., 4.);
|
||||
assert_eq!(dot_product((), vector_a, vector_b), 11.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_expression() {
|
||||
let result = math((), 0., "2 + 2".to_string(), 0.);
|
||||
assert_eq!(result, 4.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_expression() {
|
||||
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
|
||||
assert_eq!(result, 20.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_expression() {
|
||||
let result = math((), 0., "0".to_string(), 0.);
|
||||
assert_eq!(result, 0.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_expression() {
|
||||
let result = math((), 0., "invalid".to_string(), 0.);
|
||||
assert_eq!(result, 0.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn identity_node() {
|
||||
assert_eq!(identity(&4), &4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn foo() {
|
||||
let fnn = FnNode::new(|(a, b)| (b, a));
|
||||
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn add_vectors() {
|
||||
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn subtract_f64() {
|
||||
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn divide_vectors() {
|
||||
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn modulo_positive() {
|
||||
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn modulo_negative() {
|
||||
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,226 +1,25 @@
|
||||
pub use self::color::{Color, Luma, SRGBA8};
|
||||
use crate::Ctx;
|
||||
use crate::GraphicGroupTable;
|
||||
pub use crate::color::*;
|
||||
use crate::raster_types::{CPU, RasterDataTable};
|
||||
use crate::registry::types::Percentage;
|
||||
use crate::vector::VectorDataTable;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core::fmt::Debug;
|
||||
use glam::DVec2;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
|
||||
/// as to not yet rename all references
|
||||
pub mod color {
|
||||
pub use super::*;
|
||||
}
|
||||
|
||||
pub mod adjustments;
|
||||
pub mod bbox;
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
pub mod brush_cache;
|
||||
pub mod color;
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
pub mod curve;
|
||||
pub mod discrete_srgb;
|
||||
pub mod image;
|
||||
|
||||
pub use self::image::Image;
|
||||
pub use adjustments::*;
|
||||
|
||||
pub trait Linear {
|
||||
fn from_f32(x: f32) -> Self;
|
||||
fn to_f32(self) -> f32;
|
||||
fn from_f64(x: f64) -> Self;
|
||||
fn to_f64(self) -> f64;
|
||||
fn lerp(self, other: Self, value: Self) -> Self
|
||||
where
|
||||
Self: Sized + Copy,
|
||||
Self: core::ops::Sub<Self, Output = Self>,
|
||||
Self: core::ops::Mul<Self, Output = Self>,
|
||||
Self: core::ops::Add<Self, Output = Self>,
|
||||
{
|
||||
self + (other - self) * value
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl Linear for f32 {
|
||||
#[inline(always)] fn from_f32(x: f32) -> Self { x }
|
||||
#[inline(always)] fn to_f32(self) -> f32 { self }
|
||||
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
|
||||
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl Linear for f64 {
|
||||
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
|
||||
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
|
||||
#[inline(always)] fn from_f64(x: f64) -> Self { x }
|
||||
#[inline(always)] fn to_f64(self) -> f64 { self }
|
||||
}
|
||||
|
||||
pub trait Channel: Copy + Debug {
|
||||
fn to_linear<Out: Linear>(self) -> Out;
|
||||
fn from_linear<In: Linear>(linear: In) -> Self;
|
||||
}
|
||||
|
||||
pub trait LinearChannel: Channel {
|
||||
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
|
||||
Out::from_linear(self.to_linear::<f64>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Linear + Debug + Copy> Channel for T {
|
||||
#[inline(always)]
|
||||
fn to_linear<Out: Linear>(self) -> Out {
|
||||
Out::from_f64(self.to_f64())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_linear<In: Linear>(linear: In) -> Self {
|
||||
Self::from_f64(linear.to_f64())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Linear + Debug + Copy> LinearChannel for T {}
|
||||
|
||||
use num_derive::*;
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
|
||||
pub struct SRGBGammaFloat(f32);
|
||||
|
||||
impl Channel for SRGBGammaFloat {
|
||||
#[inline(always)]
|
||||
fn to_linear<Out: Linear>(self) -> Out {
|
||||
let x = self.0;
|
||||
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn from_linear<In: Linear>(linear: In) -> Self {
|
||||
let x = linear.to_f32();
|
||||
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
|
||||
}
|
||||
}
|
||||
pub trait RGBPrimaries {
|
||||
const RED: DVec2;
|
||||
const GREEN: DVec2;
|
||||
const BLUE: DVec2;
|
||||
const WHITE: DVec2;
|
||||
}
|
||||
pub trait Rec709Primaries {}
|
||||
impl<T: Rec709Primaries> RGBPrimaries for T {
|
||||
const RED: DVec2 = DVec2::new(0.64, 0.33);
|
||||
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
|
||||
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
|
||||
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
|
||||
}
|
||||
|
||||
pub trait SRGB: Rec709Primaries {}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
pub trait Serde {}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
|
||||
#[cfg(not(feature = "serde"))]
|
||||
impl<T> Serde for T {}
|
||||
|
||||
// TODO: Come up with a better name for this trait
|
||||
pub trait Pixel: Clone + Pod + Zeroable + Default {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
bytemuck::bytes_of(self).to_vec()
|
||||
}
|
||||
// TODO: use u8 for Color
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
|
||||
}
|
||||
|
||||
fn byte_size() -> usize {
|
||||
core::mem::size_of::<Self>()
|
||||
}
|
||||
}
|
||||
pub trait RGB: Pixel {
|
||||
type ColorChannel: Channel;
|
||||
|
||||
fn red(&self) -> Self::ColorChannel;
|
||||
fn r(&self) -> Self::ColorChannel {
|
||||
self.red()
|
||||
}
|
||||
fn green(&self) -> Self::ColorChannel;
|
||||
fn g(&self) -> Self::ColorChannel {
|
||||
self.green()
|
||||
}
|
||||
fn blue(&self) -> Self::ColorChannel;
|
||||
fn b(&self) -> Self::ColorChannel {
|
||||
self.blue()
|
||||
}
|
||||
}
|
||||
pub trait RGBMut: RGB {
|
||||
fn set_red(&mut self, red: Self::ColorChannel);
|
||||
fn set_green(&mut self, green: Self::ColorChannel);
|
||||
fn set_blue(&mut self, blue: Self::ColorChannel);
|
||||
}
|
||||
|
||||
pub trait AssociatedAlpha: RGB + Alpha {
|
||||
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
|
||||
}
|
||||
|
||||
pub trait UnassociatedAlpha: RGB + Alpha {
|
||||
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
|
||||
}
|
||||
|
||||
pub trait Alpha {
|
||||
type AlphaChannel: LinearChannel;
|
||||
const TRANSPARENT: Self;
|
||||
fn alpha(&self) -> Self::AlphaChannel;
|
||||
fn a(&self) -> Self::AlphaChannel {
|
||||
self.alpha()
|
||||
}
|
||||
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
|
||||
}
|
||||
pub trait AlphaMut: Alpha {
|
||||
fn set_alpha(&mut self, value: Self::AlphaChannel);
|
||||
}
|
||||
|
||||
pub trait Depth {
|
||||
type DepthChannel: Channel;
|
||||
fn depth(&self) -> Self::DepthChannel;
|
||||
fn d(&self) -> Self::DepthChannel {
|
||||
self.depth()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtraChannels<const NUM: usize> {
|
||||
type ChannelType: Channel;
|
||||
fn extra_channels(&self) -> [Self::ChannelType; NUM];
|
||||
}
|
||||
|
||||
pub trait Luminance {
|
||||
type LuminanceChannel: LinearChannel;
|
||||
fn luminance(&self) -> Self::LuminanceChannel;
|
||||
fn l(&self) -> Self::LuminanceChannel {
|
||||
self.luminance()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LuminanceMut: Luminance {
|
||||
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
|
||||
}
|
||||
|
||||
// TODO: We might rename this to Raster at some point
|
||||
pub trait Sample {
|
||||
type Pixel: Pixel;
|
||||
// TODO: Add an area parameter
|
||||
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
|
||||
}
|
||||
|
||||
impl<T: Sample> Sample for &T {
|
||||
type Pixel = T::Pixel;
|
||||
|
||||
#[inline(always)]
|
||||
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
|
||||
(**self).sample(pos, area)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Bitmap {
|
||||
type Pixel: Pixel;
|
||||
fn width(&self) -> u32;
|
||||
@@ -286,112 +85,3 @@ impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
|
||||
(*self).get_pixel_mut(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::image::Image;
|
||||
pub mod image;
|
||||
|
||||
trait SetBlendMode {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode);
|
||||
}
|
||||
|
||||
impl SetBlendMode for VectorDataTable {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for GraphicGroupTable {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for RasterDataTable<CPU> {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait SetClip {
|
||||
fn set_clip(&mut self, clip: bool);
|
||||
}
|
||||
|
||||
impl SetClip for VectorDataTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for GraphicGroupTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for RasterDataTable<CPU> {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blend_mode<T: SetBlendMode>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn opacity<T: MultiplyAlpha>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
#[default(100.)] fill: Percentage,
|
||||
#[default(false)] clip: bool,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value.multiply_fill(fill / 100.);
|
||||
value.set_clip(clip);
|
||||
value
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::GraphicElement;
|
||||
use crate::blending::BlendMode;
|
||||
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
|
||||
use crate::raster::curve::{Curve, ValueMapperNode};
|
||||
use crate::raster::image::Image;
|
||||
use crate::raster::{Channel, Color, Pixel};
|
||||
use crate::raster_types::{CPU, Raster, RasterDataTable};
|
||||
use crate::registry::types::{Angle, Percentage, SignedPercentage};
|
||||
use crate::vector::VectorDataTable;
|
||||
use crate::vector::style::GradientStops;
|
||||
use crate::{Ctx, Node};
|
||||
use crate::{GraphicElement, GraphicGroupTable};
|
||||
use core::cmp::Ordering;
|
||||
use core::fmt::Debug;
|
||||
use dyn_any::DynAny;
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Debug;
|
||||
|
||||
// TODO: Implement the following:
|
||||
// Color Balance
|
||||
@@ -32,8 +29,7 @@ use spirv_std::num_traits::float::Float;
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum LuminanceCalculation {
|
||||
#[default]
|
||||
@@ -45,218 +41,6 @@ pub enum LuminanceCalculation {
|
||||
MaximumChannels,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
|
||||
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
|
||||
pub enum BlendMode {
|
||||
// Basic group
|
||||
#[default]
|
||||
Normal,
|
||||
|
||||
// Darken group
|
||||
Darken,
|
||||
Multiply,
|
||||
ColorBurn,
|
||||
LinearBurn,
|
||||
DarkerColor,
|
||||
|
||||
// Lighten group
|
||||
Lighten,
|
||||
Screen,
|
||||
ColorDodge,
|
||||
LinearDodge,
|
||||
LighterColor,
|
||||
|
||||
// Contrast group
|
||||
Overlay,
|
||||
SoftLight,
|
||||
HardLight,
|
||||
VividLight,
|
||||
LinearLight,
|
||||
PinLight,
|
||||
HardMix,
|
||||
|
||||
// Inversion group
|
||||
Difference,
|
||||
Exclusion,
|
||||
Subtract,
|
||||
Divide,
|
||||
|
||||
// Component group
|
||||
Hue,
|
||||
Saturation,
|
||||
Color,
|
||||
Luminosity,
|
||||
|
||||
// Other stuff
|
||||
Erase,
|
||||
Restore,
|
||||
MultiplyAlpha,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// All standard blend modes ordered by group.
|
||||
pub fn list() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion, Subtract, Divide],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
/// The subset of [`BlendMode::list()`] that is supported by SVG.
|
||||
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn index_in_list(&self) -> Option<usize> {
|
||||
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
|
||||
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => Some("normal"),
|
||||
// Darken group
|
||||
BlendMode::Darken => Some("darken"),
|
||||
BlendMode::Multiply => Some("multiply"),
|
||||
BlendMode::ColorBurn => Some("color-burn"),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => Some("lighten"),
|
||||
BlendMode::Screen => Some("screen"),
|
||||
BlendMode::ColorDodge => Some("color-dodge"),
|
||||
// Contrast group
|
||||
BlendMode::Overlay => Some("overlay"),
|
||||
BlendMode::SoftLight => Some("soft-light"),
|
||||
BlendMode::HardLight => Some("hard-light"),
|
||||
// Inversion group
|
||||
BlendMode::Difference => Some("difference"),
|
||||
BlendMode::Exclusion => Some("exclusion"),
|
||||
// Component group
|
||||
BlendMode::Hue => Some("hue"),
|
||||
BlendMode::Saturation => Some("saturation"),
|
||||
BlendMode::Color => Some("color"),
|
||||
BlendMode::Luminosity => Some("luminosity"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the blend mode CSS style declaration.
|
||||
pub fn render(&self) -> String {
|
||||
format!(
|
||||
r#" mix-blend-mode: {};"#,
|
||||
self.to_svg_style_name().unwrap_or_else(|| {
|
||||
warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BlendMode {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => write!(f, "Normal"),
|
||||
// Darken group
|
||||
BlendMode::Darken => write!(f, "Darken"),
|
||||
BlendMode::Multiply => write!(f, "Multiply"),
|
||||
BlendMode::ColorBurn => write!(f, "Color Burn"),
|
||||
BlendMode::LinearBurn => write!(f, "Linear Burn"),
|
||||
BlendMode::DarkerColor => write!(f, "Darker Color"),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => write!(f, "Lighten"),
|
||||
BlendMode::Screen => write!(f, "Screen"),
|
||||
BlendMode::ColorDodge => write!(f, "Color Dodge"),
|
||||
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
|
||||
BlendMode::LighterColor => write!(f, "Lighter Color"),
|
||||
// Contrast group
|
||||
BlendMode::Overlay => write!(f, "Overlay"),
|
||||
BlendMode::SoftLight => write!(f, "Soft Light"),
|
||||
BlendMode::HardLight => write!(f, "Hard Light"),
|
||||
BlendMode::VividLight => write!(f, "Vivid Light"),
|
||||
BlendMode::LinearLight => write!(f, "Linear Light"),
|
||||
BlendMode::PinLight => write!(f, "Pin Light"),
|
||||
BlendMode::HardMix => write!(f, "Hard Mix"),
|
||||
// Inversion group
|
||||
BlendMode::Difference => write!(f, "Difference"),
|
||||
BlendMode::Exclusion => write!(f, "Exclusion"),
|
||||
BlendMode::Subtract => write!(f, "Subtract"),
|
||||
BlendMode::Divide => write!(f, "Divide"),
|
||||
// Component group
|
||||
BlendMode::Hue => write!(f, "Hue"),
|
||||
BlendMode::Saturation => write!(f, "Saturation"),
|
||||
BlendMode::Color => write!(f, "Color"),
|
||||
BlendMode::Luminosity => write!(f, "Luminosity"),
|
||||
// Other utility blend modes (hidden from the normal list)
|
||||
BlendMode::Erase => write!(f, "Erase"),
|
||||
BlendMode::Restore => write!(f, "Restore"),
|
||||
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
impl From<BlendMode> for vello::peniko::Mix {
|
||||
fn from(val: BlendMode) -> Self {
|
||||
match val {
|
||||
// Normal group
|
||||
BlendMode::Normal => vello::peniko::Mix::Normal,
|
||||
// Darken group
|
||||
BlendMode::Darken => vello::peniko::Mix::Darken,
|
||||
BlendMode::Multiply => vello::peniko::Mix::Multiply,
|
||||
BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn,
|
||||
// Lighten group
|
||||
BlendMode::Lighten => vello::peniko::Mix::Lighten,
|
||||
BlendMode::Screen => vello::peniko::Mix::Screen,
|
||||
BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge,
|
||||
// Contrast group
|
||||
BlendMode::Overlay => vello::peniko::Mix::Overlay,
|
||||
BlendMode::SoftLight => vello::peniko::Mix::SoftLight,
|
||||
BlendMode::HardLight => vello::peniko::Mix::HardLight,
|
||||
// Inversion group
|
||||
BlendMode::Difference => vello::peniko::Mix::Difference,
|
||||
BlendMode::Exclusion => vello::peniko::Mix::Exclusion,
|
||||
// Component group
|
||||
BlendMode::Hue => vello::peniko::Mix::Hue,
|
||||
BlendMode::Saturation => vello::peniko::Mix::Saturation,
|
||||
BlendMode::Color => vello::peniko::Mix::Color,
|
||||
BlendMode::Luminosity => vello::peniko::Mix::Luminosity,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Adjustment"))]
|
||||
fn luminance<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
@@ -281,7 +65,7 @@ fn luminance<T: Adjust<Color>>(
|
||||
input
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster"))]
|
||||
#[node_macro::node(category("Raster: Channels"))]
|
||||
fn extract_channel<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
@@ -304,7 +88,7 @@ fn extract_channel<T: Adjust<Color>>(
|
||||
input
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster"))]
|
||||
#[node_macro::node(category("Raster: Channels"))]
|
||||
fn make_opaque<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
@@ -346,7 +130,7 @@ fn brightness_contrast<T: Adjust<Color>>(
|
||||
let brightness = brightness as f32 / 255.;
|
||||
|
||||
let contrast = contrast as f32 / 100.;
|
||||
let contrast = if contrast > 0. { (contrast * core::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
|
||||
let contrast = if contrast > 0. { (contrast * std::f32::consts::FRAC_PI_2 - 0.01).tan() } else { contrast };
|
||||
|
||||
let offset = brightness * contrast + brightness - contrast / 2.;
|
||||
|
||||
@@ -368,13 +152,13 @@ fn brightness_contrast<T: Adjust<Color>>(
|
||||
y: [0., 130. + brightness * 51., 233. + brightness * 10., 255.].map(|x| x / 255.),
|
||||
};
|
||||
let brightness_curve_solutions = brightness_curve_points.solve();
|
||||
let mut brightness_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
|
||||
let mut brightness_lut: [f32; WINDOW_SIZE] = std::array::from_fn(|i| {
|
||||
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
|
||||
brightness_curve_points.interpolate(x, &brightness_curve_solutions)
|
||||
});
|
||||
// Special handling for when brightness is negative
|
||||
if brightness_is_negative {
|
||||
brightness_lut = core::array::from_fn(|i| {
|
||||
brightness_lut = std::array::from_fn(|i| {
|
||||
let mut x = i;
|
||||
while x > 1 && brightness_lut[x] > i as f32 / WINDOW_SIZE as f32 {
|
||||
x -= 1;
|
||||
@@ -393,7 +177,7 @@ fn brightness_contrast<T: Adjust<Color>>(
|
||||
y: [0., 64. - contrast * 30., 192. + contrast * 30., 255.].map(|x| x / 255.),
|
||||
};
|
||||
let contrast_curve_solutions = contrast_curve_points.solve();
|
||||
let contrast_lut: [f32; WINDOW_SIZE] = core::array::from_fn(|i| {
|
||||
let contrast_lut: [f32; WINDOW_SIZE] = std::array::from_fn(|i| {
|
||||
let x = i as f32 / (WINDOW_SIZE as f32 - 1.);
|
||||
contrast_curve_points.interpolate(x, &contrast_curve_solutions)
|
||||
});
|
||||
@@ -919,8 +703,7 @@ async fn vibrance<T: Adjust<Color>>(
|
||||
}
|
||||
|
||||
/// Color Channel
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Radio)]
|
||||
pub enum RedGreenBlue {
|
||||
#[default]
|
||||
@@ -930,8 +713,7 @@ pub enum RedGreenBlue {
|
||||
}
|
||||
|
||||
/// Color Channel
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Radio)]
|
||||
pub enum RedGreenBlueAlpha {
|
||||
#[default]
|
||||
@@ -942,8 +724,7 @@ pub enum RedGreenBlueAlpha {
|
||||
}
|
||||
|
||||
/// Style of noise pattern
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum NoiseType {
|
||||
#[default]
|
||||
@@ -958,8 +739,7 @@ pub enum NoiseType {
|
||||
WhiteNoise,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
/// Style of layered levels of the noise pattern
|
||||
pub enum FractalType {
|
||||
#[default]
|
||||
@@ -975,8 +755,7 @@ pub enum FractalType {
|
||||
}
|
||||
|
||||
/// Distance function used by the cellular noise
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CellularDistanceFunction {
|
||||
#[default]
|
||||
Euclidean,
|
||||
@@ -986,8 +765,7 @@ pub enum CellularDistanceFunction {
|
||||
Hybrid,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CellularReturnType {
|
||||
CellValue,
|
||||
#[default]
|
||||
@@ -1006,8 +784,7 @@ pub enum CellularReturnType {
|
||||
}
|
||||
|
||||
/// Type of domain warp
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum DomainWarpType {
|
||||
#[default]
|
||||
@@ -1033,6 +810,7 @@ async fn channel_mixer<T: Adjust<Color>>(
|
||||
mut image: T,
|
||||
|
||||
monochrome: bool,
|
||||
|
||||
#[default(40.)]
|
||||
#[name("Red")]
|
||||
monochrome_r: f64,
|
||||
@@ -1116,8 +894,7 @@ async fn channel_mixer<T: Adjust<Color>>(
|
||||
image
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
#[widget(Radio)]
|
||||
pub enum RelativeAbsolute {
|
||||
#[default]
|
||||
@@ -1126,8 +903,7 @@ pub enum RelativeAbsolute {
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub enum SelectiveColorChoice {
|
||||
#[default]
|
||||
Reds,
|
||||
@@ -1158,43 +934,54 @@ async fn selective_color<T: Adjust<Color>>(
|
||||
GradientStops,
|
||||
)]
|
||||
mut image: T,
|
||||
|
||||
mode: RelativeAbsolute,
|
||||
|
||||
#[name("(Reds) Cyan")] r_c: f64,
|
||||
#[name("(Reds) Magenta")] r_m: f64,
|
||||
#[name("(Reds) Yellow")] r_y: f64,
|
||||
#[name("(Reds) Black")] r_k: f64,
|
||||
|
||||
#[name("(Yellows) Cyan")] y_c: f64,
|
||||
#[name("(Yellows) Magenta")] y_m: f64,
|
||||
#[name("(Yellows) Yellow")] y_y: f64,
|
||||
#[name("(Yellows) Black")] y_k: f64,
|
||||
|
||||
#[name("(Greens) Cyan")] g_c: f64,
|
||||
#[name("(Greens) Magenta")] g_m: f64,
|
||||
#[name("(Greens) Yellow")] g_y: f64,
|
||||
#[name("(Greens) Black")] g_k: f64,
|
||||
|
||||
#[name("(Cyans) Cyan")] c_c: f64,
|
||||
#[name("(Cyans) Magenta")] c_m: f64,
|
||||
#[name("(Cyans) Yellow")] c_y: f64,
|
||||
#[name("(Cyans) Black")] c_k: f64,
|
||||
|
||||
#[name("(Blues) Cyan")] b_c: f64,
|
||||
#[name("(Blues) Magenta")] b_m: f64,
|
||||
#[name("(Blues) Yellow")] b_y: f64,
|
||||
#[name("(Blues) Black")] b_k: f64,
|
||||
|
||||
#[name("(Magentas) Cyan")] m_c: f64,
|
||||
#[name("(Magentas) Magenta")] m_m: f64,
|
||||
#[name("(Magentas) Yellow")] m_y: f64,
|
||||
#[name("(Magentas) Black")] m_k: f64,
|
||||
|
||||
#[name("(Whites) Cyan")] w_c: f64,
|
||||
#[name("(Whites) Magenta")] w_m: f64,
|
||||
#[name("(Whites) Yellow")] w_y: f64,
|
||||
#[name("(Whites) Black")] w_k: f64,
|
||||
|
||||
#[name("(Neutrals) Cyan")] n_c: f64,
|
||||
#[name("(Neutrals) Magenta")] n_m: f64,
|
||||
#[name("(Neutrals) Yellow")] n_y: f64,
|
||||
#[name("(Neutrals) Black")] n_k: f64,
|
||||
|
||||
#[name("(Blacks) Cyan")] k_c: f64,
|
||||
#[name("(Blacks) Magenta")] k_m: f64,
|
||||
#[name("(Blacks) Yellow")] k_y: f64,
|
||||
#[name("(Blacks) Black")] k_k: f64,
|
||||
|
||||
_colors: SelectiveColorChoice,
|
||||
) -> T {
|
||||
image.adjust(|color| {
|
||||
@@ -1274,70 +1061,6 @@ async fn selective_color<T: Adjust<Color>>(
|
||||
image
|
||||
}
|
||||
|
||||
pub(super) trait MultiplyAlpha {
|
||||
fn multiply_alpha(&mut self, factor: f64);
|
||||
}
|
||||
|
||||
impl MultiplyAlpha for Color {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for VectorDataTable {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for GraphicGroupTable {
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyAlpha for RasterDataTable<CPU>
|
||||
where
|
||||
GraphicElement: From<Image<Color>>,
|
||||
{
|
||||
fn multiply_alpha(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.opacity *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait MultiplyFill {
|
||||
fn multiply_fill(&mut self, factor: f64);
|
||||
}
|
||||
impl MultiplyFill for Color {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for VectorDataTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for GraphicGroupTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for RasterDataTable<CPU> {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aims for interoperable compatibility with:
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
|
||||
//
|
||||
@@ -1419,7 +1142,7 @@ fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve
|
||||
anchor: [1.; 2],
|
||||
handles: [curve.last_handle, [0.; 2]],
|
||||
};
|
||||
for sample in curve.manipulator_groups.iter().chain(core::iter::once(&end)) {
|
||||
for sample in curve.manipulator_groups.iter().chain(std::iter::once(&end)) {
|
||||
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
|
||||
|
||||
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
|
||||
@@ -1501,22 +1224,10 @@ fn color_overlay<T: Adjust<Color>>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::raster::adjustments::BlendMode;
|
||||
use crate::Color;
|
||||
use crate::blending::BlendMode;
|
||||
use crate::raster::image::Image;
|
||||
use crate::raster_types::{Raster, RasterDataTable};
|
||||
use crate::{Color, Node};
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FutureWrapperNode<T: Clone>(T);
|
||||
|
||||
impl<'i, T: 'i + Clone + Send> Node<'i, ()> for FutureWrapperNode<T> {
|
||||
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
|
||||
fn eval(&'i self, _input: ()) -> Self::Output {
|
||||
let value = self.0.clone();
|
||||
Box::pin(async move { value })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn color_overlay_multiply() {
|
||||
|
||||
@@ -3,28 +3,27 @@ use crate::raster_types::CPU;
|
||||
use crate::raster_types::Raster;
|
||||
use crate::vector::brush_stroke::BrushStroke;
|
||||
use crate::vector::brush_stroke::BrushStyle;
|
||||
use core::hash::Hash;
|
||||
use dyn_any::DynAny;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct BrushCacheImpl {
|
||||
// The full previous input that was cached.
|
||||
prev_input: Vec<BrushStroke>,
|
||||
|
||||
// The strokes that have been fully processed and blended into the background.
|
||||
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
|
||||
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
|
||||
background: Instance<Raster<CPU>>,
|
||||
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
|
||||
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
|
||||
blended_image: Instance<Raster<CPU>>,
|
||||
#[cfg_attr(feature = "serde", serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance"))]
|
||||
#[serde(deserialize_with = "crate::graphene_core::raster::image::migrate_image_frame_instance")]
|
||||
last_stroke_texture: Instance<Raster<CPU>>,
|
||||
|
||||
// A cache for brush textures.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
#[serde(skip)]
|
||||
brush_texture_cache: HashMap<BrushStyle, Raster<CPU>>,
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ impl BrushCacheImpl {
|
||||
|
||||
// Take our previous blended image (and invalidate the cache).
|
||||
// Since we're about to replace our cache anyway, this saves a clone.
|
||||
background = core::mem::take(&mut self.blended_image);
|
||||
background = std::mem::take(&mut self.blended_image);
|
||||
|
||||
// Check if the first non-blended stroke is an extension of the last one.
|
||||
let mut first_stroke_texture = Instance {
|
||||
@@ -70,7 +69,7 @@ impl BrushCacheImpl {
|
||||
let new_points = strokes[0].compute_blit_points();
|
||||
let is_point_prefix = new_points.get(..prev_points.len()) == Some(&prev_points);
|
||||
if same_style && is_point_prefix {
|
||||
first_stroke_texture = core::mem::take(&mut self.last_stroke_texture);
|
||||
first_stroke_texture = std::mem::take(&mut self.last_stroke_texture);
|
||||
first_stroke_point_skip = prev_points.len();
|
||||
}
|
||||
}
|
||||
@@ -93,7 +92,7 @@ impl BrushCacheImpl {
|
||||
|
||||
impl Hash for BrushCacheImpl {
|
||||
// Zero hash.
|
||||
fn hash<H: core::hash::Hasher>(&self, _state: &mut H) {}
|
||||
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -104,8 +103,7 @@ pub struct BrushPlan {
|
||||
pub first_stroke_point_skip: usize,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, DynAny)]
|
||||
#[derive(Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BrushCache {
|
||||
inner: Arc<Mutex<BrushCacheImpl>>,
|
||||
proto: bool,
|
||||
@@ -151,7 +149,7 @@ impl PartialEq for BrushCache {
|
||||
}
|
||||
|
||||
impl Hash for BrushCache {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.lock().unwrap().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use super::{Channel, Linear, LuminanceMut};
|
||||
use crate::Node;
|
||||
use core::ops::{Add, Mul, Sub};
|
||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Curve {
|
||||
#[serde(rename = "manipulatorGroups")]
|
||||
pub manipulator_groups: Vec<CurveManipulatorGroup>,
|
||||
@@ -31,8 +30,7 @@ impl std::hash::Hash for Curve {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CurveManipulatorGroup {
|
||||
pub anchor: [f32; 2],
|
||||
pub handles: [[f32; 2]; 2],
|
||||
@@ -95,12 +93,7 @@ impl CubicSplines {
|
||||
// Gaussian elimination: forward elimination
|
||||
for row in 0..4 {
|
||||
let pivot_row_index = (row..4)
|
||||
.max_by(|&a_row, &b_row| {
|
||||
augmented_matrix[a_row][row]
|
||||
.abs()
|
||||
.partial_cmp(&augmented_matrix[b_row][row].abs())
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
})
|
||||
.max_by(|&a_row, &b_row| augmented_matrix[a_row][row].abs().partial_cmp(&augmented_matrix[b_row][row].abs()).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.unwrap();
|
||||
|
||||
// Swap the current row with the row that has the largest pivot element
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
use crate::{
|
||||
AlphaBlending,
|
||||
instances::{Instance, Instances},
|
||||
raster_types::Raster,
|
||||
};
|
||||
|
||||
use super::Color;
|
||||
use super::discrete_srgb::float_to_srgb_u8;
|
||||
use alloc::vec::Vec;
|
||||
use crate::AlphaBlending;
|
||||
use crate::color::float_to_srgb_u8;
|
||||
use crate::instances::{Instance, Instances};
|
||||
use crate::raster_types::Raster;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::vec::Vec;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod base64_serde {
|
||||
//! Basic wrapper for [`serde`] to perform [`base64`] encoding
|
||||
|
||||
@@ -40,23 +36,22 @@ mod base64_serde {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Default, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, PartialEq, Default, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Image<P: Pixel> {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
#[cfg_attr(feature = "serde", serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64"))]
|
||||
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
|
||||
pub data: Vec<P>,
|
||||
/// Optional: Stores a base64 string representation of the image which can be used to speed up the conversion
|
||||
/// to an svg string. This is used as a cache in order to not have to encode the data on every graph evaluation.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
#[serde(skip)]
|
||||
pub base64_string: Option<String>,
|
||||
// TODO: Add an `origin` field to store where in the local space the image is anchored.
|
||||
// TODO: Currently it is always anchored at the top left corner at (0, 0). The bottom right corner of the new origin field would correspond to (1, 1).
|
||||
}
|
||||
|
||||
impl<P: Pixel + Debug> Debug for Image<P> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let length = self.data.len();
|
||||
f.debug_struct("Image")
|
||||
.field("width", &self.width)
|
||||
@@ -203,7 +198,7 @@ where
|
||||
|
||||
impl<P: Pixel> IntoIterator for Image<P> {
|
||||
type Item = P;
|
||||
type IntoIter = alloc::vec::IntoIter<P>;
|
||||
type IntoIter = std::vec::IntoIter<P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.into_iter()
|
||||
}
|
||||
@@ -233,8 +228,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GraphicElement {
|
||||
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
|
||||
GraphicGroup(GraphicGroupTable),
|
||||
@@ -243,8 +237,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
|
||||
RasterFrame(RasterFrame),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ImageFrame<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
}
|
||||
@@ -272,8 +265,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldImageFrame<P: Pixel> {
|
||||
image: Image<P>,
|
||||
transform: DAffine2,
|
||||
@@ -299,7 +291,15 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
|
||||
*image_frame_table.instance_mut_iter().next().unwrap().alpha_blending = alpha_blending;
|
||||
image_frame_table
|
||||
}
|
||||
FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(image_frame.instance_ref_iter().next().unwrap().instance.image.clone())),
|
||||
FormatVersions::ImageFrame(image_frame) => RasterDataTable::new(Raster::new_cpu(
|
||||
image_frame
|
||||
.instance_ref_iter()
|
||||
.next()
|
||||
.unwrap_or(Instances::new(ImageFrame::default()).instance_ref_iter().next().unwrap())
|
||||
.instance
|
||||
.image
|
||||
.clone(),
|
||||
)),
|
||||
FormatVersions::ImageFrameTable(image_frame_table) => RasterDataTable::new(Raster::new_cpu(image_frame_table.instance_ref_iter().next().unwrap().instance.clone())),
|
||||
FormatVersions::RasterDataTable(raster_data_table) => raster_data_table,
|
||||
})
|
||||
@@ -329,8 +329,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GraphicElement {
|
||||
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
|
||||
GraphicGroup(GraphicGroupTable),
|
||||
@@ -339,8 +338,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
|
||||
RasterFrame(RasterFrame),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ImageFrame<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
}
|
||||
@@ -368,8 +366,7 @@ pub fn migrate_image_frame_instance<'de, D: serde::Deserializer<'de>>(deserializ
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldImageFrame<P: Pixel> {
|
||||
image: Image<P>,
|
||||
transform: DAffine2,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::Color;
|
||||
use crate::bounds::BoundingBox;
|
||||
use crate::instances::Instances;
|
||||
use crate::math::quad::Quad;
|
||||
use crate::raster::Image;
|
||||
use core::ops::Deref;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
#[cfg(feature = "wgpu")]
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -11,18 +14,18 @@ pub struct CPU;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)]
|
||||
pub struct GPU;
|
||||
|
||||
trait Storage {}
|
||||
trait Storage: 'static {}
|
||||
impl Storage for CPU {}
|
||||
impl Storage for GPU {}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||
#[allow(private_bounds)]
|
||||
pub struct Raster<T: 'static + Storage> {
|
||||
pub struct Raster<T: Storage> {
|
||||
data: RasterStorage,
|
||||
storage: T,
|
||||
}
|
||||
|
||||
unsafe impl<T: 'static + Storage> dyn_any::StaticType for Raster<T> {
|
||||
unsafe impl<T: Storage> dyn_any::StaticType for Raster<T> {
|
||||
type Static = Raster<T>;
|
||||
}
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
@@ -54,6 +57,10 @@ impl Raster<CPU> {
|
||||
let RasterStorage::Cpu(cpu) = self.data else { unreachable!() };
|
||||
cpu
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let data = self.data();
|
||||
data.height == 0 || data.width == 0
|
||||
}
|
||||
}
|
||||
impl Default for Raster<CPU> {
|
||||
fn default() -> Self {
|
||||
@@ -90,6 +97,10 @@ impl Raster<GPU> {
|
||||
let RasterStorage::Gpu(gpu) = &self.data else { unreachable!() };
|
||||
gpu.clone()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let data = self.data();
|
||||
data.width() == 0 || data.height() == 0
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl Deref for Raster<GPU> {
|
||||
@@ -100,3 +111,28 @@ impl Deref for Raster<GPU> {
|
||||
}
|
||||
}
|
||||
pub type RasterDataTable<Storage> = Instances<Raster<Storage>>;
|
||||
|
||||
// TODO: Make this not dupliated
|
||||
impl BoundingBox for RasterDataTable<CPU> {
|
||||
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
|
||||
.flat_map(|instance| {
|
||||
let transform = transform * *instance.transform;
|
||||
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for RasterDataTable<GPU> {
|
||||
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.filter(|instance| !instance.instance.is_empty()) // Eliminate empty images
|
||||
.flat_map(|instance| {
|
||||
let transform = transform * *instance.transform;
|
||||
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub struct FieldMetadata {
|
||||
pub unit: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
|
||||
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
|
||||
const WIDGET_HINT: ChoiceWidgetHint;
|
||||
const DESCRIPTION: Option<&'static str>;
|
||||
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
|
||||
@@ -108,10 +108,10 @@ pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::ne
|
||||
pub static NODE_METADATA: LazyLock<Mutex<HashMap<String, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n + Send>>;
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
|
||||
pub type LocalFuture<'n, T> = Pin<Box<dyn core::future::Future<Output = T> + 'n>>;
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn std::future::Future<Output = T> + 'n>>;
|
||||
pub type LocalFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n>>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -169,8 +169,8 @@ impl Drop for NodeContainer {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for NodeContainer {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl std::fmt::Debug for NodeContainer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NodeContainer").finish()
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ impl NodeContainer {
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
unsafe fn dealloc_unchecked(&mut self) {
|
||||
unsafe {
|
||||
std::mem::drop(Box::from_raw(self.node as *mut TypeErasedNode));
|
||||
drop(Box::from_raw(self.node as *mut TypeErasedNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ where
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
@@ -227,8 +227,8 @@ impl<I, O> DowncastBothNode<I, O> {
|
||||
pub const fn new(node: SharedNodeContainer) -> Self {
|
||||
Self {
|
||||
node,
|
||||
_i: core::marker::PhantomData,
|
||||
_o: core::marker::PhantomData,
|
||||
_i: PhantomData,
|
||||
_o: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ where
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
@@ -271,14 +271,14 @@ pub struct DynAnyNode<I, O, Node> {
|
||||
|
||||
impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode<I, O, N>
|
||||
where
|
||||
I: 'input + dyn_any::StaticType + WasmNotSend,
|
||||
O: 'input + dyn_any::StaticType + WasmNotSend,
|
||||
I: 'input + StaticType + WasmNotSend,
|
||||
O: 'input + StaticType + WasmNotSend,
|
||||
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
|
||||
{
|
||||
type Output = FutureAny<'input>;
|
||||
#[inline]
|
||||
fn eval(&'input self, input: Any<'input>) -> Self::Output {
|
||||
let node_name = core::any::type_name::<N>();
|
||||
let node_name = std::any::type_name::<N>();
|
||||
let output = |input| {
|
||||
let result = self.node.eval(input);
|
||||
async move { Box::new(result.await) as Any<'input> }
|
||||
@@ -293,21 +293,21 @@ where
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
impl<'input, I, O, N> DynAnyNode<I, O, N>
|
||||
where
|
||||
I: 'input + dyn_any::StaticType,
|
||||
O: 'input + dyn_any::StaticType,
|
||||
I: 'input + StaticType,
|
||||
O: 'input + StaticType,
|
||||
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
|
||||
{
|
||||
pub const fn new(node: N) -> Self {
|
||||
Self {
|
||||
node,
|
||||
_i: core::marker::PhantomData,
|
||||
_o: core::marker::PhantomData,
|
||||
_i: PhantomData,
|
||||
_o: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::Node;
|
||||
use core::marker::PhantomData;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// This is how we can generically define composition of two nodes.
|
||||
/// This is done generically as shown: <https://files.keavon.com/-/SurprisedGaseousAnhinga/capture.png>
|
||||
@@ -58,10 +58,10 @@ pub struct AsyncComposeNode<First, Second, I> {
|
||||
impl<'i, Input: 'static, First, Second> Node<'i, Input> for AsyncComposeNode<First, Second, Input>
|
||||
where
|
||||
First: Node<'i, Input>,
|
||||
First::Output: core::future::Future,
|
||||
Second: Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
|
||||
First::Output: Future,
|
||||
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
|
||||
{
|
||||
type Output = core::pin::Pin<Box<dyn core::future::Future<Output = <Second as Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output>>::Output> + 'i>>;
|
||||
type Output = std::pin::Pin<Box<dyn Future<Output = <Second as Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output>>::Output> + 'i>>;
|
||||
fn eval(&'i self, input: Input) -> Self::Output {
|
||||
Box::pin(async move {
|
||||
let arg = self.first.eval(input).await;
|
||||
@@ -73,8 +73,8 @@ where
|
||||
impl<'i, First, Second, Input: 'i> AsyncComposeNode<First, Second, Input>
|
||||
where
|
||||
First: Node<'i, Input>,
|
||||
First::Output: core::future::Future,
|
||||
Second: Node<'i, <<First as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
|
||||
First::Output: Future,
|
||||
Second: Node<'i, <<First as Node<'i, Input>>::Output as Future>::Output> + 'i,
|
||||
{
|
||||
pub const fn new(first: First, second: Second) -> Self {
|
||||
AsyncComposeNode::<First, Second, Input> { first, second, phantom: PhantomData }
|
||||
@@ -97,8 +97,8 @@ pub trait AndThen<'i, Input: 'i>: Sized {
|
||||
fn and_then<Second>(self, second: Second) -> AsyncComposeNode<Self, Second, Input>
|
||||
where
|
||||
Self: Node<'i, Input>,
|
||||
Self::Output: core::future::Future,
|
||||
Second: Node<'i, <<Self as Node<'i, Input>>::Output as core::future::Future>::Output> + 'i,
|
||||
Self::Output: Future,
|
||||
Second: Node<'i, <<Self as Node<'i, Input>>::Output as Future>::Output> + 'i,
|
||||
{
|
||||
AsyncComposeNode::new(self, second)
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ impl FontCache {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for FontCache {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.preview_urls.len().hash(state);
|
||||
self.preview_urls.iter().for_each(|(font, url)| {
|
||||
font.hash(state);
|
||||
|
||||
@@ -208,7 +208,7 @@ pub fn bounding_box(str: &str, buzz_face: Option<&rustybuzz::Face>, typesetting:
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn load_face(data: &[u8]) -> rustybuzz::Face {
|
||||
pub fn load_face(data: &[u8]) -> rustybuzz::Face<'_> {
|
||||
rustybuzz::Face::from_slice(data, 0).expect("Loading font failed")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::instances::Instances;
|
||||
use crate::raster::bbox::AxisAlignedBbox;
|
||||
use crate::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use crate::vector::VectorDataTable;
|
||||
use crate::{Artboard, CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
|
||||
use crate::Artboard;
|
||||
use crate::math::bbox::AxisAlignedBbox;
|
||||
pub use crate::vector::ReferencePoint;
|
||||
use core::f64;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
|
||||
@@ -67,8 +65,7 @@ impl TransformMut for Footprint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RenderQuality {
|
||||
/// Low quality, fast rendering
|
||||
Preview,
|
||||
@@ -81,8 +78,7 @@ pub enum RenderQuality {
|
||||
/// Render at full quality
|
||||
Full,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Footprint {
|
||||
/// Inverse of the transform which will be applied to the node output during the rendering process
|
||||
pub transform: DAffine2,
|
||||
@@ -136,8 +132,8 @@ impl From<()> for Footprint {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Footprint {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for Footprint {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state));
|
||||
self.resolution.hash(state)
|
||||
}
|
||||
@@ -154,186 +150,3 @@ impl<T: TransformMut> ApplyTransform for T {
|
||||
impl ApplyTransform for () {
|
||||
fn apply_transform(&mut self, &_modification: &DAffine2) {}
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn transform<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
|
||||
translate: DVec2,
|
||||
rotate: f64,
|
||||
scale: DVec2,
|
||||
shear: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> Instances<T> {
|
||||
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
|
||||
|
||||
let footprint = ctx.try_footprint().copied();
|
||||
|
||||
let mut ctx = OwnedContextImpl::from(ctx);
|
||||
if let Some(mut footprint) = footprint {
|
||||
footprint.apply_transform(&matrix);
|
||||
ctx = ctx.with_footprint(footprint);
|
||||
}
|
||||
|
||||
let mut transform_target = transform_target.eval(ctx.into_context()).await;
|
||||
|
||||
for data_transform in transform_target.instance_mut_iter() {
|
||||
*data_transform.transform = matrix * *data_transform.transform;
|
||||
}
|
||||
|
||||
transform_target
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
fn replace_transform<Data, TransformInput: Transform>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
|
||||
#[implementations(DAffine2)] transform: TransformInput,
|
||||
) -> Instances<Data> {
|
||||
for data_transform in data.instance_mut_iter() {
|
||||
*data_transform.transform = transform.transform();
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Debug"))]
|
||||
async fn boundless_footprint<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
Context -> String,
|
||||
Context -> f64,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = T>,
|
||||
) -> T {
|
||||
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
|
||||
|
||||
transform_target.eval(ctx.into_context()).await
|
||||
}
|
||||
#[node_macro::node(category("Debug"))]
|
||||
async fn freeze_real_time<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
Context -> String,
|
||||
Context -> f64,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = T>,
|
||||
) -> T {
|
||||
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
|
||||
|
||||
transform_target.eval(ctx.into_context()).await
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ReferencePoint {
|
||||
#[default]
|
||||
None,
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl ReferencePoint {
|
||||
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
|
||||
let size = bounding_box.size();
|
||||
let offset = match self {
|
||||
ReferencePoint::None => return None,
|
||||
ReferencePoint::TopLeft => DVec2::ZERO,
|
||||
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
|
||||
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
|
||||
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
|
||||
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
|
||||
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
|
||||
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
|
||||
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
|
||||
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
|
||||
};
|
||||
Some(bounding_box.start + offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ReferencePoint {
|
||||
fn from(input: &str) -> Self {
|
||||
match input {
|
||||
"None" => ReferencePoint::None,
|
||||
"TopLeft" => ReferencePoint::TopLeft,
|
||||
"TopCenter" => ReferencePoint::TopCenter,
|
||||
"TopRight" => ReferencePoint::TopRight,
|
||||
"CenterLeft" => ReferencePoint::CenterLeft,
|
||||
"Center" => ReferencePoint::Center,
|
||||
"CenterRight" => ReferencePoint::CenterRight,
|
||||
"BottomLeft" => ReferencePoint::BottomLeft,
|
||||
"BottomCenter" => ReferencePoint::BottomCenter,
|
||||
"BottomRight" => ReferencePoint::BottomRight,
|
||||
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReferencePoint> for Option<DVec2> {
|
||||
fn from(input: ReferencePoint) -> Self {
|
||||
match input {
|
||||
ReferencePoint::None => None,
|
||||
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
|
||||
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
|
||||
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
|
||||
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
|
||||
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
|
||||
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
|
||||
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
|
||||
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
|
||||
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DVec2> for ReferencePoint {
|
||||
fn from(input: DVec2) -> Self {
|
||||
const TOLERANCE: f64 = 1e-5_f64;
|
||||
if input.y.abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::TopLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopRight;
|
||||
}
|
||||
} else if (input.y - 0.5).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::Center;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterRight;
|
||||
}
|
||||
} else if (input.y - 1.).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomRight;
|
||||
}
|
||||
}
|
||||
ReferencePoint::None
|
||||
}
|
||||
}
|
||||
|
||||
89
node-graph/gcore/src/transform_nodes.rs
Normal file
89
node-graph/gcore/src/transform_nodes.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::instances::Instances;
|
||||
use crate::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use crate::transform::{ApplyTransform, Footprint, Transform};
|
||||
use crate::vector::VectorDataTable;
|
||||
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
|
||||
use core::f64;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn transform<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
|
||||
translate: DVec2,
|
||||
rotate: f64,
|
||||
scale: DVec2,
|
||||
shear: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> Instances<T> {
|
||||
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
|
||||
|
||||
let footprint = ctx.try_footprint().copied();
|
||||
|
||||
let mut ctx = OwnedContextImpl::from(ctx);
|
||||
if let Some(mut footprint) = footprint {
|
||||
footprint.apply_transform(&matrix);
|
||||
ctx = ctx.with_footprint(footprint);
|
||||
}
|
||||
|
||||
let mut transform_target = transform_target.eval(ctx.into_context()).await;
|
||||
|
||||
for data_transform in transform_target.instance_mut_iter() {
|
||||
*data_transform.transform = matrix * *data_transform.transform;
|
||||
}
|
||||
|
||||
transform_target
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
fn replace_transform<Data, TransformInput: Transform>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
|
||||
#[implementations(DAffine2)] transform: TransformInput,
|
||||
) -> Instances<Data> {
|
||||
for data_transform in data.instance_mut_iter() {
|
||||
*data_transform.transform = transform.transform();
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Debug"))]
|
||||
async fn boundless_footprint<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
Context -> String,
|
||||
Context -> f64,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = T>,
|
||||
) -> T {
|
||||
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
|
||||
|
||||
transform_target.eval(ctx.into_context()).await
|
||||
}
|
||||
#[node_macro::node(category("Debug"))]
|
||||
async fn freeze_real_time<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
Context -> GraphicGroupTable,
|
||||
Context -> RasterDataTable<CPU>,
|
||||
Context -> RasterDataTable<GPU>,
|
||||
Context -> String,
|
||||
Context -> f64,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = T>,
|
||||
) -> T {
|
||||
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
|
||||
|
||||
transform_target.eval(ctx.into_context()).await
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use core::any::TypeId;
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
@@ -6,20 +6,20 @@ pub use std::borrow::Cow;
|
||||
macro_rules! concrete {
|
||||
($type:ty) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(core::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(core::any::type_name::<$type>()),
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
|
||||
alias: None,
|
||||
size: core::mem::size_of::<$type>(),
|
||||
align: core::mem::align_of::<$type>(),
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
($type:ty, $name:ty) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(core::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(core::any::type_name::<$type>()),
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
|
||||
alias: Some($crate::Cow::Borrowed(stringify!($name))),
|
||||
size: core::mem::size_of::<$type>(),
|
||||
align: core::mem::align_of::<$type>(),
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -28,11 +28,11 @@ macro_rules! concrete {
|
||||
macro_rules! concrete_with_name {
|
||||
($type:ty, $name:expr_2021) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(core::any::TypeId::of::<$type>()),
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed($name),
|
||||
alias: None,
|
||||
size: core::mem::size_of::<$type>(),
|
||||
align: core::mem::align_of::<$type>(),
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -114,8 +114,8 @@ impl NodeIOTypes {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for NodeIOTypes {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
impl std::fmt::Debug for NodeIOTypes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"node({}) → {}",
|
||||
[&self.call_argument].into_iter().chain(&self.inputs).map(|input| input.to_string()).collect::<Vec<_>>().join(", "),
|
||||
@@ -124,8 +124,7 @@ impl core::fmt::Debug for NodeIOTypes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProtoNodeIdentifier {
|
||||
pub name: Cow<'static, str>,
|
||||
}
|
||||
@@ -141,7 +140,7 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
|
||||
let name = String::deserialize(deserializer)?;
|
||||
let name = match name.as_str() {
|
||||
"f32" => "f64".to_string(),
|
||||
"graphene_core::transform::Footprint" => "core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
|
||||
"graphene_core::transform::Footprint" => "std::option::Option<std::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
|
||||
"graphene_core::graphic_element::GraphicGroup" => "graphene_core::instances::Instances<graphene_core::graphic_element::GraphicGroup>".to_string(),
|
||||
"graphene_core::vector::vector_data::VectorData" => "graphene_core::instances::Instances<graphene_core::vector::vector_data::VectorData>".to_string(),
|
||||
"graphene_core::raster::image::ImageFrame<Color>"
|
||||
@@ -156,10 +155,9 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
|
||||
Ok(Cow::Owned(name))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Eq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TypeDescriptor {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
#[serde(skip)]
|
||||
#[specta(skip)]
|
||||
pub id: Option<TypeId>,
|
||||
#[serde(deserialize_with = "migrate_type_descriptor_names")]
|
||||
@@ -172,8 +170,8 @@ pub struct TypeDescriptor {
|
||||
pub align: usize,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for TypeDescriptor {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for TypeDescriptor {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
@@ -192,8 +190,7 @@ impl PartialEq for TypeDescriptor {
|
||||
}
|
||||
|
||||
/// Graph runtime type information used for type inference.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Type {
|
||||
/// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type.
|
||||
Generic(Cow<'static, str>),
|
||||
@@ -264,10 +261,10 @@ impl Type {
|
||||
pub fn new<T: dyn_any::StaticType + Sized>() -> Self {
|
||||
Self::Concrete(TypeDescriptor {
|
||||
id: Some(TypeId::of::<T::Static>()),
|
||||
name: Cow::Borrowed(core::any::type_name::<T::Static>()),
|
||||
name: Cow::Borrowed(std::any::type_name::<T::Static>()),
|
||||
alias: None,
|
||||
size: core::mem::size_of::<T>(),
|
||||
align: core::mem::align_of::<T>(),
|
||||
size: size_of::<T>(),
|
||||
align: align_of::<T>(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,8 +315,8 @@ fn format_type(ty: &str) -> String {
|
||||
.join("<")
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Type {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
impl std::fmt::Debug for Type {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let result = match self {
|
||||
Self::Generic(name) => name.to_string(),
|
||||
#[cfg(feature = "type_id_logging")]
|
||||
|
||||
@@ -43,9 +43,9 @@ mod u64_string {
|
||||
}
|
||||
|
||||
mod uuid_generation {
|
||||
use core::cell::Cell;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_chacha::rand_core::{RngCore, SeedableRng};
|
||||
use std::cell::Cell;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static RNG: Mutex<Option<ChaCha20Rng>> = Mutex::new(None);
|
||||
@@ -79,8 +79,8 @@ impl NodeId {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for NodeId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
impl std::fmt::Display for NodeId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::Node;
|
||||
use core::cell::{Cell, RefCell, RefMut};
|
||||
use core::marker::PhantomData;
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct IntNode<const N: u32>;
|
||||
|
||||
@@ -1,8 +1,57 @@
|
||||
use super::poisson_disk::poisson_disk_sample;
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
|
||||
|
||||
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
|
||||
// Divide the segment.
|
||||
let first_segment = segment.subsegment(0.0..t);
|
||||
let second_segment = segment.subsegment(t..1.);
|
||||
|
||||
let mut first_bezpath = BezPath::new();
|
||||
let mut second_bezpath = BezPath::new();
|
||||
|
||||
// Append the segments up to the subdividing segment from original bezpath to first bezpath.
|
||||
for segment in bezpath.segments().take(segment_index) {
|
||||
if first_bezpath.elements().is_empty() {
|
||||
first_bezpath.move_to(segment.start());
|
||||
}
|
||||
first_bezpath.push(segment.as_path_el());
|
||||
}
|
||||
|
||||
// Append the first segment of the subdivided segment.
|
||||
if first_bezpath.elements().is_empty() {
|
||||
first_bezpath.move_to(first_segment.start());
|
||||
}
|
||||
first_bezpath.push(first_segment.as_path_el());
|
||||
|
||||
// Append the second segment of the subdivided segment in the second bezpath.
|
||||
if second_bezpath.elements().is_empty() {
|
||||
second_bezpath.move_to(second_segment.start());
|
||||
}
|
||||
second_bezpath.push(second_segment.as_path_el());
|
||||
|
||||
// Append the segments after the subdividing segment from original bezpath to second bezpath.
|
||||
for segment in bezpath.segments().skip(segment_index + 1) {
|
||||
if second_bezpath.elements().is_empty() {
|
||||
second_bezpath.move_to(segment.start());
|
||||
}
|
||||
second_bezpath.push(segment.as_path_el());
|
||||
}
|
||||
|
||||
Some((first_bezpath, second_bezpath))
|
||||
}
|
||||
|
||||
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
||||
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
||||
@@ -18,7 +67,15 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_l
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, segments_length: &[f64]) -> Option<BezPath> {
|
||||
pub fn sample_polyline_on_bezpath(
|
||||
bezpath: BezPath,
|
||||
point_spacing_type: PointSpacingType,
|
||||
amount: f64,
|
||||
start_offset: f64,
|
||||
stop_offset: f64,
|
||||
adaptive_spacing: bool,
|
||||
segments_length: &[f64],
|
||||
) -> Option<BezPath> {
|
||||
let mut sample_bezpath = BezPath::new();
|
||||
|
||||
let was_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath));
|
||||
@@ -29,22 +86,33 @@ pub fn sample_points_on_bezpath(bezpath: BezPath, spacing: f64, start_offset: f6
|
||||
// Adjust the usable length by subtracting start and stop offsets.
|
||||
let mut used_length = total_length - start_offset - stop_offset;
|
||||
|
||||
// Sanity check that the usable length is positive.
|
||||
if used_length <= 0. {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine the number of points to generate along the path.
|
||||
let sample_count = if adaptive_spacing {
|
||||
// Calculate point count to evenly distribute points while covering the entire path.
|
||||
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
|
||||
(used_length / spacing).round()
|
||||
} else {
|
||||
// Calculate point count based on exact spacing, which may not cover the entire path.
|
||||
const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;
|
||||
|
||||
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
|
||||
let count = (used_length / spacing + f64::EPSILON).floor();
|
||||
used_length -= used_length % spacing;
|
||||
count
|
||||
// Determine the number of points to generate along the path.
|
||||
let sample_count = match point_spacing_type {
|
||||
PointSpacingType::Separation => {
|
||||
let spacing = amount.min(used_length - f64::EPSILON);
|
||||
|
||||
if adaptive_spacing {
|
||||
// Calculate point count to evenly distribute points while covering the entire path.
|
||||
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
|
||||
(used_length / spacing).round().min(SAFETY_MAX_COUNT)
|
||||
} else {
|
||||
// Calculate point count based on exact spacing, which may not cover the entire path.
|
||||
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
|
||||
let count = (used_length / spacing + f64::EPSILON).floor().min(SAFETY_MAX_COUNT);
|
||||
if count != SAFETY_MAX_COUNT {
|
||||
used_length -= used_length % spacing;
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
PointSpacingType::Quantity => (amount - 1.).floor().clamp(1., SAFETY_MAX_COUNT),
|
||||
};
|
||||
|
||||
// Skip if there are no points to generate.
|
||||
@@ -108,7 +176,7 @@ pub fn t_value_to_parametric(bezpath: &BezPath, t: f64, euclidian: bool, segment
|
||||
|
||||
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
|
||||
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
|
||||
pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accuracy: f64) -> f64 {
|
||||
pub fn eval_pathseg_euclidean(path_segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
|
||||
let mut low_t = 0.;
|
||||
let mut mid_t = 0.5;
|
||||
let mut high_t = 1.;
|
||||
@@ -139,7 +207,7 @@ pub fn eval_pathseg_euclidean(path_segment: kurbo::PathSeg, distance: f64, accur
|
||||
/// Converts from a bezpath (composed of multiple segments) to a point along a certain segment represented.
|
||||
/// The returned tuple represents the segment index and the `t` value along that segment.
|
||||
/// Both the input global `t` value and the output `t` value are in euclidean space, meaning there is a constant rate of change along the arc length.
|
||||
fn global_euclidean_to_local_euclidean(bezpath: &kurbo::BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
|
||||
fn global_euclidean_to_local_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
|
||||
let mut accumulator = 0.;
|
||||
for (index, length) in lengths.iter().enumerate() {
|
||||
let length_ratio = length / total_length;
|
||||
@@ -158,7 +226,7 @@ enum BezPathTValue {
|
||||
|
||||
/// Convert a [BezPathTValue] to a parametric `(segment_index, t)` tuple.
|
||||
/// - Asserts that `t` values contained within the `SubpathTValue` argument lie in the range [0, 1].
|
||||
fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
|
||||
fn bezpath_t_value_to_parametric(bezpath: &BezPath, t: BezPathTValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
|
||||
let segment_count = bezpath.segments().count();
|
||||
assert!(segment_count >= 1);
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Node;
|
||||
use crate::ops::ExtractXyNode;
|
||||
use crate::extract_xy::{ExtractXyNode, XY};
|
||||
use crate::vector::VectorData;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DVec2;
|
||||
@@ -109,7 +109,7 @@ mod test {
|
||||
pub struct FutureWrapperNode<T: Clone>(T);
|
||||
|
||||
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
|
||||
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
|
||||
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
let value = self.0.clone();
|
||||
Box::pin(async move { value })
|
||||
@@ -121,7 +121,7 @@ mod test {
|
||||
let owned = OwnedContextImpl::default().into_context();
|
||||
let rect = crate::vector::generator_nodes::RectangleNode::new(
|
||||
FutureWrapperNode(()),
|
||||
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(crate::ops::XY::Y)),
|
||||
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(XY::Y)),
|
||||
FutureWrapperNode(2_f64),
|
||||
FutureWrapperNode(false),
|
||||
FutureWrapperNode(0_f64),
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
use crate::vector::{PointId, VectorData, VectorDataIndex};
|
||||
use glam::DVec2;
|
||||
use crate::vector::{PointDomain, PointId, SegmentDomain, VectorData, VectorDataIndex};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use petgraph::prelude::UnGraphMap;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
impl VectorData {
|
||||
pub trait MergeByDistanceExt {
|
||||
/// Collapse all points with edges shorter than the specified distance
|
||||
pub(crate) fn merge_by_distance(&mut self, distance: f64) {
|
||||
fn merge_by_distance_topological(&mut self, distance: f64);
|
||||
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
|
||||
}
|
||||
|
||||
impl MergeByDistanceExt for VectorData {
|
||||
fn merge_by_distance_topological(&mut self, distance: f64) {
|
||||
// Treat self as an undirected graph
|
||||
let indices = VectorDataIndex::build_from(self);
|
||||
|
||||
// TODO: We lose information on the winding order by using an undirected graph. Switch to a directed graph and fix the algorithm to handle that.
|
||||
// Graph containing only short edges, referencing the data graph
|
||||
let mut short_edges = UnGraphMap::new();
|
||||
|
||||
for segment_id in self.segment_ids().iter().copied() {
|
||||
let length = indices.segment_chord_length(segment_id);
|
||||
if length < distance {
|
||||
@@ -92,4 +99,116 @@ impl VectorData {
|
||||
self.segment_domain.retain(|id| !segments_to_delete.contains(id), usize::MAX);
|
||||
self.point_domain.retain(&mut self.segment_domain, |id| !points_to_delete.contains(id));
|
||||
}
|
||||
|
||||
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) {
|
||||
let point_count = self.point_domain.positions().len();
|
||||
|
||||
// Find min x and y for grid cell normalization
|
||||
let mut min_x = f64::MAX;
|
||||
let mut min_y = f64::MAX;
|
||||
|
||||
// Calculate mins without collecting all positions
|
||||
for &pos in self.point_domain.positions() {
|
||||
let transformed_pos = transform.transform_point2(pos);
|
||||
min_x = min_x.min(transformed_pos.x);
|
||||
min_y = min_y.min(transformed_pos.y);
|
||||
}
|
||||
|
||||
// Create a spatial grid with cell size of 'distance'
|
||||
use std::collections::HashMap;
|
||||
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
|
||||
|
||||
// Add points to grid cells without collecting all positions first
|
||||
for i in 0..point_count {
|
||||
let pos = transform.transform_point2(self.point_domain.positions()[i]);
|
||||
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
|
||||
|
||||
grid.entry((grid_x, grid_y)).or_default().push(i);
|
||||
}
|
||||
|
||||
// Create point index mapping for merged points
|
||||
let mut point_index_map = vec![None; point_count];
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
// Process each point
|
||||
for i in 0..point_count {
|
||||
// Skip points that have already been processed
|
||||
if point_index_map[i].is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pos_i = transform.transform_point2(self.point_domain.positions()[i]);
|
||||
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
|
||||
|
||||
let mut group = vec![i];
|
||||
|
||||
// Check only neighboring cells (3x3 grid around current cell)
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
let neighbor_cell = (grid_x + dx, grid_y + dy);
|
||||
|
||||
if let Some(indices) = grid.get(&neighbor_cell) {
|
||||
for &j in indices {
|
||||
if j > i && point_index_map[j].is_none() {
|
||||
let pos_j = transform.transform_point2(self.point_domain.positions()[j]);
|
||||
if pos_i.distance(pos_j) <= distance {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create merged point - calculate positions as needed
|
||||
let merged_position = group
|
||||
.iter()
|
||||
.map(|&idx| transform.transform_point2(self.point_domain.positions()[idx]))
|
||||
.fold(DVec2::ZERO, |sum, pos| sum + pos)
|
||||
/ group.len() as f64;
|
||||
|
||||
let merged_position = transform.inverse().transform_point2(merged_position);
|
||||
let merged_index = merged_positions.len();
|
||||
|
||||
merged_positions.push(merged_position);
|
||||
merged_indices.push(self.point_domain.ids()[group[0]]);
|
||||
|
||||
// Update mapping for all points in the group
|
||||
for &idx in &group {
|
||||
point_index_map[idx] = Some(merged_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new point domain with merged points
|
||||
let mut new_point_domain = PointDomain::new();
|
||||
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
|
||||
new_point_domain.push(idx, pos);
|
||||
}
|
||||
|
||||
// Update segment domain
|
||||
let mut new_segment_domain = SegmentDomain::new();
|
||||
for segment_idx in 0..self.segment_domain.ids().len() {
|
||||
let id = self.segment_domain.ids()[segment_idx];
|
||||
let start = self.segment_domain.start_point()[segment_idx];
|
||||
let end = self.segment_domain.end_point()[segment_idx];
|
||||
let handles = self.segment_domain.handles()[segment_idx];
|
||||
let stroke = self.segment_domain.stroke()[segment_idx];
|
||||
|
||||
// Get new indices for start and end points
|
||||
let new_start = point_index_map[start].unwrap();
|
||||
let new_end = point_index_map[end].unwrap();
|
||||
|
||||
// Skip segments where start and end points were merged
|
||||
if new_start != new_end {
|
||||
new_segment_domain.push(id, new_start, new_end, handles, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new vector data
|
||||
self.point_domain = new_point_domain;
|
||||
self.segment_domain = new_segment_domain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod bezpath_algorithms;
|
||||
mod instance;
|
||||
mod merge_by_distance;
|
||||
pub mod instance;
|
||||
pub mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
mod poisson_disk;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
|
||||
@@ -8,7 +8,7 @@ const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
|
||||
fn segment_to_bezier(seg: kurbo::PathSeg) -> bezier_rs::Bezier {
|
||||
fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier {
|
||||
match seg {
|
||||
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
|
||||
kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use core::f64;
|
||||
use glam::DVec2;
|
||||
use std::collections::HashMap;
|
||||
use std::f64;
|
||||
|
||||
const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn poisson_disk_sample(
|
||||
// - Dividing into an integer number of cells across the dartboard domain, to avoid wastefully throwing darts beyond the width and height of the dartboard domain
|
||||
// - Being fully covered by the radius around a dart thrown anywhere in its area, where the worst-case is a corner which has a distance of sqrt(2) to the opposite corner
|
||||
let greater_dimension = width.max(height);
|
||||
let base_level_grid_size = greater_dimension / (greater_dimension * std::f64::consts::SQRT_2 / (diameter / 2.)).ceil();
|
||||
let base_level_grid_size = greater_dimension / (greater_dimension * f64::consts::SQRT_2 / (diameter / 2.)).ceil();
|
||||
|
||||
// Initialize the problem by including all base-level squares in the active list since they're all part of the yet-to-be-targetted dartboard domain
|
||||
let base_level = ActiveListLevel::new_filled(base_level_grid_size, offset, width, height, &point_in_shape_checker, &line_intersect_shape_checker);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::Color;
|
||||
use crate::math::bbox::AxisAlignedBbox;
|
||||
use crate::raster::BlendMode;
|
||||
use crate::raster::bbox::AxisAlignedBbox;
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// The style of a brush.
|
||||
#[derive(Clone, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BrushStyle {
|
||||
pub color: Color,
|
||||
pub diameter: f64,
|
||||
@@ -55,8 +54,7 @@ impl PartialEq for BrushStyle {
|
||||
}
|
||||
|
||||
/// A single sample of brush parameters across the brush stroke.
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BrushInputSample {
|
||||
// The position of the sample in layer space, in pixels.
|
||||
// The origin of layer space is not specified.
|
||||
@@ -72,8 +70,7 @@ impl Hash for BrushInputSample {
|
||||
}
|
||||
|
||||
/// The parameters for a single stroke brush.
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BrushStroke {
|
||||
pub style: BrushStyle,
|
||||
pub trace: Vec<BrushInputSample>,
|
||||
|
||||
162
node-graph/gcore/src/vector/click_target.rs
Normal file
162
node-graph/gcore/src/vector/click_target.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use crate::math::math_ext::QuadExt;
|
||||
use crate::math::quad::Quad;
|
||||
use crate::vector::PointId;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FreePoint {
|
||||
pub id: PointId,
|
||||
pub position: DVec2,
|
||||
}
|
||||
|
||||
impl FreePoint {
|
||||
pub fn new(id: PointId, position: DVec2) -> Self {
|
||||
Self { id, position }
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, transform: DAffine2) {
|
||||
self.position = transform.transform_point2(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ClickTargetType {
|
||||
Subpath(Subpath<PointId>),
|
||||
FreePoint(FreePoint),
|
||||
}
|
||||
|
||||
/// Represents a clickable target for the layer
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClickTarget {
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
Self {
|
||||
target_type: ClickTargetType::Subpath(subpath),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_free_point(point: FreePoint) -> Self {
|
||||
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
|
||||
let stroke_width = 10.;
|
||||
let bounding_box = Some([
|
||||
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
]);
|
||||
|
||||
Self {
|
||||
target_type: ClickTargetType::FreePoint(point),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_type(&self) -> &ClickTargetType {
|
||||
&self.target_type
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box
|
||||
}
|
||||
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref mut subpath) => {
|
||||
subpath.apply_transform(affine_transform);
|
||||
}
|
||||
ClickTargetType::FreePoint(ref mut point) => {
|
||||
point.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
self.update_bbox();
|
||||
}
|
||||
|
||||
fn update_bbox(&mut self) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
self.bounding_box = subpath.bounding_box();
|
||||
}
|
||||
ClickTargetType::FreePoint(ref point) => {
|
||||
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the path
|
||||
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
|
||||
// Check if the matrix is not invertible
|
||||
let mut layer_transform = layer_transform;
|
||||
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
}
|
||||
|
||||
let inverse = layer_transform.inverse();
|
||||
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
|
||||
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
|
||||
if subpath.iter().any(outline_intersects) {
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
|
||||
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (accounting for stroke size)
|
||||
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
|
||||
let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)];
|
||||
let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y;
|
||||
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
|
||||
if !self
|
||||
.bounding_box
|
||||
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allows for selecting lines
|
||||
// TODO: actual intersection of stroke
|
||||
let inflated_quad = Quad::from_box(target_bounds);
|
||||
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (not accounting for stroke size)
|
||||
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
|
||||
// Check if the point is within the bounding box
|
||||
if self
|
||||
.bounding_box
|
||||
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
|
||||
{
|
||||
// Check if the point is within the shape
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
|
||||
ClickTargetType::FreePoint(free_point) => free_point.position == point,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,12 +119,12 @@ fn star<T: AsU64>(
|
||||
#[hard_min(2.)]
|
||||
#[implementations(u32, u64, f64)]
|
||||
sides: T,
|
||||
#[default(50)] radius: f64,
|
||||
#[default(25)] inner_radius: f64,
|
||||
#[default(50)] radius_1: f64,
|
||||
#[default(25)] radius_2: f64,
|
||||
) -> VectorDataTable {
|
||||
let points = sides.as_u64();
|
||||
let diameter: f64 = radius * 2.;
|
||||
let inner_diameter = inner_radius * 2.;
|
||||
let diameter: f64 = radius_1 * 2.;
|
||||
let inner_diameter = radius_2 * 2.;
|
||||
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
|
||||
}
|
||||
@@ -158,8 +158,8 @@ fn grid<T: GridSpacing>(
|
||||
#[implementations(f64, DVec2)]
|
||||
spacing: T,
|
||||
#[default(30., 30.)] angles: DVec2,
|
||||
#[default(10)] rows: u32,
|
||||
#[default(10)] columns: u32,
|
||||
#[default(10)] rows: u32,
|
||||
) -> VectorDataTable {
|
||||
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
|
||||
let (angle_a, angle_b) = angles.into();
|
||||
|
||||
@@ -13,31 +13,6 @@ pub enum CentroidType {
|
||||
Length,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum BooleanOperation {
|
||||
#[default]
|
||||
#[icon("BooleanUnion")]
|
||||
Union,
|
||||
#[icon("BooleanSubtractFront")]
|
||||
SubtractFront,
|
||||
#[icon("BooleanSubtractBack")]
|
||||
SubtractBack,
|
||||
#[icon("BooleanIntersect")]
|
||||
Intersect,
|
||||
#[icon("BooleanDifference")]
|
||||
Difference,
|
||||
}
|
||||
|
||||
pub trait AsU32 {
|
||||
fn as_u32(&self) -> u32;
|
||||
}
|
||||
impl AsU32 for u32 {
|
||||
fn as_u32(&self) -> u32 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsU64 {
|
||||
fn as_u64(&self) -> u64;
|
||||
}
|
||||
@@ -94,6 +69,26 @@ pub enum ArcType {
|
||||
PieSlice,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum MergeByDistanceAlgorithm {
|
||||
#[default]
|
||||
Spatial,
|
||||
Topological,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PointSpacingType {
|
||||
#[default]
|
||||
/// The desired spacing distance between points.
|
||||
Separation,
|
||||
/// The exact number of points to span the path.
|
||||
Quantity,
|
||||
}
|
||||
|
||||
pub fn point_to_dvec2(point: Point) -> DVec2 {
|
||||
DVec2 { x: point.x, y: point.y }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
mod algorithms;
|
||||
pub mod algorithms;
|
||||
pub mod brush_stroke;
|
||||
pub mod click_target;
|
||||
pub mod generator_nodes;
|
||||
pub mod misc;
|
||||
mod reference_point;
|
||||
pub mod style;
|
||||
mod vector_data;
|
||||
mod vector_nodes;
|
||||
|
||||
pub use bezier_rs;
|
||||
pub use reference_point::*;
|
||||
pub use style::PathStyle;
|
||||
pub use vector_data::*;
|
||||
pub use vector_nodes::*;
|
||||
|
||||
103
node-graph/gcore/src/vector/reference_point.rs
Normal file
103
node-graph/gcore/src/vector/reference_point.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::math::bbox::AxisAlignedBbox;
|
||||
use glam::DVec2;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ReferencePoint {
|
||||
#[default]
|
||||
None,
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl ReferencePoint {
|
||||
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
|
||||
let size = bounding_box.size();
|
||||
let offset = match self {
|
||||
ReferencePoint::None => return None,
|
||||
ReferencePoint::TopLeft => DVec2::ZERO,
|
||||
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
|
||||
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
|
||||
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
|
||||
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
|
||||
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
|
||||
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
|
||||
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
|
||||
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
|
||||
};
|
||||
Some(bounding_box.start + offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ReferencePoint {
|
||||
fn from(input: &str) -> Self {
|
||||
match input {
|
||||
"None" => ReferencePoint::None,
|
||||
"TopLeft" => ReferencePoint::TopLeft,
|
||||
"TopCenter" => ReferencePoint::TopCenter,
|
||||
"TopRight" => ReferencePoint::TopRight,
|
||||
"CenterLeft" => ReferencePoint::CenterLeft,
|
||||
"Center" => ReferencePoint::Center,
|
||||
"CenterRight" => ReferencePoint::CenterRight,
|
||||
"BottomLeft" => ReferencePoint::BottomLeft,
|
||||
"BottomCenter" => ReferencePoint::BottomCenter,
|
||||
"BottomRight" => ReferencePoint::BottomRight,
|
||||
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReferencePoint> for Option<DVec2> {
|
||||
fn from(input: ReferencePoint) -> Self {
|
||||
match input {
|
||||
ReferencePoint::None => None,
|
||||
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
|
||||
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
|
||||
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
|
||||
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
|
||||
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
|
||||
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
|
||||
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
|
||||
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
|
||||
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DVec2> for ReferencePoint {
|
||||
fn from(input: DVec2) -> Self {
|
||||
const TOLERANCE: f64 = 1e-5_f64;
|
||||
if input.y.abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::TopLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::TopRight;
|
||||
}
|
||||
} else if (input.y - 0.5).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::Center;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::CenterRight;
|
||||
}
|
||||
} else if (input.y - 1.).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return ReferencePoint::BottomRight;
|
||||
}
|
||||
}
|
||||
ReferencePoint::None
|
||||
}
|
||||
}
|
||||
@@ -1,312 +1,9 @@
|
||||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
use crate::Color;
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::renderer::{RenderParams, format_transform_matrix};
|
||||
pub use crate::gradient::*;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum GradientType {
|
||||
#[default]
|
||||
Linear,
|
||||
Radial,
|
||||
}
|
||||
|
||||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||
// TODO: Use linear not gamma colors
|
||||
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct GradientStops(Vec<(f64, Color)>);
|
||||
|
||||
impl std::hash::Hash for GradientStops {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.len().hash(state);
|
||||
self.0.iter().for_each(|(position, color)| {
|
||||
position.to_bits().hash(state);
|
||||
color.hash(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GradientStops {
|
||||
fn default() -> Self {
|
||||
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a GradientStops {
|
||||
type Item = &'a (f64, Color);
|
||||
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for GradientStops {
|
||||
type Output = (f64, Color);
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for GradientStops {
|
||||
type Target = Vec<(f64, Color)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for GradientStops {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops {
|
||||
pub fn new(stops: Vec<(f64, Color)>) -> Self {
|
||||
let mut stops = Self(stops);
|
||||
stops.sort();
|
||||
stops
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.0.is_empty() {
|
||||
return Color::BLACK;
|
||||
}
|
||||
|
||||
if t <= self.0[0].0 {
|
||||
return self.0[0].1;
|
||||
}
|
||||
if t >= self.0[self.0.len() - 1].0 {
|
||||
return self.0[self.0.len() - 1].1;
|
||||
}
|
||||
|
||||
for i in 0..self.0.len() - 1 {
|
||||
let (t1, c1) = self.0[i];
|
||||
let (t2, c2) = self.0[i + 1];
|
||||
if t >= t1 && t <= t2 {
|
||||
let normalized_t = (t - t1) / (t2 - t1);
|
||||
return c1.lerp(&c2, normalized_t as f32);
|
||||
}
|
||||
}
|
||||
|
||||
Color::BLACK
|
||||
}
|
||||
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
|
||||
}
|
||||
|
||||
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
|
||||
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient fill.
|
||||
///
|
||||
/// Contains the start and end points, along with the colors at varying points along the length.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct Gradient {
|
||||
pub stops: GradientStops,
|
||||
pub gradient_type: GradientType,
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Default for Gradient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stops: GradientStops::default(),
|
||||
gradient_type: GradientType::Linear,
|
||||
start: DVec2::new(0., 0.5),
|
||||
end: DVec2::new(1., 0.5),
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Gradient {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stops.0.len().hash(state);
|
||||
[].iter()
|
||||
.chain(self.start.to_array().iter())
|
||||
.chain(self.end.to_array().iter())
|
||||
.chain(self.transform.to_cols_array().iter())
|
||||
.chain(self.stops.0.iter().map(|(position, _)| position))
|
||||
.for_each(|x| x.to_bits().hash(state));
|
||||
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
|
||||
self.gradient_type.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Gradient {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
|
||||
Gradient {
|
||||
start,
|
||||
end,
|
||||
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
|
||||
transform,
|
||||
gradient_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let start = self.start + (other.start - self.start) * time;
|
||||
let end = self.end + (other.end - self.end) * time;
|
||||
let transform = self.transform;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.stops.0.iter())
|
||||
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
||||
let position = a_pos + (b_pos - a_pos) * time;
|
||||
let color = a_color.lerp(b_color, time as f32);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = GradientStops::new(stops);
|
||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||
|
||||
Self {
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
stops,
|
||||
gradient_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 {
|
||||
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
|
||||
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
stop.push_str("<stop");
|
||||
if *position != 0. {
|
||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||
}
|
||||
let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
stop.push_str(" />")
|
||||
}
|
||||
|
||||
let mod_gradient = if transformed_bound_transform.matrix2.determinant() != 0. {
|
||||
transformed_bound_transform.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
|
||||
};
|
||||
let mod_points = element_transform * stroke_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(self.start);
|
||||
let end = mod_points.transform_point2(self.end);
|
||||
|
||||
let gradient_id = crate::uuid::generate_uuid();
|
||||
|
||||
let matrix = format_transform_matrix(mod_gradient);
|
||||
let gradient_transform = if matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{}""#, matrix) };
|
||||
|
||||
match self.gradient_type {
|
||||
GradientType::Linear => {
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
|
||||
gradient_id, start.x, end.x, start.y, end.y, stop
|
||||
);
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
|
||||
gradient_id, start.x, start.y, radius, stop
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
gradient_id
|
||||
}
|
||||
|
||||
/// Insert a stop into the gradient, the index if successful
|
||||
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
|
||||
// Transform the start and end positions to the same coordinate space as the mouse.
|
||||
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
|
||||
|
||||
// Calculate the new position by finding the closest point on the line
|
||||
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
|
||||
|
||||
// Don't insert point past end of line
|
||||
if !(0. ..=1.).contains(&new_position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute the color of the inserted stop
|
||||
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
|
||||
// Lerp between the nearest colors if applicable
|
||||
(a, Some(b)) => a.lerp(
|
||||
&b,
|
||||
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
|
||||
),
|
||||
// Use the start or the end color if applicable
|
||||
(v, _) => v,
|
||||
};
|
||||
|
||||
// Compute the correct index to keep the positions in order
|
||||
let mut index = 0;
|
||||
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let new_color = get_color(index - 1, new_position);
|
||||
|
||||
// Insert the new stop
|
||||
self.stops.0.insert(index, (new_position, new_color));
|
||||
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
use glam::DAffine2;
|
||||
|
||||
/// Describes the fill of a layer.
|
||||
///
|
||||
@@ -380,24 +77,6 @@ impl Fill {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a gradient from the fill
|
||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||
match self {
|
||||
@@ -521,7 +200,7 @@ pub enum StrokeCap {
|
||||
}
|
||||
|
||||
impl StrokeCap {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeCap::Butt => "butt",
|
||||
StrokeCap::Round => "round",
|
||||
@@ -541,7 +220,7 @@ pub enum StrokeJoin {
|
||||
}
|
||||
|
||||
impl StrokeJoin {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeJoin::Bevel => "bevel",
|
||||
StrokeJoin::Miter => "miter",
|
||||
@@ -611,8 +290,8 @@ pub struct Stroke {
|
||||
pub paint_order: PaintOrder,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Stroke {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for Stroke {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.color.hash(state);
|
||||
self.weight.to_bits().hash(state);
|
||||
{
|
||||
@@ -711,60 +390,6 @@ impl Stroke {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self, aligned_strokes: bool, override_paint_order: bool, _render_params: &RenderParams) -> String {
|
||||
// Don't render a stroke at all if it would be invisible
|
||||
let Some(color) = self.color else { return String::new() };
|
||||
if !self.has_renderable_stroke() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Set to None if the value is the SVG default
|
||||
let weight = (self.weight != 1.).then_some(self.weight);
|
||||
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
|
||||
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
|
||||
let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap);
|
||||
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
|
||||
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
|
||||
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
|
||||
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || override_paint_order).then_some(PaintOrder::StrokeBelow);
|
||||
|
||||
// Render the needed stroke attributes
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(mut weight) = weight {
|
||||
if stroke_align.is_some() && aligned_strokes {
|
||||
weight *= 2.;
|
||||
}
|
||||
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
|
||||
}
|
||||
if let Some(dash_array) = dash_array {
|
||||
let _ = write!(&mut attributes, r#" stroke-dasharray="{}""#, dash_array);
|
||||
}
|
||||
if let Some(dash_offset) = dash_offset {
|
||||
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
|
||||
}
|
||||
if let Some(stroke_cap) = stroke_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name());
|
||||
}
|
||||
if let Some(stroke_join) = stroke_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name());
|
||||
}
|
||||
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, stroke_join_miter_limit);
|
||||
}
|
||||
// Add vector-effect attribute to make strokes non-scaling
|
||||
if self.non_scaling {
|
||||
let _ = write!(&mut attributes, r#" vector-effect="non-scaling-stroke""#);
|
||||
}
|
||||
if paint_order.is_some() {
|
||||
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
self.color = *color;
|
||||
|
||||
@@ -846,12 +471,12 @@ impl Default for Stroke {
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct PathStyle {
|
||||
stroke: Option<Stroke>,
|
||||
fill: Fill,
|
||||
pub stroke: Option<Stroke>,
|
||||
pub fill: Fill,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for PathStyle {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for PathStyle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stroke.hash(state);
|
||||
self.fill.hash(state);
|
||||
}
|
||||
@@ -1008,41 +633,6 @@ impl PathStyle {
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render(
|
||||
&self,
|
||||
svg_defs: &mut String,
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: [DVec2; 2],
|
||||
transformed_bounds: [DVec2; 2],
|
||||
aligned_strokes: bool,
|
||||
override_paint_order: bool,
|
||||
render_params: &RenderParams,
|
||||
) -> String {
|
||||
let view_mode = render_params.view_mode;
|
||||
match view_mode {
|
||||
ViewMode::Outline => {
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
|
||||
// Outline strokes should be non-scaling by default
|
||||
outline_stroke.non_scaling = true;
|
||||
let stroke_attribute = outline_stroke.render(aligned_strokes, override_paint_order, render_params);
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
_ => {
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let stroke_attribute = self
|
||||
.stroke
|
||||
.as_ref()
|
||||
.map(|stroke| stroke.render(aligned_strokes, override_paint_order, render_params))
|
||||
.unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents different ways of rendering an object
|
||||
|
||||
@@ -4,12 +4,16 @@ mod modification;
|
||||
|
||||
use super::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::bounds::BoundingBox;
|
||||
use crate::instances::Instances;
|
||||
use crate::renderer::{ClickTargetType, FreePoint};
|
||||
use crate::math::quad::Quad;
|
||||
use crate::transform::Transform;
|
||||
use crate::vector::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
pub use attributes::*;
|
||||
use bezier_rs::ManipulatorGroup;
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup};
|
||||
use core::borrow::Borrow;
|
||||
use core::hash::Hash;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
pub use indexed::VectorDataIndex;
|
||||
@@ -21,8 +25,7 @@ use std::collections::HashMap;
|
||||
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldVectorData {
|
||||
pub transform: DAffine2,
|
||||
pub alpha_blending: AlphaBlending,
|
||||
@@ -75,8 +78,7 @@ pub type VectorDataTable = Instances<VectorData>;
|
||||
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
|
||||
///
|
||||
/// Segments are connected if they share endpoints.
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VectorData {
|
||||
pub style: PathStyle,
|
||||
|
||||
@@ -105,8 +107,8 @@ impl Default for VectorData {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for VectorData {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl std::hash::Hash for VectorData {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.point_domain.hash(state);
|
||||
self.segment_domain.hash(state);
|
||||
self.region_domain.hash(state);
|
||||
@@ -188,11 +190,6 @@ impl VectorData {
|
||||
self.point_domain.push(id, point.position);
|
||||
}
|
||||
|
||||
/// Appends a Kurbo BezPath to the vector data.
|
||||
pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) {
|
||||
AppendBezpath::append_bezpath(self, bezpath);
|
||||
}
|
||||
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
@@ -337,6 +334,13 @@ impl VectorData {
|
||||
index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index]))
|
||||
}
|
||||
|
||||
/// Returns the number of linear segments connected to the given point.
|
||||
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
|
||||
self.segment_bezier_iter()
|
||||
.filter(|(_, bez, start, end)| ((*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear)))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get an array slice of all segment IDs.
|
||||
pub fn segment_ids(&self) -> &[SegmentId] {
|
||||
self.segment_domain.ids()
|
||||
@@ -414,7 +418,7 @@ impl VectorData {
|
||||
}
|
||||
|
||||
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId> {
|
||||
let pair = self.colinear_manipulators.iter().find(|pair| pair.iter().any(|&val| val == handle))?;
|
||||
let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?;
|
||||
let other = pair.iter().copied().find(|&val| val != handle)?;
|
||||
if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) {
|
||||
Some(other)
|
||||
@@ -494,9 +498,31 @@ impl VectorData {
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for VectorDataTable {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
self.instance_ref_iter()
|
||||
.flat_map(|instance| {
|
||||
if !include_stroke {
|
||||
return instance.instance.bounding_box_with_transform(transform * *instance.transform);
|
||||
}
|
||||
|
||||
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
|
||||
|
||||
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
|
||||
let scale = transform.decompose_scale();
|
||||
|
||||
// We use the full line width here to account for different styles of stroke caps
|
||||
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
|
||||
|
||||
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ManipulatorPointId {
|
||||
/// A control anchor - the start or end point of a bézier.
|
||||
Anchor(PointId),
|
||||
@@ -583,8 +609,7 @@ impl ManipulatorPointId {
|
||||
}
|
||||
|
||||
/// The type of handle found on a bézier curve.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum HandleType {
|
||||
/// The first handle on a cubic bézier or the only handle on a quadratic bézier.
|
||||
Primary,
|
||||
@@ -593,8 +618,7 @@ pub enum HandleType {
|
||||
}
|
||||
|
||||
/// Represents a primary or end handle found in a particular segment.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HandleId {
|
||||
pub ty: HandleType,
|
||||
pub segment: SegmentId,
|
||||
@@ -642,16 +666,6 @@ impl HandleId {
|
||||
handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX)
|
||||
}
|
||||
|
||||
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
|
||||
#[must_use]
|
||||
pub fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
|
||||
let Self { ty, segment } = self;
|
||||
match ty {
|
||||
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
|
||||
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an end handle to the primary handle and a primary handle to an end handle. Note that the new handle may not exist (e.g. for a quadratic bézier).
|
||||
#[must_use]
|
||||
pub fn opposite(self) -> Self {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::vector_data::{HandleId, VectorData};
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup};
|
||||
use core::iter::zip;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::zip;
|
||||
|
||||
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
|
||||
macro_rules! create_ids {
|
||||
($($id:ident),*) => {
|
||||
$(
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
/// A strongly typed ID
|
||||
pub struct $id(u64);
|
||||
|
||||
@@ -53,7 +53,7 @@ create_ids! { InstanceId, PointId, SegmentId, RegionId, StrokeId, FillId }
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct NoHash(Option<u64>);
|
||||
|
||||
impl core::hash::Hasher for NoHash {
|
||||
impl Hasher for NoHash {
|
||||
fn finish(&self) -> u64 {
|
||||
self.0.unwrap()
|
||||
}
|
||||
@@ -70,15 +70,14 @@ impl core::hash::Hasher for NoHash {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct NoHashBuilder;
|
||||
|
||||
impl core::hash::BuildHasher for NoHashBuilder {
|
||||
impl std::hash::BuildHasher for NoHashBuilder {
|
||||
type Hasher = NoHash;
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
NoHash::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
/// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.
|
||||
pub struct PointDomain {
|
||||
id: Vec<PointId>,
|
||||
@@ -86,8 +85,8 @@ pub struct PointDomain {
|
||||
pub(crate) position: Vec<DVec2>,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for PointDomain {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl Hash for PointDomain {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
self.position.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state));
|
||||
}
|
||||
@@ -133,6 +132,11 @@ impl PointDomain {
|
||||
self.position.push(position);
|
||||
}
|
||||
|
||||
pub fn push_unchecked(&mut self, id: PointId, position: DVec2) {
|
||||
self.id.push(id);
|
||||
self.position.push(position);
|
||||
}
|
||||
|
||||
pub fn positions(&self) -> &[DVec2] {
|
||||
&self.position
|
||||
}
|
||||
@@ -195,15 +199,14 @@ impl PointDomain {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
/// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes.
|
||||
pub struct SegmentDomain {
|
||||
#[serde(alias = "ids")]
|
||||
id: Vec<SegmentId>,
|
||||
start_point: Vec<usize>,
|
||||
end_point: Vec<usize>,
|
||||
handles: Vec<bezier_rs::BezierHandles>,
|
||||
handles: Vec<BezierHandles>,
|
||||
stroke: Vec<StrokeId>,
|
||||
}
|
||||
|
||||
@@ -293,7 +296,7 @@ impl SegmentDomain {
|
||||
self.end_point[segment_index] = new;
|
||||
}
|
||||
|
||||
pub fn handles(&self) -> &[bezier_rs::BezierHandles] {
|
||||
pub fn handles(&self) -> &[BezierHandles] {
|
||||
&self.handles
|
||||
}
|
||||
|
||||
@@ -301,7 +304,7 @@ impl SegmentDomain {
|
||||
&self.stroke
|
||||
}
|
||||
|
||||
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
|
||||
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles, stroke: StrokeId) {
|
||||
debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain");
|
||||
|
||||
self.id.push(id);
|
||||
@@ -319,12 +322,12 @@ impl SegmentDomain {
|
||||
self.id.iter().copied().zip(self.end_point.iter_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut bezier_rs::BezierHandles, usize, usize)> {
|
||||
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut BezierHandles, usize, usize)> {
|
||||
let nested = self.id.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
|
||||
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
|
||||
}
|
||||
|
||||
pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut bezier_rs::BezierHandles, &mut usize, &mut usize)> {
|
||||
pub(crate) fn handles_and_points_mut(&mut self) -> impl Iterator<Item = (&mut BezierHandles, &mut usize, &mut usize)> {
|
||||
let nested = self.handles.iter_mut().zip(&mut self.start_point).zip(&mut self.end_point);
|
||||
nested.map(|((a, b), c)| (a, b, c))
|
||||
}
|
||||
@@ -368,7 +371,7 @@ impl SegmentDomain {
|
||||
self.id.iter().position(|&check_id| check_id == id)
|
||||
}
|
||||
|
||||
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
|
||||
fn resolve_range(&self, range: &std::ops::RangeInclusive<SegmentId>) -> Option<std::ops::RangeInclusive<usize>> {
|
||||
match (self.id_to_index(*range.start()), self.id_to_index(*range.end())) {
|
||||
(Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.id.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
|
||||
_ => {
|
||||
@@ -439,14 +442,13 @@ impl SegmentDomain {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
/// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the
|
||||
/// [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes.
|
||||
pub struct RegionDomain {
|
||||
#[serde(alias = "ids")]
|
||||
id: Vec<RegionId>,
|
||||
segment_range: Vec<core::ops::RangeInclusive<SegmentId>>,
|
||||
segment_range: Vec<std::ops::RangeInclusive<SegmentId>>,
|
||||
fill: Vec<FillId>,
|
||||
}
|
||||
|
||||
@@ -476,7 +478,7 @@ impl RegionDomain {
|
||||
/// Like [`Self::retain`] but also gives the function access to the segment range.
|
||||
///
|
||||
/// Note that this function requires an allocation that `retain` avoids.
|
||||
pub fn retain_with_region(&mut self, f: impl Fn(&RegionId, &core::ops::RangeInclusive<SegmentId>) -> bool) {
|
||||
pub fn retain_with_region(&mut self, f: impl Fn(&RegionId, &std::ops::RangeInclusive<SegmentId>) -> bool) {
|
||||
let keep = self.id.iter().zip(self.segment_range.iter()).map(|(id, range)| f(id, range)).collect::<Vec<_>>();
|
||||
let mut iter = keep.iter().copied();
|
||||
self.segment_range.retain(|_| iter.next().unwrap());
|
||||
@@ -486,7 +488,7 @@ impl RegionDomain {
|
||||
self.id.retain(|_| iter.next().unwrap());
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
|
||||
pub fn push(&mut self, id: RegionId, segment_range: std::ops::RangeInclusive<SegmentId>, fill: FillId) {
|
||||
if self.id.contains(&id) {
|
||||
warn!("Duplicate region");
|
||||
return;
|
||||
@@ -504,7 +506,7 @@ impl RegionDomain {
|
||||
self.id.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
|
||||
}
|
||||
|
||||
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut core::ops::RangeInclusive<SegmentId>)> {
|
||||
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut std::ops::RangeInclusive<SegmentId>)> {
|
||||
self.id.iter().copied().zip(self.segment_range.iter_mut())
|
||||
}
|
||||
|
||||
@@ -516,7 +518,7 @@ impl RegionDomain {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn segment_range(&self) -> &[core::ops::RangeInclusive<SegmentId>] {
|
||||
pub fn segment_range(&self) -> &[std::ops::RangeInclusive<SegmentId>] {
|
||||
&self.segment_range
|
||||
}
|
||||
|
||||
@@ -545,7 +547,7 @@ impl RegionDomain {
|
||||
/// Iterates over regions in the domain.
|
||||
///
|
||||
/// Tuple is: (id, segment_range, fill)
|
||||
pub fn iter(&self) -> impl Iterator<Item = (RegionId, core::ops::RangeInclusive<SegmentId>, FillId)> + '_ {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (RegionId, std::ops::RangeInclusive<SegmentId>, FillId)> + '_ {
|
||||
let ids = self.id.iter().copied();
|
||||
let segment_range = self.segment_range.iter().cloned();
|
||||
let fill = self.fill.iter().copied();
|
||||
@@ -643,7 +645,7 @@ impl FoundSubpath {
|
||||
|
||||
impl VectorData {
|
||||
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
|
||||
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier {
|
||||
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> bezier_rs::Bezier {
|
||||
let start = self.point_domain.positions()[start];
|
||||
let end = self.point_domain.positions()[end];
|
||||
bezier_rs::Bezier { start, end, handles }
|
||||
@@ -752,15 +754,15 @@ impl VectorData {
|
||||
}
|
||||
|
||||
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
|
||||
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
let mut first_point = None;
|
||||
let mut groups = Vec::new();
|
||||
let mut last: Option<(usize, bezier_rs::BezierHandles)> = None;
|
||||
let mut last: Option<(usize, BezierHandles)> = None;
|
||||
|
||||
for (handle, start, end) in segments {
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[start],
|
||||
in_handle: last.and_then(|(_, handle)| handle.end()),
|
||||
out_handle: handle.start(),
|
||||
@@ -776,7 +778,7 @@ impl VectorData {
|
||||
if closed {
|
||||
groups[0].in_handle = last_handle.end();
|
||||
} else {
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[end],
|
||||
in_handle: last_handle.end(),
|
||||
out_handle: None,
|
||||
@@ -789,10 +791,10 @@ impl VectorData {
|
||||
}
|
||||
|
||||
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous.
|
||||
fn subpath_from_segments(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
fn subpath_from_segments(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
|
||||
let mut first_point = None;
|
||||
let mut groups = Vec::new();
|
||||
let mut last: Option<(usize, bezier_rs::BezierHandles)> = None;
|
||||
let mut last: Option<(usize, BezierHandles)> = None;
|
||||
|
||||
for (handle, start, end) in segments {
|
||||
if last.is_some_and(|(previous_end, _)| previous_end != start) {
|
||||
@@ -801,7 +803,7 @@ impl VectorData {
|
||||
}
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[start],
|
||||
in_handle: last.and_then(|(_, handle)| handle.end()),
|
||||
out_handle: handle.start(),
|
||||
@@ -817,7 +819,7 @@ impl VectorData {
|
||||
if closed {
|
||||
groups[0].in_handle = last_handle.end();
|
||||
} else {
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.point_domain.positions()[end],
|
||||
in_handle: last_handle.end(),
|
||||
out_handle: None,
|
||||
@@ -849,7 +851,7 @@ impl VectorData {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_stroke_path_iter(&self) -> StrokePathIter {
|
||||
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> {
|
||||
let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()];
|
||||
for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() {
|
||||
points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false));
|
||||
@@ -908,13 +910,13 @@ impl VectorData {
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct an iterator [`bezier_rs::ManipulatorGroup`] for stroke.
|
||||
pub fn manipulator_groups(&self) -> impl Iterator<Item = bezier_rs::ManipulatorGroup<PointId>> + '_ {
|
||||
/// Construct an iterator [`ManipulatorGroup`] for stroke.
|
||||
pub fn manipulator_groups(&self) -> impl Iterator<Item = ManipulatorGroup<PointId>> + '_ {
|
||||
self.stroke_bezier_paths().flat_map(|mut path| std::mem::take(path.manipulator_groups_mut()))
|
||||
}
|
||||
|
||||
/// Get manipulator by id
|
||||
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<bezier_rs::ManipulatorGroup<PointId>> {
|
||||
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<ManipulatorGroup<PointId>> {
|
||||
let id = id.into();
|
||||
self.manipulator_groups().find(|group| group.id == id)
|
||||
}
|
||||
@@ -994,7 +996,7 @@ pub struct StrokePathIter<'a> {
|
||||
}
|
||||
|
||||
impl Iterator for StrokePathIter<'_> {
|
||||
type Item = (Vec<bezier_rs::ManipulatorGroup<PointId>>, bool);
|
||||
type Item = (Vec<ManipulatorGroup<PointId>>, bool);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current_start = if let Some((index, _)) = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1) {
|
||||
@@ -1016,7 +1018,7 @@ impl Iterator for StrokePathIter<'_> {
|
||||
loop {
|
||||
let Some(val) = self.points[point_index].take_first() else {
|
||||
// Dead end
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.vector_data.point_domain.positions()[point_index],
|
||||
in_handle,
|
||||
out_handle: None,
|
||||
@@ -1035,7 +1037,7 @@ impl Iterator for StrokePathIter<'_> {
|
||||
} else {
|
||||
self.vector_data.segment_domain.end_point()[val.segment_index]
|
||||
};
|
||||
groups.push(bezier_rs::ManipulatorGroup {
|
||||
groups.push(ManipulatorGroup {
|
||||
anchor: self.vector_data.point_domain.positions()[point_index],
|
||||
in_handle,
|
||||
out_handle: handles.start(),
|
||||
|
||||
@@ -3,14 +3,13 @@ use crate::Ctx;
|
||||
use crate::instances::Instance;
|
||||
use crate::uuid::generate_uuid;
|
||||
use bezier_rs::BezierHandles;
|
||||
use core::hash::BuildHasher;
|
||||
use dyn_any::DynAny;
|
||||
use kurbo::{BezPath, PathEl, Point};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::BuildHasher;
|
||||
|
||||
/// Represents a procedural change to the [`PointDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PointModification {
|
||||
add: Vec<PointId>,
|
||||
remove: HashSet<PointId>,
|
||||
@@ -81,8 +80,7 @@ impl PointModification {
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`SegmentDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SegmentModification {
|
||||
add: Vec<SegmentId>,
|
||||
remove: HashSet<SegmentId>,
|
||||
@@ -254,13 +252,12 @@ impl SegmentModification {
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`RegionDomain`] in [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RegionModification {
|
||||
add: Vec<RegionId>,
|
||||
remove: HashSet<RegionId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
segment_range: HashMap<RegionId, core::ops::RangeInclusive<SegmentId>>,
|
||||
segment_range: HashMap<RegionId, std::ops::RangeInclusive<SegmentId>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
fill: HashMap<RegionId, FillId>,
|
||||
}
|
||||
@@ -299,8 +296,7 @@ impl RegionModification {
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`VectorData`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VectorModification {
|
||||
points: PointModification,
|
||||
segments: SegmentModification,
|
||||
@@ -416,8 +412,8 @@ impl VectorModification {
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for VectorModification {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
impl Hash for VectorModification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
generate_uuid().hash(state)
|
||||
}
|
||||
}
|
||||
@@ -638,6 +634,33 @@ impl<'a> AppendBezpath<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VectorDataExt {
|
||||
/// Appends a Kurbo BezPath to the vector data.
|
||||
fn append_bezpath(&mut self, bezpath: BezPath);
|
||||
}
|
||||
|
||||
impl VectorDataExt for VectorData {
|
||||
fn append_bezpath(&mut self, bezpath: BezPath) {
|
||||
AppendBezpath::append_bezpath(self, bezpath);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HandleExt {
|
||||
/// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle.
|
||||
#[must_use]
|
||||
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType;
|
||||
}
|
||||
|
||||
impl HandleExt for HandleId {
|
||||
fn set_relative_position(self, relative_position: DVec2) -> VectorModificationType {
|
||||
let Self { ty, segment } = self;
|
||||
match ty {
|
||||
HandleType::Primary => VectorModificationType::SetPrimaryHandle { segment, relative_position },
|
||||
HandleType::End => VectorModificationType::SetEndHandle { segment, relative_position },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_points_on_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::offset_subpath::offset_subpath;
|
||||
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||
use super::misc::{CentroidType, point_to_dvec2};
|
||||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataExt, VectorDataTable};
|
||||
use crate::bounds::BoundingBox;
|
||||
use crate::instances::{Instance, InstanceMut, Instances};
|
||||
use crate::raster_types::{CPU, RasterDataTable};
|
||||
use crate::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, ReferencePoint, Transform};
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType};
|
||||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use crate::vector::{FillId, PointDomain, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
use bezier_rs::{Join, ManipulatorGroup, Subpath};
|
||||
use core::f64::consts::PI;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Point, Shape};
|
||||
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Shape};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::f64::consts::PI;
|
||||
use std::f64::consts::TAU;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Implemented for types that can be converted to an iterator of vector data.
|
||||
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
|
||||
trait VectorDataTableIterMut {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>>;
|
||||
}
|
||||
|
||||
impl VectorDataTableIterMut for GraphicGroupTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
|
||||
// Grab only the direct children
|
||||
self.instance_mut_iter()
|
||||
.filter_map(|element| element.instance.as_vector_data_mut())
|
||||
@@ -38,7 +39,7 @@ impl VectorDataTableIterMut for GraphicGroupTable {
|
||||
}
|
||||
|
||||
impl VectorDataTableIterMut for VectorDataTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<'_, VectorData>> {
|
||||
self.instance_mut_iter()
|
||||
}
|
||||
}
|
||||
@@ -210,7 +211,7 @@ where
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
||||
async fn repeat<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
@@ -220,10 +221,7 @@ async fn repeat<I: 'n + Send + Clone>(
|
||||
direction: PixelSize,
|
||||
angle: Angle,
|
||||
#[default(4)] instances: IntegerCount,
|
||||
) -> Instances<I>
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
) -> Instances<I> {
|
||||
let angle = angle.to_radians();
|
||||
let count = instances.max(1);
|
||||
let total = (count - 1) as f64;
|
||||
@@ -249,7 +247,7 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
||||
async fn circular_repeat<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
@@ -257,10 +255,7 @@ async fn circular_repeat<I: 'n + Send + Clone>(
|
||||
angle_offset: Angle,
|
||||
#[default(5)] radius: f64,
|
||||
#[default(5)] instances: IntegerCount,
|
||||
) -> Instances<I>
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
) -> Instances<I> {
|
||||
let count = instances.max(1);
|
||||
|
||||
let mut result_table = Instances::<I>::default();
|
||||
@@ -284,7 +279,7 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Copy to Points"), category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(name("Copy to Points"), category("Instancing"), path(graphene_core::vector))]
|
||||
async fn copy_to_points<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
points: VectorDataTable,
|
||||
@@ -312,10 +307,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
|
||||
random_rotation: Angle,
|
||||
/// Seed to determine unique variations on all the randomized instance angles.
|
||||
random_rotation_seed: SeedValue,
|
||||
) -> Instances<I>
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
) -> Instances<I> {
|
||||
let mut result_table = Instances::<I>::default();
|
||||
|
||||
let random_scale_difference = random_scale_max - random_scale_min;
|
||||
@@ -366,7 +358,7 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
||||
async fn mirror<I: 'n + Send + Clone>(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>)] instance: Instances<I>,
|
||||
@@ -376,7 +368,7 @@ async fn mirror<I: 'n + Send + Clone>(
|
||||
#[default(true)] keep_original: bool,
|
||||
) -> Instances<I>
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
Instances<I>: BoundingBox,
|
||||
{
|
||||
let mut result_table = Instances::default();
|
||||
|
||||
@@ -424,7 +416,7 @@ where
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn round_corners(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
@@ -551,141 +543,36 @@ async fn round_corners(
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Spatial Merge by Distance"), category("Debug"), path(graphene_core::vector))]
|
||||
async fn spatial_merge_by_distance(
|
||||
#[node_macro::node(name("Merge by Distance"), category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
pub fn merge_by_distance(
|
||||
_: impl Ctx,
|
||||
vector_data: VectorDataTable,
|
||||
#[default(0.1)]
|
||||
#[hard_min(0.0001)]
|
||||
distance: f64,
|
||||
distance: PixelLength,
|
||||
algorithm: MergeByDistanceAlgorithm,
|
||||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let point_count = vector_data.point_domain.positions().len();
|
||||
|
||||
// Find min x and y for grid cell normalization
|
||||
let mut min_x = f64::MAX;
|
||||
let mut min_y = f64::MAX;
|
||||
|
||||
// Calculate mins without collecting all positions
|
||||
for &pos in vector_data.point_domain.positions() {
|
||||
let transformed_pos = vector_data_transform.transform_point2(pos);
|
||||
min_x = min_x.min(transformed_pos.x);
|
||||
min_y = min_y.min(transformed_pos.y);
|
||||
}
|
||||
|
||||
// Create a spatial grid with cell size of 'distance'
|
||||
use std::collections::HashMap;
|
||||
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
|
||||
|
||||
// Add points to grid cells without collecting all positions first
|
||||
for i in 0..point_count {
|
||||
let pos = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
|
||||
let grid_x = ((pos.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos.y - min_y) / distance).floor() as i32;
|
||||
|
||||
grid.entry((grid_x, grid_y)).or_default().push(i);
|
||||
}
|
||||
|
||||
// Create point index mapping for merged points
|
||||
let mut point_index_map = vec![None; point_count];
|
||||
let mut merged_positions = Vec::new();
|
||||
let mut merged_indices = Vec::new();
|
||||
|
||||
// Process each point
|
||||
for i in 0..point_count {
|
||||
// Skip points that have already been processed
|
||||
if point_index_map[i].is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pos_i = vector_data_transform.transform_point2(vector_data.point_domain.positions()[i]);
|
||||
let grid_x = ((pos_i.x - min_x) / distance).floor() as i32;
|
||||
let grid_y = ((pos_i.y - min_y) / distance).floor() as i32;
|
||||
|
||||
let mut group = vec![i];
|
||||
|
||||
// Check only neighboring cells (3x3 grid around current cell)
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
let neighbor_cell = (grid_x + dx, grid_y + dy);
|
||||
|
||||
if let Some(indices) = grid.get(&neighbor_cell) {
|
||||
for &j in indices {
|
||||
if j > i && point_index_map[j].is_none() {
|
||||
let pos_j = vector_data_transform.transform_point2(vector_data.point_domain.positions()[j]);
|
||||
if pos_i.distance(pos_j) <= distance {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create merged point - calculate positions as needed
|
||||
let merged_position = group
|
||||
.iter()
|
||||
.map(|&idx| vector_data_transform.transform_point2(vector_data.point_domain.positions()[idx]))
|
||||
.fold(DVec2::ZERO, |sum, pos| sum + pos)
|
||||
/ group.len() as f64;
|
||||
|
||||
let merged_position = vector_data_transform.inverse().transform_point2(merged_position);
|
||||
let merged_index = merged_positions.len();
|
||||
|
||||
merged_positions.push(merged_position);
|
||||
merged_indices.push(vector_data.point_domain.ids()[group[0]]);
|
||||
|
||||
// Update mapping for all points in the group
|
||||
for &idx in &group {
|
||||
point_index_map[idx] = Some(merged_index);
|
||||
match algorithm {
|
||||
MergeByDistanceAlgorithm::Spatial => {
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
vector_data_instance.instance.merge_by_distance_spatial(vector_data_instance.transform, distance);
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new point domain with merged points
|
||||
let mut new_point_domain = PointDomain::new();
|
||||
for (idx, pos) in merged_indices.into_iter().zip(merged_positions) {
|
||||
new_point_domain.push(idx, pos);
|
||||
}
|
||||
|
||||
// Update segment domain
|
||||
let mut new_segment_domain = SegmentDomain::new();
|
||||
for segment_idx in 0..vector_data.segment_domain.ids().len() {
|
||||
let id = vector_data.segment_domain.ids()[segment_idx];
|
||||
let start = vector_data.segment_domain.start_point()[segment_idx];
|
||||
let end = vector_data.segment_domain.end_point()[segment_idx];
|
||||
let handles = vector_data.segment_domain.handles()[segment_idx];
|
||||
let stroke = vector_data.segment_domain.stroke()[segment_idx];
|
||||
|
||||
// Get new indices for start and end points
|
||||
let new_start = point_index_map[start].unwrap();
|
||||
let new_end = point_index_map[end].unwrap();
|
||||
|
||||
// Skip segments where start and end points were merged
|
||||
if new_start != new_end {
|
||||
new_segment_domain.push(id, new_start, new_end, handles, stroke);
|
||||
MergeByDistanceAlgorithm::Topological => {
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
vector_data_instance.instance.merge_by_distance_topological(distance);
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new vector data
|
||||
let mut result = vector_data.clone();
|
||||
result.point_domain = new_point_domain;
|
||||
result.segment_domain = new_segment_domain;
|
||||
|
||||
// Create and return the result
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Debug"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle: VectorDataTable) -> VectorDataTable {
|
||||
let Some((target, target_transform)) = rectangle.get(0).map(|rect| (rect.instance, rect.transform)) else {
|
||||
return vector_data;
|
||||
@@ -774,7 +661,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
|
||||
}
|
||||
|
||||
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
|
||||
#[node_macro::node(category("Vector"), name("Auto-Tangents"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(graphene_core::vector))]
|
||||
async fn auto_tangents(
|
||||
_: impl Ctx,
|
||||
source: VectorDataTable,
|
||||
@@ -1011,7 +898,7 @@ async fn auto_tangents(
|
||||
// result_table
|
||||
// }
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1038,7 +925,7 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn dimensions(_: impl Ctx, vector_data: VectorDataTable) -> DVec2 {
|
||||
vector_data
|
||||
.instance_ref_iter()
|
||||
@@ -1095,7 +982,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: VectorDataTable, #[default(
|
||||
points
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector), properties("offset_path_properties"))]
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1139,7 +1026,7 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1198,7 +1085,7 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn flatten_path<I: 'n + Send>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable)] graphic_group_input: Instances<I>) -> VectorDataTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
GraphicElement: From<Instances<I>>,
|
||||
{
|
||||
// A node based solution to support passing through vector data could be a network node with a cache node connected to
|
||||
// a Flatten Path connected to an if else node, another connection from the cache directly
|
||||
@@ -1243,18 +1130,26 @@ where
|
||||
};
|
||||
|
||||
// Flatten the graphic group input into the output VectorData instance
|
||||
let base_graphic_group = GraphicGroupTable::new(graphic_group_input.to_graphic_element());
|
||||
let base_graphic_group = GraphicGroupTable::new(GraphicElement::from(graphic_group_input));
|
||||
flatten_group(&base_graphic_group, &mut output);
|
||||
|
||||
// Return the single-row VectorDataTable containing the flattened VectorData subpaths
|
||||
output_table
|
||||
}
|
||||
|
||||
/// Convert vector geometry into a polyline composed of evenly spaced points.
|
||||
#[node_macro::node(category(""), path(graphene_core::vector))]
|
||||
async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, subpath_segment_lengths: Vec<f64>) -> VectorDataTable {
|
||||
// Limit the smallest spacing to something sensible to avoid freezing the application.
|
||||
let spacing = spacing.max(0.01);
|
||||
|
||||
async fn sample_polyline(
|
||||
_: impl Ctx,
|
||||
vector_data: VectorDataTable,
|
||||
spacing: PointSpacingType,
|
||||
separation: f64,
|
||||
quantity: f64,
|
||||
start_offset: f64,
|
||||
stop_offset: f64,
|
||||
adaptive_spacing: bool,
|
||||
subpath_segment_lengths: Vec<f64>,
|
||||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
@@ -1289,7 +1184,11 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
|
||||
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
|
||||
next_segment_index += segment_count;
|
||||
|
||||
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
|
||||
let amount = match spacing {
|
||||
PointSpacingType::Separation => separation,
|
||||
PointSpacingType::Quantity => quantity,
|
||||
};
|
||||
let Some(mut sample_bezpath) = sample_polyline_on_bezpath(bezpath, spacing, amount, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1307,9 +1206,109 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
|
||||
/// Splits a path at a given progress from 0 to 1 along the path, creating two new subpaths from the original one (if the path is initially open) or one open subpath (if the path is initially closed).
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Position on Path"), category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn split_path(_: impl Ctx, mut vector_data: VectorDataTable, progress: Fraction, parameterized_distance: bool, reverse: bool) -> VectorDataTable {
|
||||
let euclidian = !parameterized_distance;
|
||||
|
||||
let bezpaths = vector_data
|
||||
.instance_ref_iter()
|
||||
.enumerate()
|
||||
.flat_map(|(instance_row_index, vector_data)| vector_data.instance.stroke_bezpath_iter().map(|bezpath| (instance_row_index, bezpath)).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bezpath_count = bezpaths.len() as f64;
|
||||
let t_value = progress.clamp(0., bezpath_count);
|
||||
let t_value = if reverse { bezpath_count - t_value } else { t_value };
|
||||
let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize };
|
||||
|
||||
if let Some((instance_row_index, bezpath)) = bezpaths.get(index).cloned() {
|
||||
let mut result_vector_data = VectorData {
|
||||
style: vector_data.get(instance_row_index).unwrap().instance.style.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == instance_row_index) {
|
||||
result_vector_data.append_bezpath(bezpath.clone());
|
||||
}
|
||||
let t = if t_value == bezpath_count { 1. } else { t_value.fract() };
|
||||
|
||||
if let Some((first, second)) = split_bezpath(&bezpath, t, euclidian) {
|
||||
result_vector_data.append_bezpath(first);
|
||||
result_vector_data.append_bezpath(second);
|
||||
} else {
|
||||
result_vector_data.append_bezpath(bezpath);
|
||||
}
|
||||
|
||||
*vector_data.get_mut(instance_row_index).unwrap().instance = result_vector_data;
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Splits path segments into separate disconnected pieces where each is a distinct subpath.
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn split_segments(_: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
|
||||
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
|
||||
for vector_data_instance in vector_data.instance_mut_iter() {
|
||||
let points_count = vector_data_instance.instance.point_domain.ids().len();
|
||||
let segments_count = vector_data_instance.instance.segment_domain.ids().len();
|
||||
|
||||
let mut point_usages = vec![0_usize; points_count];
|
||||
|
||||
// Count how many times each point is used as an endpoint of the segments
|
||||
let start_points = vector_data_instance.instance.segment_domain.start_point().iter();
|
||||
let end_points = vector_data_instance.instance.segment_domain.end_point().iter();
|
||||
for (&start, &end) in start_points.zip(end_points) {
|
||||
point_usages[start] += 1;
|
||||
point_usages[end] += 1;
|
||||
}
|
||||
|
||||
let mut new_points = PointDomain::new();
|
||||
let mut offset_sum: usize = 0;
|
||||
let mut points_with_new_offsets = Vec::with_capacity(points_count);
|
||||
|
||||
// Build a new point domain with the original points, but with duplications based on their extra usages by the segments
|
||||
for (index, (point_id, point)) in vector_data_instance.instance.point_domain.iter().enumerate() {
|
||||
// Ensure at least one usage to preserve free-floating points not connected to any segments
|
||||
let usage_count = point_usages[index].max(1);
|
||||
|
||||
new_points.push_unchecked(point_id, point);
|
||||
|
||||
for i in 1..usage_count {
|
||||
new_points.push_unchecked(point_id.generate_from_hash(i as u64), point);
|
||||
}
|
||||
|
||||
points_with_new_offsets.push(offset_sum);
|
||||
offset_sum += usage_count;
|
||||
}
|
||||
|
||||
// Reconcile the segment domain with the new points
|
||||
vector_data_instance.instance.point_domain = new_points;
|
||||
for original_segment_index in 0..segments_count {
|
||||
let original_point_start_index = vector_data_instance.instance.segment_domain.start_point()[original_segment_index];
|
||||
let original_point_end_index = vector_data_instance.instance.segment_domain.end_point()[original_segment_index];
|
||||
|
||||
point_usages[original_point_start_index] -= 1;
|
||||
point_usages[original_point_end_index] -= 1;
|
||||
|
||||
let start_usage = points_with_new_offsets[original_point_start_index] + point_usages[original_point_start_index];
|
||||
let end_usage = points_with_new_offsets[original_point_end_index] + point_usages[original_point_end_index];
|
||||
|
||||
vector_data_instance.instance.segment_domain.set_start_point(original_segment_index, start_usage);
|
||||
vector_data_instance.instance.segment_domain.set_end_point(original_segment_index, end_usage);
|
||||
}
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn position_on_path(
|
||||
_: impl Ctx,
|
||||
/// The path to traverse.
|
||||
@@ -1344,8 +1343,9 @@ async fn position_on_path(
|
||||
}
|
||||
|
||||
/// Determines the angle of the tangent at a point on the path, given by its progress from 0 to 1 along the path.
|
||||
///
|
||||
/// If multiple subpaths make up the path, the whole number part of the progress value selects the subpath and the decimal part determines the position along it.
|
||||
#[node_macro::node(name("Tangent on Path"), category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn tangent_on_path(
|
||||
_: impl Ctx,
|
||||
/// The path to traverse.
|
||||
@@ -1409,7 +1409,6 @@ async fn poisson_disk_points(
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
// TODO: apply transform to points instead of modifying the paths
|
||||
bezpath.apply_affine(Affine::new(vector_data_instance.transform.to_cols_array()));
|
||||
bezpath.close_path();
|
||||
let bbox = bezpath.bounding_box();
|
||||
(bezpath, bbox)
|
||||
@@ -1421,16 +1420,9 @@ async fn poisson_disk_points(
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut poisson_disk_bezpath = BezPath::new();
|
||||
|
||||
for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation_disk_diameter, || rng.random::<f64>()) {
|
||||
if poisson_disk_bezpath.elements().is_empty() {
|
||||
poisson_disk_bezpath.move_to(dvec2_to_point(point));
|
||||
} else {
|
||||
poisson_disk_bezpath.line_to(dvec2_to_point(point));
|
||||
}
|
||||
result.point_domain.push(PointId::generate(), point);
|
||||
}
|
||||
result.append_bezpath(poisson_disk_bezpath);
|
||||
}
|
||||
|
||||
// Transfer the style from the input vector data to the result.
|
||||
@@ -1463,7 +1455,7 @@ async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> V
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Spline"), category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1509,7 +1501,7 @@ async fn spline(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1562,7 +1554,7 @@ async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)]
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDataTable, #[default(0.5)] time: Fraction) -> VectorDataTable {
|
||||
/// Subdivides the last segment of the bezpath to until it appends 'count' number of segments.
|
||||
fn make_new_segments(bezpath: &mut BezPath, count: usize) {
|
||||
@@ -1595,6 +1587,9 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
|
||||
}
|
||||
|
||||
for segment in new_segments {
|
||||
if bezpath.elements().is_empty() {
|
||||
bezpath.move_to(segment.start())
|
||||
}
|
||||
bezpath.push(segment.as_path_el());
|
||||
}
|
||||
|
||||
@@ -1670,7 +1665,10 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
|
||||
for mut source_path in source_paths {
|
||||
source_path.apply_affine(Affine::new(source_transform.to_cols_array()));
|
||||
|
||||
let end: Point = source_path.elements().last().and_then(|element| element.end_point()).unwrap_or_default();
|
||||
// Skip if the path has no segments else get the point at the end of the path.
|
||||
let Some(end) = source_path.segments().last().and_then(|element| Some(element.end())) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for element in source_path.elements_mut() {
|
||||
match element {
|
||||
@@ -1694,20 +1692,23 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
|
||||
for mut target_path in target_paths {
|
||||
target_path.apply_affine(Affine::new(source_transform.to_cols_array()));
|
||||
|
||||
let end: Point = target_path.elements().last().and_then(|element| element.end_point()).unwrap_or_default();
|
||||
// Skip if the path has no segments else get the point at the start of the path.
|
||||
let Some(start) = target_path.segments().next().and_then(|element| Some(element.start())) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for element in target_path.elements_mut() {
|
||||
match element {
|
||||
PathEl::MoveTo(point) => *point = point.lerp(end, time),
|
||||
PathEl::LineTo(point) => *point = point.lerp(end, time),
|
||||
PathEl::MoveTo(point) => *point = start.lerp(*point, time),
|
||||
PathEl::LineTo(point) => *point = start.lerp(*point, time),
|
||||
PathEl::QuadTo(point, point1) => {
|
||||
*point = point.lerp(end, time);
|
||||
*point1 = point1.lerp(end, time);
|
||||
*point = start.lerp(*point, time);
|
||||
*point1 = start.lerp(*point1, time);
|
||||
}
|
||||
PathEl::CurveTo(point, point1, point2) => {
|
||||
*point = point.lerp(end, time);
|
||||
*point1 = point1.lerp(end, time);
|
||||
*point2 = point2.lerp(end, time);
|
||||
*point = start.lerp(*point, time);
|
||||
*point1 = start.lerp(*point1, time);
|
||||
*point2 = start.lerp(*point2, time);
|
||||
}
|
||||
PathEl::ClosePath => {}
|
||||
}
|
||||
@@ -1832,7 +1833,7 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2,
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1846,7 +1847,7 @@ fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length)
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
|
||||
fn close_path(_: impl Ctx, source: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
@@ -1858,24 +1859,35 @@ fn close_path(_: impl Ctx, source: VectorDataTable) -> VectorDataTable {
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
|
||||
source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point))
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))]
|
||||
fn merge_by_distance(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut source_instance in source.instance_iter() {
|
||||
source_instance.instance.merge_by_distance(distance);
|
||||
result_table.push(source_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
#[node_macro::node(category("General"), path(graphene_core::vector))]
|
||||
async fn count_elements<I>(_: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, RasterDataTable<CPU>, RasterDataTable<GPU>)] source: Instances<I>) -> u64 {
|
||||
source.instance_iter().count() as u64
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn path_length(_: impl Ctx, source: VectorDataTable) -> f64 {
|
||||
source
|
||||
.instance_iter()
|
||||
.map(|vector_data_instance| {
|
||||
let transform = vector_data_instance.transform;
|
||||
vector_data_instance
|
||||
.instance
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
bezpath.perimeter(DEFAULT_ACCURACY)
|
||||
})
|
||||
.sum::<f64>()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 {
|
||||
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
|
||||
let vector_data = vector_data.eval(new_ctx).await;
|
||||
@@ -1889,7 +1901,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
|
||||
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>, centroid_type: CentroidType) -> DVec2 {
|
||||
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
|
||||
let vector_data = vector_data.eval(new_ctx).await;
|
||||
@@ -1947,13 +1959,14 @@ mod test {
|
||||
use super::*;
|
||||
use crate::Node;
|
||||
use bezier_rs::Bezier;
|
||||
use kurbo::Rect;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FutureWrapperNode<T: Clone>(T);
|
||||
|
||||
impl<'i, T: 'i + Clone + Send> Node<'i, Footprint> for FutureWrapperNode<T> {
|
||||
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
|
||||
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
|
||||
fn eval(&'i self, _input: Footprint) -> Self::Output {
|
||||
let value = self.0.clone();
|
||||
Box::pin(async move { value })
|
||||
@@ -1964,6 +1977,24 @@ mod test {
|
||||
VectorDataTable::new(VectorData::from_subpath(data))
|
||||
}
|
||||
|
||||
fn create_vector_data_instance(bezpath: BezPath, transform: DAffine2) -> Instance<VectorData> {
|
||||
let mut instance = VectorData::default();
|
||||
instance.append_bezpath(bezpath);
|
||||
Instance {
|
||||
instance,
|
||||
transform,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn vector_node_from_instances(data: Vec<Instance<VectorData>>) -> VectorDataTable {
|
||||
let mut vector_data_table = VectorDataTable::default();
|
||||
for instance in data {
|
||||
vector_data_table.push(instance);
|
||||
}
|
||||
vector_data_table
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeat() {
|
||||
let direction = DVec2::X * 1.5;
|
||||
@@ -2015,7 +2046,7 @@ mod test {
|
||||
// Test a VectorData with non-zero rotation
|
||||
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
|
||||
let mut square = VectorDataTable::new(square);
|
||||
*square.get_mut(0).unwrap().transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
|
||||
*square.get_mut(0).unwrap().transform *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4);
|
||||
let bounding_box = BoundingBoxNode {
|
||||
vector_data: FutureWrapperNode(square),
|
||||
}
|
||||
@@ -2051,51 +2082,63 @@ mod test {
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn sample_points() {
|
||||
async fn sample_polyline() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
|
||||
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
|
||||
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 30., 0., 0., 0., false, vec![100.]).await;
|
||||
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
|
||||
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn adaptive_spacing() {
|
||||
async fn sample_polyline_adaptive_spacing() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
|
||||
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
|
||||
let sample_polyline = super::sample_polyline(Footprint::default(), vector_node(path), PointSpacingType::Separation, 18., 0., 45., 10., true, vec![100.]).await;
|
||||
let sample_polyline = sample_polyline.instance_ref_iter().next().unwrap().instance;
|
||||
assert_eq!(sample_polyline.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn poisson() {
|
||||
let sample_points = super::poisson_disk_points(
|
||||
let poisson_points = super::poisson_disk_points(
|
||||
Footprint::default(),
|
||||
vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)),
|
||||
10. * std::f64::consts::SQRT_2,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
|
||||
let poisson_points = poisson_points.instance_ref_iter().next().unwrap().instance;
|
||||
assert!(
|
||||
(20..=40).contains(&sample_points.point_domain.positions().len()),
|
||||
(20..=40).contains(&poisson_points.point_domain.positions().len()),
|
||||
"actual len {}",
|
||||
sample_points.point_domain.positions().len()
|
||||
poisson_points.point_domain.positions().len()
|
||||
);
|
||||
for point in sample_points.point_domain.positions() {
|
||||
for point in poisson_points.point_domain.positions() {
|
||||
assert!(point.length() < 50. + 1., "Expected point in circle {point}")
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn lengths() {
|
||||
async fn segment_lengths() {
|
||||
let subpath = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let lengths = subpath_segment_lengths(Footprint::default(), vector_node(subpath)).await;
|
||||
assert_eq!(lengths, vec![100.]);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn path_length() {
|
||||
let bezpath = Rect::new(100., 100., 201., 201.).to_path(DEFAULT_ACCURACY);
|
||||
let transform = DAffine2::from_scale(DVec2::new(2., 2.));
|
||||
let instance = create_vector_data_instance(bezpath, transform);
|
||||
let instances = (0..5).map(|_| instance.clone()).collect::<Vec<Instance<VectorData>>>();
|
||||
|
||||
let length = super::path_length(Footprint::default(), vector_node_from_instances(instances)).await;
|
||||
|
||||
// 101 (each rectangle edge length) * 4 (rectangle perimeter) * 2 (scale) * 5 (number of rows)
|
||||
assert_eq!(length, 101. * 4. * 2. * 5.);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn spline() {
|
||||
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
|
||||
let spline = spline.instance_ref_iter().next().unwrap().instance;
|
||||
@@ -2106,16 +2149,16 @@ mod test {
|
||||
async fn morph() {
|
||||
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
|
||||
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
|
||||
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
|
||||
let sample_points = sample_points.instance_ref_iter().next().unwrap().instance;
|
||||
let morphed = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5).await;
|
||||
let morphed = morphed.instance_ref_iter().next().unwrap().instance;
|
||||
assert_eq!(
|
||||
&sample_points.point_domain.positions()[..4],
|
||||
&morphed.point_domain.positions()[..4],
|
||||
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn contains_segment(vector: VectorData, target: bezier_rs::Bezier) {
|
||||
fn contains_segment(vector: VectorData, target: Bezier) {
|
||||
let segments = vector.segment_bezier_iter().map(|x| x.1);
|
||||
let count = segments.filter(|bezier| bezier.abs_diff_eq(&target, 0.01) || bezier.reversed().abs_diff_eq(&target, 0.01)).count();
|
||||
assert_eq!(
|
||||
@@ -2136,16 +2179,16 @@ mod test {
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 8);
|
||||
|
||||
// Segments
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
|
||||
|
||||
// Joins
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2159,12 +2202,12 @@ mod test {
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
|
||||
// Segments
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
|
||||
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
|
||||
contains_segment(beveled.clone(), trimmed);
|
||||
|
||||
// Join
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2183,12 +2226,12 @@ mod test {
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
|
||||
// Segments
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-10., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-10., 0.)));
|
||||
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
|
||||
contains_segment(beveled.clone(), trimmed);
|
||||
|
||||
// Join
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2201,13 +2244,13 @@ mod test {
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
|
||||
// Segments
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
|
||||
|
||||
// Joins
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2222,11 +2265,11 @@ mod test {
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
|
||||
// Segments
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
|
||||
contains_segment(beveled.clone(), point);
|
||||
let [start, end] = curve.split(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))));
|
||||
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(start.start, start.end));
|
||||
contains_segment(beveled.clone(), Bezier::from_linear_dvec2(start.start, start.end));
|
||||
contains_segment(beveled.clone(), end);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user