mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Restructure node crates (#3384)
* Restructure node-graph folder * Fix wasm compilation * Move node definitions out of *-types crates * Cleanup * Fix warnings * Fix warnings * Start adding migrations * Add migrations and move memo nodes to gcore * Move nodes/gsvg-render -> rendering * Replace some hard coded identifiers and fix automatic conversion * Fix Vec2Value node migration * Fix formatting * Add more migrations * Cleanup features * Fix core_types::raster import * Update demo artwork (to make profile ci work) * Move *-types to node-graph/libraries folder * Add missing node migrations * Migrate more nodes * Remove impure memo node * More fixes and remove warning * Migrate context and add a few missing migrations --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
27
node-graph/libraries/application-io/Cargo.toml
Normal file
27
node-graph/libraries/application-io/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "graphene-application-io"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "graphene application io interface"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
wasm = ["dep:web-sys"]
|
||||
wgpu = ["dep:wgpu"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
text-nodes = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
glam = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
web-sys = { workspace = true, optional = true }
|
||||
wgpu = { workspace = true, optional = true }
|
||||
311
node-graph/libraries/application-io/src/lib.rs
Normal file
311
node-graph/libraries/application-io/src/lib.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use core_types::transform::Footprint;
|
||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||
use glam::{DAffine2, UVec2};
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::pin::Pin;
|
||||
use std::ptr::addr_of;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use text_nodes::FontCache;
|
||||
use vector_types::vector::style::RenderMode;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SurfaceId(pub u64);
|
||||
|
||||
impl std::fmt::Display for SurfaceId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, 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_family = "wasm")]
|
||||
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: wgpu::Texture,
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
pub texture: (),
|
||||
}
|
||||
|
||||
impl<'a> serde::Deserialize<'a> for ImageTexture {
|
||||
fn deserialize<D>(_: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
unimplemented!("attempted to serialize a 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_family = "wasm")]
|
||||
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
|
||||
// #[cfg(target_family = "wasm")]
|
||||
// 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>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandle = SurfaceHandle<web_sys::HtmlCanvasElement>;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandleFrame = SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
|
||||
|
||||
// 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_family = "wasm")]
|
||||
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
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 {}
|
||||
|
||||
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 use_vello(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ExportFormat {
|
||||
#[default]
|
||||
Svg,
|
||||
Raster,
|
||||
}
|
||||
|
||||
#[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 scale: f64,
|
||||
pub export_format: ExportFormat,
|
||||
pub time: TimingInformation,
|
||||
#[serde(alias = "view_mode")]
|
||||
pub render_mode: RenderMode,
|
||||
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 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 std::fmt::Formatter<'_>) -> std::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>;
|
||||
}
|
||||
49
node-graph/libraries/core-types/Cargo.toml
Normal file
49
node-graph/libraries/core-types/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "core-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Core types and traits for Graphene node system"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
nightly = []
|
||||
type_id_logging = []
|
||||
dealloc_nodes = []
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
no-std-types = { workspace = true, features = ["std"] }
|
||||
|
||||
# Workspace dependencies
|
||||
bitflags = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
dyn-any = { workspace = true }
|
||||
ctor = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
image = { workspace = true }
|
||||
tinyvec = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
lyon_geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
polycool = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Workspace dependencies
|
||||
tokio = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
35
node-graph/libraries/core-types/src/bounds.rs
Normal file
35
node-graph/libraries/core-types/src/bounds.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::Color;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||
pub enum RenderBoundingBox {
|
||||
#[default]
|
||||
None,
|
||||
Infinite,
|
||||
Rectangle([DVec2; 2]),
|
||||
}
|
||||
|
||||
pub trait BoundingBox {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
||||
}
|
||||
|
||||
macro_rules! none_impl {
|
||||
($t:path) => {
|
||||
impl BoundingBox for $t {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
RenderBoundingBox::None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
none_impl!(bool);
|
||||
none_impl!(f32);
|
||||
none_impl!(f64);
|
||||
none_impl!(DVec2);
|
||||
none_impl!(String);
|
||||
|
||||
impl BoundingBox for Color {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
RenderBoundingBox::Infinite
|
||||
}
|
||||
}
|
||||
9
node-graph/libraries/core-types/src/consts.rs
Normal file
9
node-graph/libraries/core-types/src/consts.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::Color;
|
||||
|
||||
// RENDERING
|
||||
pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
||||
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5;
|
||||
|
||||
// Fonts
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Cabin";
|
||||
pub const DEFAULT_FONT_STYLE: &str = "Regular (400)";
|
||||
520
node-graph/libraries/core-types/src/context.rs
Normal file
520
node-graph/libraries/core-types/src/context.rs
Normal file
@@ -0,0 +1,520 @@
|
||||
use crate::transform::Footprint;
|
||||
pub use no_std_types::context::{ArcCtx, Ctx};
|
||||
use std::any::Any;
|
||||
use std::borrow::Borrow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::panic::Location;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait ExtractFootprint {
|
||||
#[track_caller]
|
||||
fn try_footprint(&self) -> Option<&Footprint>;
|
||||
#[track_caller]
|
||||
fn footprint(&self) -> &Footprint {
|
||||
self.try_footprint().unwrap_or_else(|| {
|
||||
log::error!("Context did not have a footprint, called from: {}", Location::caller());
|
||||
&Footprint::DEFAULT
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtractRealTime {
|
||||
fn try_real_time(&self) -> Option<f64>;
|
||||
}
|
||||
|
||||
pub trait ExtractAnimationTime {
|
||||
fn try_animation_time(&self) -> Option<f64>;
|
||||
}
|
||||
|
||||
pub trait ExtractIndex {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>>;
|
||||
}
|
||||
|
||||
// Consider returning a slice or something like that
|
||||
pub trait ExtractVarArgs {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult>;
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult>;
|
||||
fn hash_varargs(&self, hasher: &mut dyn Hasher);
|
||||
}
|
||||
|
||||
// Consider returning a slice or something like that
|
||||
pub trait CloneVarArgs: ExtractVarArgs {
|
||||
// fn box_clone(&self) -> Vec<DynBox>;
|
||||
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>>;
|
||||
}
|
||||
|
||||
// Inject* traits for providing context features to downstream nodes
|
||||
pub trait InjectFootprint {}
|
||||
pub trait InjectRealTime {}
|
||||
pub trait InjectAnimationTime {}
|
||||
pub trait InjectIndex {}
|
||||
pub trait InjectVarArgs {}
|
||||
|
||||
// Modify* marker traits for context-transparent nodes
|
||||
pub trait ModifyFootprint: ExtractFootprint + InjectFootprint {}
|
||||
pub trait ModifyRealTime: ExtractRealTime + InjectRealTime {}
|
||||
pub trait ModifyAnimationTime: ExtractAnimationTime + InjectAnimationTime {}
|
||||
pub trait ModifyIndex: ExtractIndex + InjectIndex {}
|
||||
pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {}
|
||||
|
||||
pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs {}
|
||||
|
||||
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs> ExtractAll for T {}
|
||||
|
||||
impl<T: Ctx> InjectFootprint for T {}
|
||||
impl<T: Ctx> InjectRealTime for T {}
|
||||
impl<T: Ctx> InjectIndex for T {}
|
||||
impl<T: Ctx> InjectAnimationTime for T {}
|
||||
impl<T: Ctx> InjectVarArgs for T {}
|
||||
|
||||
impl<T: Ctx + InjectFootprint + ExtractFootprint> ModifyFootprint for T {}
|
||||
impl<T: Ctx + InjectRealTime + ExtractRealTime> ModifyRealTime for T {}
|
||||
impl<T: Ctx + InjectIndex + ExtractIndex> ModifyIndex for T {}
|
||||
impl<T: Ctx + InjectAnimationTime + ExtractAnimationTime> ModifyAnimationTime for T {}
|
||||
impl<T: Ctx + InjectVarArgs + ExtractVarArgs> ModifyVarArgs for T {}
|
||||
|
||||
// Public enum for flexible node macro codegen
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ContextFeature {
|
||||
ExtractFootprint,
|
||||
ExtractRealTime,
|
||||
ExtractAnimationTime,
|
||||
ExtractIndex,
|
||||
ExtractVarArgs,
|
||||
InjectFootprint,
|
||||
InjectRealTime,
|
||||
InjectAnimationTime,
|
||||
InjectIndex,
|
||||
InjectVarArgs,
|
||||
}
|
||||
|
||||
// Internal bitflags for fast compiler analysis
|
||||
use bitflags::bitflags;
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct ContextFeatures: u32 {
|
||||
const FOOTPRINT = 1 << 0;
|
||||
const REAL_TIME = 1 << 1;
|
||||
const ANIMATION_TIME = 1 << 2;
|
||||
const INDEX = 1 << 3;
|
||||
const VARARGS = 1 << 4;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct ContextDependencies {
|
||||
pub extract: ContextFeatures,
|
||||
pub inject: ContextFeatures,
|
||||
}
|
||||
|
||||
impl From<&[ContextFeature]> for ContextDependencies {
|
||||
fn from(features: &[ContextFeature]) -> Self {
|
||||
let mut extract = ContextFeatures::empty();
|
||||
let mut inject = ContextFeatures::empty();
|
||||
for feature in features {
|
||||
extract |= match feature {
|
||||
ContextFeature::ExtractFootprint => ContextFeatures::FOOTPRINT,
|
||||
ContextFeature::ExtractRealTime => ContextFeatures::REAL_TIME,
|
||||
ContextFeature::ExtractAnimationTime => ContextFeatures::ANIMATION_TIME,
|
||||
ContextFeature::ExtractIndex => ContextFeatures::INDEX,
|
||||
ContextFeature::ExtractVarArgs => ContextFeatures::VARARGS,
|
||||
_ => ContextFeatures::empty(),
|
||||
};
|
||||
inject |= match feature {
|
||||
ContextFeature::InjectFootprint => ContextFeatures::FOOTPRINT,
|
||||
ContextFeature::InjectRealTime => ContextFeatures::REAL_TIME,
|
||||
ContextFeature::InjectAnimationTime => ContextFeatures::ANIMATION_TIME,
|
||||
ContextFeature::InjectIndex => ContextFeatures::INDEX,
|
||||
ContextFeature::InjectVarArgs => ContextFeatures::VARARGS,
|
||||
_ => ContextFeatures::empty(),
|
||||
};
|
||||
}
|
||||
Self { extract, inject }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VarArgsResult {
|
||||
IndexOutOfBounds,
|
||||
NoVarArgs,
|
||||
}
|
||||
impl Ctx for Footprint {}
|
||||
impl ExtractFootprint for () {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
log::error!("tried to extract footprint form (), {}", Location::caller());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ExtractFootprint + Ctx + Sync + Send> ExtractFootprint for &T {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
(*self).try_footprint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ExtractFootprint + Sync> ExtractFootprint for Option<T> {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
self.as_ref().and_then(|x| x.try_footprint())
|
||||
}
|
||||
#[track_caller]
|
||||
fn footprint(&self) -> &Footprint {
|
||||
self.try_footprint().unwrap_or_else(|| {
|
||||
log::warn!("trying to extract footprint from context None {} ", Location::caller());
|
||||
&Footprint::DEFAULT
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<T: ExtractRealTime + Sync> ExtractRealTime for Option<T> {
|
||||
fn try_real_time(&self) -> Option<f64> {
|
||||
self.as_ref().and_then(|x| x.try_real_time())
|
||||
}
|
||||
}
|
||||
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Option<T> {
|
||||
fn try_animation_time(&self) -> Option<f64> {
|
||||
self.as_ref().and_then(|x| x.try_animation_time())
|
||||
}
|
||||
}
|
||||
impl<T: ExtractIndex> ExtractIndex for Option<T> {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
|
||||
self.as_ref().and_then(|x| x.try_index())
|
||||
}
|
||||
}
|
||||
impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Option<T> {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
|
||||
let Some(inner) = self else { return Err(VarArgsResult::NoVarArgs) };
|
||||
inner.vararg(index)
|
||||
}
|
||||
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
|
||||
let Some(inner) = self else { return Err(VarArgsResult::NoVarArgs) };
|
||||
inner.varargs_len()
|
||||
}
|
||||
|
||||
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
|
||||
if let Some(inner) = self {
|
||||
inner.hash_varargs(hasher)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: ExtractFootprint + Sync> ExtractFootprint for Arc<T> {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
(**self).try_footprint()
|
||||
}
|
||||
}
|
||||
impl<T: ExtractRealTime + Sync> ExtractRealTime for Arc<T> {
|
||||
fn try_real_time(&self) -> Option<f64> {
|
||||
(**self).try_real_time()
|
||||
}
|
||||
}
|
||||
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Arc<T> {
|
||||
fn try_animation_time(&self) -> Option<f64> {
|
||||
(**self).try_animation_time()
|
||||
}
|
||||
}
|
||||
impl<T: ExtractIndex> ExtractIndex for Arc<T> {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
|
||||
(**self).try_index()
|
||||
}
|
||||
}
|
||||
impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Arc<T> {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
|
||||
(**self).vararg(index)
|
||||
}
|
||||
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
|
||||
(**self).varargs_len()
|
||||
}
|
||||
|
||||
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
|
||||
(**self).hash_varargs(hasher)
|
||||
}
|
||||
}
|
||||
impl<T: CloneVarArgs + Sync> CloneVarArgs for Option<T> {
|
||||
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
|
||||
self.as_ref().and_then(CloneVarArgs::arc_clone)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ExtractVarArgs + Sync> ExtractVarArgs for &T {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
|
||||
(*self).vararg(index)
|
||||
}
|
||||
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
|
||||
(*self).varargs_len()
|
||||
}
|
||||
|
||||
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
|
||||
(*self).hash_varargs(hasher)
|
||||
}
|
||||
}
|
||||
impl<T: CloneVarArgs + Sync> CloneVarArgs for Arc<T> {
|
||||
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
|
||||
(**self).arc_clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ctx for ContextImpl<'_> {}
|
||||
impl ArcCtx for OwnedContextImpl {}
|
||||
|
||||
impl ExtractFootprint for ContextImpl<'_> {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
self.footprint
|
||||
}
|
||||
}
|
||||
impl ExtractRealTime for ContextImpl<'_> {
|
||||
fn try_real_time(&self) -> Option<f64> {
|
||||
self.real_time
|
||||
}
|
||||
}
|
||||
impl ExtractIndex for ContextImpl<'_> {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
|
||||
self.index.clone().map(|x| x.into_iter())
|
||||
}
|
||||
}
|
||||
impl ExtractVarArgs for ContextImpl<'_> {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
|
||||
let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) };
|
||||
inner.get(index).ok_or(VarArgsResult::IndexOutOfBounds).copied()
|
||||
}
|
||||
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
|
||||
let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) };
|
||||
Ok(inner.len())
|
||||
}
|
||||
|
||||
fn hash_varargs(&self, _hasher: &mut dyn Hasher) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractFootprint for OwnedContextImpl {
|
||||
fn try_footprint(&self) -> Option<&Footprint> {
|
||||
self.footprint.as_ref()
|
||||
}
|
||||
}
|
||||
impl ExtractRealTime for OwnedContextImpl {
|
||||
fn try_real_time(&self) -> Option<f64> {
|
||||
self.real_time
|
||||
}
|
||||
}
|
||||
impl ExtractAnimationTime for OwnedContextImpl {
|
||||
fn try_animation_time(&self) -> Option<f64> {
|
||||
self.animation_time
|
||||
}
|
||||
}
|
||||
impl ExtractIndex for OwnedContextImpl {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
|
||||
self.index.clone().map(|x| x.into_iter())
|
||||
}
|
||||
}
|
||||
impl ExtractVarArgs for OwnedContextImpl {
|
||||
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
|
||||
let Some(ref inner) = self.varargs else {
|
||||
let Some(ref parent) = self.parent else {
|
||||
return Err(VarArgsResult::NoVarArgs);
|
||||
};
|
||||
return parent.vararg(index);
|
||||
};
|
||||
inner.get(index).map(|x| x.as_ref() as DynRef<'_>).ok_or(VarArgsResult::IndexOutOfBounds)
|
||||
}
|
||||
|
||||
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
|
||||
let Some(ref inner) = self.varargs else {
|
||||
let Some(ref parent) = self.parent else {
|
||||
return Err(VarArgsResult::NoVarArgs);
|
||||
};
|
||||
return parent.varargs_len();
|
||||
};
|
||||
Ok(inner.len())
|
||||
}
|
||||
|
||||
fn hash_varargs(&self, mut hasher: &mut dyn Hasher) {
|
||||
match (&self.varargs, &self.parent) {
|
||||
(Some(inner), _) => {
|
||||
for arg in inner.iter() {
|
||||
arg.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
(None, Some(parent)) => {
|
||||
parent.hash_varargs(hasher);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl CloneVarArgs for Arc<OwnedContextImpl> {
|
||||
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
|
||||
type DynRef<'a> = &'a (dyn Any + Send + Sync);
|
||||
type DynBox = Box<dyn AnyHash + Send + Sync>;
|
||||
|
||||
#[derive(dyn_any::DynAny)]
|
||||
pub struct OwnedContextImpl {
|
||||
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
|
||||
index: Option<Vec<usize>>,
|
||||
real_time: Option<f64>,
|
||||
animation_time: Option<f64>,
|
||||
}
|
||||
|
||||
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_len", &self.varargs.as_ref().map(|x| x.len()))
|
||||
.field("parent", &self.parent.as_ref().map(|_| "<Parent>"))
|
||||
.field("index", &self.index)
|
||||
.field("real_time", &self.real_time)
|
||||
.field("animation_time", &self.animation_time)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OwnedContextImpl {
|
||||
#[track_caller]
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for OwnedContextImpl {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.footprint.hash(state);
|
||||
self.hash_varargs(state);
|
||||
self.index.hash(state);
|
||||
self.real_time.map(|x| x.to_bits()).hash(state);
|
||||
self.animation_time.map(|x| x.to_bits()).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnedContextImpl {
|
||||
#[track_caller]
|
||||
pub fn from<T: ExtractAll + CloneVarArgs>(value: T) -> Self {
|
||||
OwnedContextImpl::from_flags(value, ContextFeatures::all())
|
||||
}
|
||||
#[track_caller]
|
||||
pub fn from_flags<T: ExtractAll + CloneVarArgs>(value: T, bitflags: ContextFeatures) -> Self {
|
||||
let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten();
|
||||
let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten();
|
||||
let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten();
|
||||
let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten();
|
||||
let parent = bitflags
|
||||
.contains(ContextFeatures::VARARGS)
|
||||
.then(|| match value.varargs_len() {
|
||||
Ok(x) if x > 0 => value.arc_clone(),
|
||||
_ => None,
|
||||
})
|
||||
.flatten();
|
||||
|
||||
OwnedContextImpl {
|
||||
footprint,
|
||||
varargs: None,
|
||||
parent,
|
||||
index: index.map(|x| x.collect()),
|
||||
real_time,
|
||||
animation_time,
|
||||
}
|
||||
}
|
||||
pub const fn empty() -> Self {
|
||||
OwnedContextImpl {
|
||||
footprint: None,
|
||||
varargs: None,
|
||||
parent: None,
|
||||
index: None,
|
||||
real_time: None,
|
||||
animation_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DynHash {
|
||||
fn dyn_hash(&self, state: &mut dyn Hasher);
|
||||
}
|
||||
|
||||
impl<H: Hash + ?Sized> DynHash for H {
|
||||
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
|
||||
self.hash(&mut state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for dyn AnyHash {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.dyn_hash(state);
|
||||
}
|
||||
}
|
||||
impl Hash for Box<dyn AnyHash + Send + Sync> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(**self).dyn_hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyHash: DynHash + Any {}
|
||||
impl<T: DynHash + Any> AnyHash for T {}
|
||||
|
||||
impl OwnedContextImpl {
|
||||
pub fn set_footprint(&mut self, footprint: Footprint) {
|
||||
self.footprint = Some(footprint);
|
||||
}
|
||||
pub fn with_footprint(mut self, footprint: Footprint) -> Self {
|
||||
self.footprint = Some(footprint);
|
||||
self
|
||||
}
|
||||
pub fn with_real_time(mut self, real_time: f64) -> Self {
|
||||
self.real_time = Some(real_time);
|
||||
self
|
||||
}
|
||||
pub fn with_animation_time(mut self, animation_time: f64) -> Self {
|
||||
self.animation_time = Some(animation_time);
|
||||
self
|
||||
}
|
||||
pub fn with_vararg(mut self, value: Box<dyn AnyHash + Send + Sync>) -> Self {
|
||||
assert!(self.varargs.is_none_or(|value| value.is_empty()));
|
||||
self.varargs = Some(Arc::new([value]));
|
||||
self
|
||||
}
|
||||
pub fn with_index(mut self, index: usize) -> Self {
|
||||
if let Some(current_index) = &mut self.index {
|
||||
current_index.push(index);
|
||||
} else {
|
||||
self.index = Some(vec![index]);
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn into_context(self) -> Option<Arc<Self>> {
|
||||
Some(Arc::new(self))
|
||||
}
|
||||
pub fn erase_parent(mut self) -> Self {
|
||||
self.parent = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, dyn_any::DynAny)]
|
||||
pub struct ContextImpl<'a> {
|
||||
pub(crate) footprint: Option<&'a Footprint>,
|
||||
varargs: Option<&'a [DynRef<'a>]>,
|
||||
index: Option<Vec<usize>>, // This could be converted into a single enum to save extra bytes
|
||||
real_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl<'a> ContextImpl<'a> {
|
||||
pub fn with_footprint<'f>(&self, new_footprint: &'f Footprint, varargs: Option<&'f impl Borrow<[DynRef<'f>]>>) -> ContextImpl<'f>
|
||||
where
|
||||
'a: 'f,
|
||||
{
|
||||
ContextImpl {
|
||||
footprint: Some(new_footprint),
|
||||
varargs: varargs.map(|x| x.borrow()),
|
||||
index: self.index.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
17
node-graph/libraries/core-types/src/generic.rs
Normal file
17
node-graph/libraries/core-types/src/generic.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::Node;
|
||||
use std::marker::PhantomData;
|
||||
#[derive(Clone)]
|
||||
pub struct FnNode<T: Fn(I) -> O, I, O>(T, PhantomData<(I, O)>);
|
||||
|
||||
impl<'i, T: Fn(I) -> O + 'i, O: 'i, I: 'i> Node<'i, I> for FnNode<T, I, O> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> Self::Output {
|
||||
self.0(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Fn(I) -> O, I, O> FnNode<T, I, O> {
|
||||
pub fn new(f: T) -> Self {
|
||||
FnNode(f, PhantomData)
|
||||
}
|
||||
}
|
||||
156
node-graph/libraries/core-types/src/lib.rs
Normal file
156
node-graph/libraries/core-types/src/lib.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
extern crate log;
|
||||
|
||||
pub mod bounds;
|
||||
pub mod consts;
|
||||
pub mod context;
|
||||
pub mod generic;
|
||||
pub mod math;
|
||||
pub mod memo;
|
||||
pub mod misc;
|
||||
pub mod ops;
|
||||
pub mod registry;
|
||||
pub mod render_complexity;
|
||||
pub mod table;
|
||||
pub mod transform;
|
||||
pub mod uuid;
|
||||
pub mod value;
|
||||
|
||||
pub use crate as core_types;
|
||||
pub use blending::*;
|
||||
pub use color::Color;
|
||||
pub use context::*;
|
||||
pub use ctor;
|
||||
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
|
||||
pub use memo::MemoHash;
|
||||
pub use no_std_types::AsU32;
|
||||
pub use no_std_types::blending;
|
||||
pub use no_std_types::choice_type;
|
||||
pub use no_std_types::color;
|
||||
pub use no_std_types::shaders;
|
||||
pub use num_traits;
|
||||
pub use specta;
|
||||
use std::any::TypeId;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
pub use types::Cow;
|
||||
|
||||
// pub trait Node: for<'n> NodeIO<'n> {
|
||||
/// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct.
|
||||
/// See `node-graph/README.md` for information on how to define a new node.
|
||||
pub trait Node<'i, Input> {
|
||||
type Output: 'i;
|
||||
/// Evaluates the node with the single specified input.
|
||||
fn eval(&'i self, input: Input) -> Self::Output;
|
||||
/// Resets the node, e.g. the LetNode's cache is set to None.
|
||||
fn reset(&self) {}
|
||||
/// Returns the name of the node for diagnostic purposes.
|
||||
fn node_name(&self) -> &'static str {
|
||||
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 std::any::Any + Send + Sync>> {
|
||||
log::warn!("Node::serialize not implemented for {}", std::any::type_name::<Self>());
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
pub trait NodeIO<'i, Input>: Node<'i, Input>
|
||||
where
|
||||
Self::Output: 'i + StaticTypeSized,
|
||||
Input: StaticTypeSized,
|
||||
{
|
||||
fn input_type(&self) -> TypeId {
|
||||
TypeId::of::<Input::Static>()
|
||||
}
|
||||
fn input_type_name(&self) -> &'static str {
|
||||
std::any::type_name::<Input>()
|
||||
}
|
||||
fn output_type(&self) -> TypeId {
|
||||
TypeId::of::<<Self::Output as StaticTypeSized>::Static>()
|
||||
}
|
||||
fn output_type_name(&self) -> &'static str {
|
||||
std::any::type_name::<Self::Output>()
|
||||
}
|
||||
fn to_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes {
|
||||
NodeIOTypes {
|
||||
call_argument: concrete!(<Input as StaticTypeSized>::Static),
|
||||
return_value: concrete!(<Self::Output as StaticTypeSized>::Static),
|
||||
inputs,
|
||||
}
|
||||
}
|
||||
fn to_async_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes
|
||||
where
|
||||
<Self::Output as Future>::Output: StaticTypeSized,
|
||||
Self::Output: Future,
|
||||
{
|
||||
NodeIOTypes {
|
||||
call_argument: concrete!(<Input as StaticTypeSized>::Static),
|
||||
return_value: future!(<<Self::Output as Future>::Output as StaticTypeSized>::Static),
|
||||
inputs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i, N: Node<'i, I>, I> NodeIO<'i, I> for N
|
||||
where
|
||||
N::Output: 'i + StaticTypeSized,
|
||||
I: StaticTypeSized,
|
||||
{
|
||||
}
|
||||
|
||||
impl<'i, I: 'i, N: Node<'i, I> + ?Sized> Node<'i, I> for &'i N {
|
||||
type Output = N::Output;
|
||||
fn eval(&'i self, input: I) -> N::Output {
|
||||
(*self).eval(input)
|
||||
}
|
||||
}
|
||||
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for Box<N> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> O {
|
||||
(**self).eval(input)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i, I, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> O {
|
||||
(**self).eval(input)
|
||||
}
|
||||
}
|
||||
impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> O {
|
||||
(**self).eval(input)
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub trait InputAccessorSourceIdentifier {
|
||||
fn has_identifier(&self, identifier: &str) -> bool;
|
||||
}
|
||||
|
||||
pub trait InputAccessor<'n, Source: 'n>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn new_with_source(source: &'n Source) -> Option<Self>;
|
||||
}
|
||||
|
||||
pub trait NodeInputDecleration {
|
||||
const INDEX: usize;
|
||||
fn identifier() -> ProtoNodeIdentifier;
|
||||
type Result;
|
||||
}
|
||||
109
node-graph/libraries/core-types/src/math/bbox.rs
Normal file
109
node-graph/libraries/core-types/src/math/bbox.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Clone, Debug, DynAny)]
|
||||
pub struct AxisAlignedBbox {
|
||||
pub start: DVec2,
|
||||
pub end: DVec2,
|
||||
}
|
||||
|
||||
impl AxisAlignedBbox {
|
||||
pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO };
|
||||
pub const ONE: Self = Self { start: DVec2::ZERO, end: DVec2::ONE };
|
||||
|
||||
pub fn size(&self) -> DVec2 {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
pub fn to_transform(&self) -> DAffine2 {
|
||||
DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size())
|
||||
}
|
||||
|
||||
pub fn contains(&self, point: DVec2) -> bool {
|
||||
point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: &AxisAlignedBbox) -> bool {
|
||||
other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y
|
||||
}
|
||||
|
||||
pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
|
||||
AxisAlignedBbox {
|
||||
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
|
||||
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
|
||||
}
|
||||
}
|
||||
pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option<AxisAlignedBbox> {
|
||||
match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) {
|
||||
(true, true) => None,
|
||||
(true, _) => Some(other.clone()),
|
||||
(_, true) => Some(self.clone()),
|
||||
_ => Some(AxisAlignedBbox {
|
||||
start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)),
|
||||
end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersect(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox {
|
||||
AxisAlignedBbox {
|
||||
start: DVec2::new(self.start.x.max(other.start.x), self.start.y.max(other.start.y)),
|
||||
end: DVec2::new(self.end.x.min(other.end.x), self.end.y.min(other.end.y)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(DVec2, DVec2)> for AxisAlignedBbox {
|
||||
fn from((start, end): (DVec2, DVec2)) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Bbox {
|
||||
pub top_left: DVec2,
|
||||
pub top_right: DVec2,
|
||||
pub bottom_left: DVec2,
|
||||
pub bottom_right: DVec2,
|
||||
}
|
||||
|
||||
impl Bbox {
|
||||
pub fn unit() -> Self {
|
||||
Self {
|
||||
top_left: DVec2::new(0., 1.),
|
||||
top_right: DVec2::new(1., 1.),
|
||||
bottom_left: DVec2::new(0., 0.),
|
||||
bottom_right: DVec2::new(1., 0.),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_transform(transform: DAffine2) -> Self {
|
||||
Self {
|
||||
top_left: transform.transform_point2(DVec2::new(0., 1.)),
|
||||
top_right: transform.transform_point2(DVec2::new(1., 1.)),
|
||||
bottom_left: transform.transform_point2(DVec2::new(0., 0.)),
|
||||
bottom_right: transform.transform_point2(DVec2::new(1., 0.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn affine_transform(self, transform: DAffine2) -> Self {
|
||||
Self {
|
||||
top_left: transform.transform_point2(self.top_left),
|
||||
top_right: transform.transform_point2(self.top_right),
|
||||
bottom_left: transform.transform_point2(self.bottom_left),
|
||||
bottom_right: transform.transform_point2(self.bottom_right),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_axis_aligned_bbox(&self) -> AxisAlignedBbox {
|
||||
let start_x = self.top_left.x.min(self.top_right.x).min(self.bottom_left.x).min(self.bottom_right.x);
|
||||
let start_y = self.top_left.y.min(self.top_right.y).min(self.bottom_left.y).min(self.bottom_right.y);
|
||||
let end_x = self.top_left.x.max(self.top_right.x).max(self.bottom_left.x).max(self.bottom_right.x);
|
||||
let end_y = self.top_left.y.max(self.top_right.y).max(self.bottom_left.y).max(self.bottom_right.y);
|
||||
|
||||
AxisAlignedBbox {
|
||||
start: DVec2::new(start_x, start_y),
|
||||
end: DVec2::new(end_x, end_y),
|
||||
}
|
||||
}
|
||||
}
|
||||
4
node-graph/libraries/core-types/src/math/mod.rs
Normal file
4
node-graph/libraries/core-types/src/math/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod bbox;
|
||||
pub mod polynomial;
|
||||
pub mod quad;
|
||||
pub mod rect;
|
||||
292
node-graph/libraries/core-types/src/math/polynomial.rs
Normal file
292
node-graph/libraries/core-types/src/math/polynomial.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use kurbo::PathSeg;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
|
||||
|
||||
/// A struct that represents a polynomial with a maximum degree of `N-1`.
|
||||
///
|
||||
/// It provides basic mathematical operations for polynomials like addition, multiplication, differentiation, integration, etc.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct Polynomial<const N: usize> {
|
||||
coefficients: [f64; N],
|
||||
}
|
||||
|
||||
impl<const N: usize> Polynomial<N> {
|
||||
/// Create a new polynomial from the coefficients given in the array.
|
||||
///
|
||||
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
|
||||
pub fn new(coefficients: [f64; N]) -> Polynomial<N> {
|
||||
Polynomial { coefficients }
|
||||
}
|
||||
|
||||
/// Create a polynomial where all its coefficients are zero.
|
||||
pub fn zero() -> Polynomial<N> {
|
||||
Polynomial { coefficients: [0.; N] }
|
||||
}
|
||||
|
||||
/// Return an immutable reference to the coefficients.
|
||||
///
|
||||
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
|
||||
pub fn coefficients(&self) -> &[f64; N] {
|
||||
&self.coefficients
|
||||
}
|
||||
|
||||
/// Return a mutable reference to the coefficients.
|
||||
///
|
||||
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
|
||||
pub fn coefficients_mut(&mut self) -> &mut [f64; N] {
|
||||
&mut self.coefficients
|
||||
}
|
||||
|
||||
/// Evaluate the polynomial at `value`.
|
||||
pub fn eval(&self, value: f64) -> f64 {
|
||||
self.coefficients.iter().rev().copied().reduce(|acc, x| acc * value + x).unwrap()
|
||||
}
|
||||
|
||||
/// Return the same polynomial but with a different maximum degree of `M-1`.\
|
||||
///
|
||||
/// Returns `None` if the polynomial cannot fit in the specified size.
|
||||
pub fn as_size<const M: usize>(&self) -> Option<Polynomial<M>> {
|
||||
let mut coefficients = [0.; M];
|
||||
|
||||
if M >= N {
|
||||
coefficients[..N].copy_from_slice(&self.coefficients);
|
||||
} else if self.coefficients.iter().rev().take(N - M).all(|&x| x == 0.) {
|
||||
coefficients.copy_from_slice(&self.coefficients[..M])
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Polynomial { coefficients })
|
||||
}
|
||||
|
||||
/// Computes the derivative in place.
|
||||
pub fn derivative_mut(&mut self) {
|
||||
self.coefficients.iter_mut().enumerate().for_each(|(index, x)| *x *= index as f64);
|
||||
self.coefficients.rotate_left(1);
|
||||
}
|
||||
|
||||
/// Computes the antiderivative at `C = 0` in place.
|
||||
///
|
||||
/// Returns `None` if the polynomial is not big enough to accommodate the extra degree.
|
||||
pub fn antiderivative_mut(&mut self) -> Option<()> {
|
||||
if self.coefficients[N - 1] != 0. {
|
||||
return None;
|
||||
}
|
||||
self.coefficients.rotate_right(1);
|
||||
self.coefficients.iter_mut().enumerate().skip(1).for_each(|(index, x)| *x /= index as f64);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Computes the polynomial's derivative.
|
||||
pub fn derivative(&self) -> Polynomial<N> {
|
||||
let mut ans = *self;
|
||||
ans.derivative_mut();
|
||||
ans
|
||||
}
|
||||
|
||||
/// Computes the antiderivative at `C = 0`.
|
||||
///
|
||||
/// Returns `None` if the polynomial is not big enough to accommodate the extra degree.
|
||||
pub fn antiderivative(&self) -> Option<Polynomial<N>> {
|
||||
let mut ans = *self;
|
||||
ans.antiderivative_mut()?;
|
||||
Some(ans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for Polynomial<N> {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Display for Polynomial<N> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
for (index, coefficient) in self.coefficients.iter().enumerate().rev().filter(|&(_, &coefficient)| coefficient != 0.) {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
f.write_str(" + ")?
|
||||
}
|
||||
|
||||
coefficient.fmt(f)?;
|
||||
if index == 0 {
|
||||
continue;
|
||||
}
|
||||
f.write_str("x")?;
|
||||
if index == 1 {
|
||||
continue;
|
||||
}
|
||||
f.write_str("^")?;
|
||||
index.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> AddAssign<&Polynomial<N>> for Polynomial<N> {
|
||||
fn add_assign(&mut self, rhs: &Polynomial<N>) {
|
||||
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Add for &Polynomial<N> {
|
||||
type Output = Polynomial<N>;
|
||||
|
||||
fn add(self, other: &Polynomial<N>) -> Polynomial<N> {
|
||||
let mut output = *self;
|
||||
output += other;
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Neg for &Polynomial<N> {
|
||||
type Output = Polynomial<N>;
|
||||
|
||||
fn neg(self) -> Polynomial<N> {
|
||||
let mut output = *self;
|
||||
output.coefficients.iter_mut().for_each(|x| *x = -*x);
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Neg for Polynomial<N> {
|
||||
type Output = Polynomial<N>;
|
||||
|
||||
fn neg(mut self) -> Polynomial<N> {
|
||||
self.coefficients.iter_mut().for_each(|x| *x = -*x);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> SubAssign<&Polynomial<N>> for Polynomial<N> {
|
||||
fn sub_assign(&mut self, rhs: &Polynomial<N>) {
|
||||
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Sub for &Polynomial<N> {
|
||||
type Output = Polynomial<N>;
|
||||
|
||||
fn sub(self, other: &Polynomial<N>) -> Polynomial<N> {
|
||||
let mut output = *self;
|
||||
output -= other;
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> MulAssign<&Polynomial<N>> for Polynomial<N> {
|
||||
fn mul_assign(&mut self, rhs: &Polynomial<N>) {
|
||||
for i in (0..N).rev() {
|
||||
self.coefficients[i] = self.coefficients[i] * rhs.coefficients[0];
|
||||
for j in 0..i {
|
||||
self.coefficients[i] += self.coefficients[j] * rhs.coefficients[i - j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Mul for &Polynomial<N> {
|
||||
type Output = Polynomial<N>;
|
||||
|
||||
fn mul(self, other: &Polynomial<N>) -> Polynomial<N> {
|
||||
let mut output = *self;
|
||||
output *= other;
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns two [`Polynomial`]s representing the parametric equations for x and y coordinates of the bezier curve respectively.
|
||||
/// The domain of both the equations are from t=0.0 representing the start and t=1.0 representing the end of the bezier curve.
|
||||
pub fn pathseg_to_parametric_polynomial(segment: PathSeg) -> (Polynomial<4>, Polynomial<4>) {
|
||||
match segment {
|
||||
PathSeg::Line(line) => {
|
||||
let term1 = line.p0 - line.p1;
|
||||
(Polynomial::new([line.p0.x, term1.x, 0., 0.]), Polynomial::new([line.p0.y, term1.y, 0., 0.]))
|
||||
}
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let term1 = 2. * (quad_bez.p1 - quad_bez.p0);
|
||||
let term2 = quad_bez.p0 - 2. * quad_bez.p1.to_vec2() + quad_bez.p2.to_vec2();
|
||||
|
||||
(Polynomial::new([quad_bez.p0.x, term1.x, term2.x, 0.]), Polynomial::new([quad_bez.p0.y, term1.y, term2.y, 0.]))
|
||||
}
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let term1 = 3. * (cubic_bez.p1 - cubic_bez.p0);
|
||||
let term2 = 3. * (cubic_bez.p2 - cubic_bez.p1) - term1;
|
||||
let term3 = cubic_bez.p3 - cubic_bez.p0 - term2 - term1;
|
||||
|
||||
(
|
||||
Polynomial::new([cubic_bez.p0.x, term1.x, term2.x, term3.x]),
|
||||
Polynomial::new([cubic_bez.p0.y, term1.y, term2.y, term3.y]),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn evaluation() {
|
||||
let p = Polynomial::new([1., 2., 3.]);
|
||||
|
||||
assert_eq!(p.eval(1.), 6.);
|
||||
assert_eq!(p.eval(2.), 17.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_change() {
|
||||
let p1 = Polynomial::new([1., 2., 3.]);
|
||||
let p2 = Polynomial::new([1., 2., 3., 0.]);
|
||||
|
||||
assert_eq!(p1.as_size(), Some(p2));
|
||||
assert_eq!(p2.as_size(), Some(p1));
|
||||
|
||||
assert_eq!(p2.as_size::<2>(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addition_and_subtaction() {
|
||||
let p1 = Polynomial::new([1., 2., 3.]);
|
||||
let p2 = Polynomial::new([4., 5., 6.]);
|
||||
|
||||
let addition = Polynomial::new([5., 7., 9.]);
|
||||
let subtraction = Polynomial::new([-3., -3., -3.]);
|
||||
|
||||
assert_eq!(&p1 + &p2, addition);
|
||||
assert_eq!(&p1 - &p2, subtraction);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiplication() {
|
||||
let p1 = Polynomial::new([1., 2., 3.]).as_size().unwrap();
|
||||
let p2 = Polynomial::new([4., 5., 6.]).as_size().unwrap();
|
||||
|
||||
let multiplication = Polynomial::new([4., 13., 28., 27., 18.]);
|
||||
|
||||
assert_eq!(&p1 * &p2, multiplication);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derivative_and_antiderivative() {
|
||||
let mut p = Polynomial::new([1., 2., 3.]);
|
||||
let p_deriv = Polynomial::new([2., 6., 0.]);
|
||||
|
||||
assert_eq!(p.derivative(), p_deriv);
|
||||
|
||||
p.coefficients_mut()[0] = 0.;
|
||||
assert_eq!(p_deriv.antiderivative().unwrap(), p);
|
||||
|
||||
assert_eq!(p.antiderivative(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let p = Polynomial::new([1., 2., 0., 3.]);
|
||||
|
||||
assert_eq!(format!("{p:.2}"), "3.00x^3 + 2.00x + 1.00");
|
||||
}
|
||||
}
|
||||
192
node-graph/libraries/core-types/src/math/quad.rs
Normal file
192
node-graph/libraries/core-types/src/math/quad.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
/// A quad defined by four vertices. Clockwise from the top left:
|
||||
///
|
||||
/// `top_left`, `top_right`, `bottom_right`, `bottom_left`.
|
||||
pub struct Quad(pub [DVec2; 4]);
|
||||
|
||||
impl Quad {
|
||||
/// Get the top left corner of the quad.
|
||||
pub fn top_left(&self) -> DVec2 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
/// Get the top right corner of the quad.
|
||||
pub fn top_right(&self) -> DVec2 {
|
||||
self.0[1]
|
||||
}
|
||||
|
||||
/// Get the bottom right corner of the quad.
|
||||
pub fn bottom_right(&self) -> DVec2 {
|
||||
self.0[2]
|
||||
}
|
||||
|
||||
/// Get the bottom left corner of the quad.
|
||||
pub fn bottom_left(&self) -> DVec2 {
|
||||
self.0[3]
|
||||
}
|
||||
|
||||
/// Create a zero-sized quad at the point.
|
||||
pub fn from_point(point: DVec2) -> Self {
|
||||
Self([point; 4])
|
||||
}
|
||||
|
||||
/// Convert a box defined by two corner points to a quad. The points must be given as `minimum (top left)` then `maximum (bottom right)`.
|
||||
pub fn from_box(bbox: [DVec2; 2]) -> Self {
|
||||
let size = bbox[1] - bbox[0];
|
||||
Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y])
|
||||
}
|
||||
|
||||
/// Create a quad from the center and offset (distance from center to middle of an edge)
|
||||
pub fn from_square(center: DVec2, offset: f64) -> Self {
|
||||
Self::from_box([center - offset, center + offset])
|
||||
}
|
||||
|
||||
/// Get all the edges in the quad.
|
||||
pub fn all_edges(&self) -> [[DVec2; 2]; 4] {
|
||||
[[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]]
|
||||
}
|
||||
|
||||
/// Get two edges as bases.
|
||||
pub fn edges(&self) -> [[DVec2; 2]; 2] {
|
||||
[[self.0[0], self.0[1]], [self.0[1], self.0[2]]]
|
||||
}
|
||||
|
||||
/// Returns true only if the width and height are both greater than or equal to the given width.
|
||||
pub fn all_sides_at_least_width(&self, width: f64) -> bool {
|
||||
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
|
||||
}
|
||||
|
||||
/// Generates the axis aligned bounding box of the quad
|
||||
pub fn bounding_box(&self) -> [DVec2; 2] {
|
||||
[
|
||||
self.0.into_iter().reduce(|a, b| a.min(b)).unwrap_or_default(),
|
||||
self.0.into_iter().reduce(|a, b| a.max(b)).unwrap_or_default(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Gets the center of a quad
|
||||
pub fn center(&self) -> DVec2 {
|
||||
self.0.iter().sum::<DVec2>() / 4.
|
||||
}
|
||||
|
||||
/// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points.
|
||||
pub fn combine_bounds(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
|
||||
[a[0].min(b[0]), a[1].max(b[1])]
|
||||
}
|
||||
|
||||
/// "Clip" bounds of `a` to the limits of `b`.
|
||||
pub fn clip(a: [DVec2; 2], b: [DVec2; 2]) -> [DVec2; 2] {
|
||||
[
|
||||
a[0].max(b[0]), // Constrain min corner
|
||||
a[1].min(b[1]), // Constrain max corner
|
||||
]
|
||||
}
|
||||
|
||||
/// Expand a quad by a certain amount on all sides.
|
||||
///
|
||||
/// Not currently very optimized
|
||||
pub fn inflate(&self, offset: f64) -> Quad {
|
||||
let offset = |index_before, index, index_after| {
|
||||
let [point_before, point, point_after]: [DVec2; 3] = [self.0[index_before], self.0[index], self.0[index_after]];
|
||||
let [line_in, line_out] = [point - point_before, point_after - point];
|
||||
let angle = line_in.angle_to(-line_out);
|
||||
let offset_length = offset / (std::f64::consts::FRAC_PI_2 - angle / 2.).cos();
|
||||
point + (line_in.perp().normalize_or_zero() + line_out.perp().normalize_or_zero()).normalize_or_zero() * offset_length
|
||||
};
|
||||
Self([offset(3, 0, 1), offset(0, 1, 2), offset(1, 2, 3), offset(2, 3, 0)])
|
||||
}
|
||||
|
||||
/// Does this quad contain a point
|
||||
///
|
||||
/// Code from https://wrfranklin.org/Research/Short_Notes/pnpoly.html
|
||||
pub fn contains(&self, p: DVec2) -> bool {
|
||||
let mut inside = false;
|
||||
for (i, j) in (0..4).zip([3, 0, 1, 2]) {
|
||||
if (self.0[i].y > p.y) != (self.0[j].y > p.y) && p.x < ((self.0[j].x - self.0[i].x) * (p.y - self.0[i].y) / (self.0[j].y - self.0[i].y) + self.0[i].x) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
|
||||
/// https://www.cs.rpi.edu/~cutler/classes/computationalgeometry/F23/lectures/02_line_segment_intersections.pdf
|
||||
fn line_intersection_t(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> (f64, f64) {
|
||||
let t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
|
||||
let u = ((a.x - c.x) * (a.y - b.y) - (a.y - c.y) * (a.x - b.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
|
||||
|
||||
(t, u)
|
||||
}
|
||||
|
||||
fn intersect_lines(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> Option<DVec2> {
|
||||
let (t, u) = Self::line_intersection_t(a, b, c, d);
|
||||
((0. ..=1.).contains(&t) && (0. ..=1.).contains(&u)).then(|| a + t * (b - a))
|
||||
}
|
||||
|
||||
pub fn intersect_rays(a: DVec2, a_direction: DVec2, b: DVec2, b_direction: DVec2) -> Option<DVec2> {
|
||||
let (t, u) = Self::line_intersection_t(a, a + a_direction, b, b + b_direction);
|
||||
(t.is_finite() && u.is_finite()).then(|| a + t * a_direction)
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: Quad) -> bool {
|
||||
let intersects = self
|
||||
.all_edges()
|
||||
.into_iter()
|
||||
.any(|[a, b]| other.all_edges().into_iter().any(|[c, d]| Self::intersect_lines(a, b, c, d).is_some()));
|
||||
self.contains(other.center()) || other.contains(self.center()) || intersects
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Quad> for DAffine2 {
|
||||
type Output = Quad;
|
||||
|
||||
fn mul(self, rhs: Quad) -> Self::Output {
|
||||
Quad(rhs.0.map(|point| self.transform_point2(point)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn offset_quad() {
|
||||
fn eq(a: Quad, b: Quad) -> bool {
|
||||
a.0.iter().zip(b.0).all(|(a, b)| a.abs_diff_eq(b, 0.0001))
|
||||
}
|
||||
|
||||
assert!(eq(Quad::from_box([DVec2::ZERO, DVec2::ONE]).inflate(0.5), Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])));
|
||||
assert!(eq(Quad::from_box([DVec2::ONE, DVec2::ZERO]).inflate(0.5), Quad::from_box([DVec2::splat(1.5), DVec2::splat(-0.5)])));
|
||||
assert!(eq(
|
||||
(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).inflate(0.5),
|
||||
DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::splat(-0.5), DVec2::splat(1.5)])
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn quad_contains() {
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::splat(0.5)));
|
||||
assert!(Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::splat(0.5)));
|
||||
assert!(Quad::from_box([DVec2::splat(300.), DVec2::splat(500.)]).contains(DVec2::splat(350.)));
|
||||
assert!((DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::new(-0.5, 0.5)));
|
||||
|
||||
assert!(!Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::new(1., 1.1)));
|
||||
assert!(!Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::new(0.5, -0.01)));
|
||||
assert!(!(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::splat(0.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_lines() {
|
||||
assert_eq!(
|
||||
Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)),
|
||||
Some(DVec2::new(2., 5.))
|
||||
);
|
||||
assert_eq!(Quad::intersect_lines(DVec2::new(4., 6.), DVec2::new(4., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)), None);
|
||||
assert_eq!(Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 9.)), None);
|
||||
}
|
||||
#[test]
|
||||
fn intersect_quad() {
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(7.)])));
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
|
||||
assert!(!Quad::from_box([DVec2::ZERO, DVec2::splat(3.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
|
||||
}
|
||||
}
|
||||
119
node-graph/libraries/core-types/src/math/rect.rs
Normal file
119
node-graph/libraries/core-types/src/math/rect.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::math::quad::Quad;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq)]
|
||||
/// An axis aligned rect defined by two vertices.
|
||||
pub struct Rect(pub [DVec2; 2]);
|
||||
|
||||
impl Rect {
|
||||
/// Create a zero sized quad at the point
|
||||
#[must_use]
|
||||
pub fn from_point(point: DVec2) -> Self {
|
||||
Self([point; 2])
|
||||
}
|
||||
|
||||
/// Convert a box defined by two corner points to a quad.
|
||||
#[must_use]
|
||||
pub fn from_box(bbox: [DVec2; 2]) -> Self {
|
||||
Self([bbox[0].min(bbox[1]), bbox[0].max(bbox[1])])
|
||||
}
|
||||
|
||||
/// Create a quad from the center and offset (distance from center to middle of an edge)
|
||||
#[must_use]
|
||||
pub fn from_square(center: DVec2, offset: f64) -> Self {
|
||||
Self::from_box([center - offset, center + offset])
|
||||
}
|
||||
|
||||
/// Create an AABB from an iter of points, returning None if empty.
|
||||
#[must_use]
|
||||
pub fn point_iter(points: impl Iterator<Item = DVec2>) -> Option<Self> {
|
||||
let mut bounds = None;
|
||||
for point in points {
|
||||
let bounds = bounds.get_or_insert(Self::from_point(point));
|
||||
bounds[0] = bounds[0].min(point);
|
||||
bounds[1] = bounds[1].max(point);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
/// Get all the edges in the rect.
|
||||
#[must_use]
|
||||
pub fn edges(&self) -> [[DVec2; 2]; 4] {
|
||||
let corners = [self[0], DVec2::new(self[0].x, self[1].y), self[1], DVec2::new(self[1].y, self[0].x)];
|
||||
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
|
||||
}
|
||||
|
||||
/// Gets the center of a rect
|
||||
#[must_use]
|
||||
pub fn center(&self) -> DVec2 {
|
||||
self.0.iter().sum::<DVec2>() / 2.
|
||||
}
|
||||
|
||||
/// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points.
|
||||
#[must_use]
|
||||
pub fn combine_bounds(a: Self, b: Self) -> Self {
|
||||
Self::from_box([a[0].min(b[0]), a[1].max(b[1])])
|
||||
}
|
||||
|
||||
/// Expand a rect by a certain amount on top/bottom and on left/right
|
||||
#[must_use]
|
||||
pub fn expand_by(&self, x: f64, y: f64) -> Self {
|
||||
let delta = DVec2::new(x, y);
|
||||
Self::from_box([self[0] - delta, self[1] + delta])
|
||||
}
|
||||
|
||||
/// Checks if two rects intersect
|
||||
#[must_use]
|
||||
pub fn intersects(&self, other: Self) -> bool {
|
||||
let [mina, maxa] = [self[0].min(self[1]), self[0].max(self[1])];
|
||||
let [minb, maxb] = [other[0].min(other[1]), other[0].max(other[1])];
|
||||
mina.x <= maxb.x && minb.x <= maxa.x && mina.y <= maxb.y && minb.y <= maxa.y
|
||||
}
|
||||
|
||||
/// Does this rect contain a point
|
||||
#[must_use]
|
||||
pub fn contains(&self, p: DVec2) -> bool {
|
||||
(self[0].x < p.x && p.x < self[1].x) && (self[0].y < p.y && p.y < self[1].y)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn min(&self) -> DVec2 {
|
||||
self.0[0].min(self.0[1])
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn max(&self) -> DVec2 {
|
||||
self.0[0].max(self.0[1])
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn translate(&self, offset: DVec2) -> Self {
|
||||
Self([self.0[0] + offset, self.0[1] + offset])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Rect> for DAffine2 {
|
||||
type Output = Quad;
|
||||
|
||||
fn mul(self, rhs: Rect) -> Self::Output {
|
||||
self * Quad::from_box(rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for Rect {
|
||||
type Output = DVec2;
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
impl std::ops::IndexMut<usize> for Rect {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Quad {
|
||||
fn from(val: Rect) -> Self {
|
||||
Quad::from_box(val.0)
|
||||
}
|
||||
}
|
||||
105
node-graph/libraries/core-types/src/memo.rs
Normal file
105
node-graph/libraries/core-types/src/memo.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use std::hash::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Stores both what a node was called with and what it returned.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IORecord<I, O> {
|
||||
pub input: I,
|
||||
pub output: O,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
pub struct MemoHash<T: Hash> {
|
||||
hash: u64,
|
||||
value: Arc<T>,
|
||||
}
|
||||
|
||||
impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
T::deserialize(deserializer).map(|value| Self::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> MemoHash<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
let hash = Self::calc_hash(&value);
|
||||
Self { hash, value: value.into() }
|
||||
}
|
||||
pub fn new_with_hash(value: T, hash: u64) -> Self {
|
||||
Self { hash, value: value.into() }
|
||||
}
|
||||
|
||||
fn calc_hash(data: &T) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
data.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> MemoHashGuard<'_, T> {
|
||||
MemoHashGuard { inner: self }
|
||||
}
|
||||
pub fn into_inner(self) -> Arc<T> {
|
||||
self.value
|
||||
}
|
||||
pub fn hash_code(&self) -> u64 {
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
impl<T: Hash> From<T> for MemoHash<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for MemoHash<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.hash.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Deref for MemoHash<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoHashGuard<'a, T: Hash> {
|
||||
inner: &'a mut MemoHash<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> Deref for MemoHashGuard<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + Clone> std::ops::DerefMut for MemoHashGuard<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
Arc::make_mut(&mut self.inner.value)
|
||||
}
|
||||
}
|
||||
89
node-graph/libraries/core-types/src/misc.rs
Normal file
89
node-graph/libraries/core-types/src/misc.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
// TODO(TrueDoctor): Replace this with the more idiomatic approach instead of using `trait Clampable`.
|
||||
|
||||
/// A trait for types that can be clamped within a min/max range defined by f64.
|
||||
pub trait Clampable: Sized {
|
||||
/// Clamps the value to be no less than `min`.
|
||||
fn clamp_hard_min(self, min: f64) -> Self;
|
||||
/// Clamps the value to be no more than `max`.
|
||||
fn clamp_hard_max(self, max: f64) -> Self;
|
||||
}
|
||||
|
||||
// Implement for common numeric types
|
||||
macro_rules! impl_clampable_float {
|
||||
($($ty:ty),*) => {
|
||||
$(
|
||||
impl Clampable for $ty {
|
||||
#[inline(always)]
|
||||
fn clamp_hard_min(self, min: f64) -> Self {
|
||||
self.max(min as $ty)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn clamp_hard_max(self, max: f64) -> Self {
|
||||
self.min(max as $ty)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
impl_clampable_float!(f32, f64);
|
||||
|
||||
macro_rules! impl_clampable_int {
|
||||
($($ty:ty),*) => {
|
||||
$(
|
||||
impl Clampable for $ty {
|
||||
#[inline(always)]
|
||||
fn clamp_hard_min(self, min: f64) -> Self {
|
||||
// Using try_from to handle potential range issues safely, though min should ideally be valid.
|
||||
// Consider using a different approach if f64 precision vs integer range is a concern.
|
||||
<$ty>::try_from(min.ceil() as i64).ok().map_or(self, |min_val| self.max(min_val))
|
||||
}
|
||||
#[inline(always)]
|
||||
fn clamp_hard_max(self, max: f64) -> Self {
|
||||
<$ty>::try_from(max.floor() as i64).ok().map_or(self, |max_val| self.min(max_val))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
// Add relevant integer types (adjust as needed)
|
||||
impl_clampable_int!(u32, u64, i32, i64);
|
||||
|
||||
// Implement for DVec2 (component-wise clamping)
|
||||
use glam::DVec2;
|
||||
impl Clampable for DVec2 {
|
||||
#[inline(always)]
|
||||
fn clamp_hard_min(self, min: f64) -> Self {
|
||||
self.max(DVec2::splat(min))
|
||||
}
|
||||
#[inline(always)]
|
||||
fn clamp_hard_max(self, max: f64) -> Self {
|
||||
self.min(DVec2::splat(max))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_color<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<crate::table::Table<no_std_types::color::Color>, D::Error> {
|
||||
use crate::table::Table;
|
||||
use no_std_types::color::Color;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ColorFormat {
|
||||
Color(Color),
|
||||
OptionalColor(Option<Color>),
|
||||
ColorTable(Table<Color>),
|
||||
}
|
||||
|
||||
Ok(match ColorFormat::deserialize(deserializer)? {
|
||||
ColorFormat::Color(color) => Table::new_from_element(color),
|
||||
ColorFormat::OptionalColor(color) => {
|
||||
if let Some(color) = color {
|
||||
Table::new_from_element(color)
|
||||
} else {
|
||||
Table::new()
|
||||
}
|
||||
}
|
||||
ColorFormat::ColorTable(color_table) => color_table,
|
||||
})
|
||||
}
|
||||
118
node-graph/libraries/core-types/src/ops.rs
Normal file
118
node-graph/libraries/core-types/src/ops.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::{
|
||||
Node,
|
||||
table::{Table, TableRow},
|
||||
transform::Footprint,
|
||||
};
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// Type
|
||||
// TODO: Document this
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct TypeNode<N: for<'a> Node<'a, I>, I, O>(pub N, pub PhantomData<(I, O)>);
|
||||
impl<'i, N, I: 'i, O: 'i> Node<'i, I> for TypeNode<N, I, O>
|
||||
where
|
||||
N: for<'n> Node<'n, I, Output = O>,
|
||||
{
|
||||
type Output = O;
|
||||
fn eval(&'i self, input: I) -> Self::Output {
|
||||
self.0.eval(input)
|
||||
}
|
||||
|
||||
fn reset(&self) {
|
||||
self.0.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.0.serialize()
|
||||
}
|
||||
}
|
||||
impl<'i, N: for<'a> Node<'a, I>, I: 'i> TypeNode<N, I, <N as Node<'i, I>>::Output> {
|
||||
pub fn new(node: N) -> Self {
|
||||
Self(node, PhantomData)
|
||||
}
|
||||
}
|
||||
impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode<N, I, <N as Node<'i, I>>::Output> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone(), self.1)
|
||||
}
|
||||
}
|
||||
impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Node<'i, I>>::Output> {}
|
||||
|
||||
/// 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, C>: Sized {
|
||||
/// Converts this type into the (usually inferred) output type.
|
||||
#[must_use]
|
||||
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
|
||||
}
|
||||
|
||||
impl<T: ToString + Send> Convert<String, ()> for T {
|
||||
/// Converts this type into a `String` using its `ToString` implementation.
|
||||
#[inline]
|
||||
async fn convert(self, _: Footprint, _converter: ()) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// trait mentioning inner type in args
|
||||
pub trait TableConvert<U> {
|
||||
fn convert_row(self) -> U;
|
||||
}
|
||||
|
||||
//
|
||||
impl<U, T: TableConvert<U> + Send> Convert<Table<U>, ()> for Table<T> {
|
||||
async fn convert(self, _: Footprint, _: ()) -> Table<U> {
|
||||
let table: Table<U> = self
|
||||
.into_iter()
|
||||
.map(|row| TableRow {
|
||||
element: row.element.convert_row(),
|
||||
transform: row.transform,
|
||||
alpha_blending: row.alpha_blending,
|
||||
source_node_id: row.source_node_id,
|
||||
})
|
||||
.collect();
|
||||
table
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
async fn convert(self, _: Footprint, _: ()) -> $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);
|
||||
287
node-graph/libraries/core-types/src/registry.rs
Normal file
287
node-graph/libraries/core-types/src/registry.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use crate::{ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
pub use no_std_types::registry::types;
|
||||
|
||||
// Translation struct between macro and definition
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NodeMetadata {
|
||||
pub display_name: &'static str,
|
||||
pub category: Option<&'static str>,
|
||||
pub fields: Vec<FieldMetadata>,
|
||||
pub description: &'static str,
|
||||
pub properties: Option<&'static str>,
|
||||
pub context_features: Vec<ContextFeature>,
|
||||
}
|
||||
|
||||
// Translation struct between macro and definition
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FieldMetadata {
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub exposed: bool,
|
||||
pub widget_override: RegistryWidgetOverride,
|
||||
pub value_source: RegistryValueSource,
|
||||
pub default_type: Option<Type>,
|
||||
pub number_min: Option<f64>,
|
||||
pub number_max: Option<f64>,
|
||||
pub number_mode_range: Option<(f64, f64)>,
|
||||
pub number_display_decimal_places: Option<u32>,
|
||||
pub number_step: Option<f64>,
|
||||
pub unit: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RegistryWidgetOverride {
|
||||
None,
|
||||
Hidden,
|
||||
String(&'static str),
|
||||
Custom(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RegistryValueSource {
|
||||
None,
|
||||
Default(&'static str),
|
||||
Scope(&'static str),
|
||||
}
|
||||
|
||||
type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<(NodeConstructor, NodeIOTypes)>>>>;
|
||||
|
||||
pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
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_family = "wasm"))]
|
||||
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type Any<'n> = Box<dyn DynAny<'n> + 'n>;
|
||||
pub type FutureAny<'n> = DynFuture<'n, Any<'n>>;
|
||||
// TODO: is this safe? This is assumed to be send+sync.
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n + Send + Sync;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n;
|
||||
pub type TypeErasedPinnedRef<'n> = Pin<&'n TypeErasedNode<'n>>;
|
||||
pub type TypeErasedRef<'n> = &'n TypeErasedNode<'n>;
|
||||
pub type TypeErasedBox<'n> = Box<TypeErasedNode<'n>>;
|
||||
pub type TypeErasedPinned<'n> = Pin<Box<TypeErasedNode<'n>>>;
|
||||
|
||||
pub type SharedNodeContainer = std::sync::Arc<NodeContainer>;
|
||||
|
||||
pub type NodeConstructor = fn(Vec<SharedNodeContainer>) -> DynFuture<'static, TypeErasedBox<'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NodeContainer {
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
pub node: *const TypeErasedNode<'static>,
|
||||
#[cfg(not(feature = "dealloc_nodes"))]
|
||||
pub node: TypeErasedRef<'static>,
|
||||
}
|
||||
|
||||
impl Deref for NodeContainer {
|
||||
type Target = TypeErasedNode<'static>;
|
||||
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe { &*(self.node) }
|
||||
#[cfg(not(feature = "dealloc_nodes"))]
|
||||
self.node
|
||||
}
|
||||
#[cfg(not(feature = "dealloc_nodes"))]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.node
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Marks NodeContainer as Sync. This dissallows the use of threadlocal storage for nodes as this would invalidate references to them.
|
||||
// TODO: implement this on a higher level wrapper to avoid missuse
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
unsafe impl Send for NodeContainer {}
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
unsafe impl Sync for NodeContainer {}
|
||||
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
impl Drop for NodeContainer {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.dealloc_unchecked() }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NodeContainer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NodeContainer").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeContainer {
|
||||
pub fn new(node: TypeErasedBox<'static>) -> SharedNodeContainer {
|
||||
let node = Box::leak(node);
|
||||
Self { node }.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "dealloc_nodes")]
|
||||
unsafe fn dealloc_unchecked(&mut self) {
|
||||
unsafe {
|
||||
drop(Box::from_raw(self.node as *mut TypeErasedNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Boxes the input and downcasts the output.
|
||||
/// Wraps around a node taking Box<dyn DynAny> and returning Box<dyn DynAny>
|
||||
#[derive(Clone)]
|
||||
pub struct DowncastBothNode<I, O> {
|
||||
node: SharedNodeContainer,
|
||||
_i: PhantomData<I>,
|
||||
_o: PhantomData<O>,
|
||||
}
|
||||
impl<'input, O, I> Node<'input, I> for DowncastBothNode<I, O>
|
||||
where
|
||||
O: 'input + StaticType + WasmNotSend,
|
||||
I: 'input + StaticType + WasmNotSend,
|
||||
{
|
||||
type Output = DynFuture<'input, O>;
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
fn eval(&'input self, input: I) -> Self::Output {
|
||||
{
|
||||
let node_name = self.node.node_name();
|
||||
let input = Box::new(input);
|
||||
let future = self.node.eval(input);
|
||||
Box::pin(async move {
|
||||
let out = dyn_any::downcast(future.await).unwrap_or_else(|e| panic!("DowncastBothNode wrong output type: {e} in: \n{node_name}"));
|
||||
*out
|
||||
})
|
||||
}
|
||||
}
|
||||
fn reset(&self) {
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
impl<I, O> DowncastBothNode<I, O> {
|
||||
pub const fn new(node: SharedNodeContainer) -> Self {
|
||||
Self {
|
||||
node,
|
||||
_i: PhantomData,
|
||||
_o: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct FutureWrapperNode<Node> {
|
||||
node: Node,
|
||||
}
|
||||
|
||||
impl<'i, T: 'i + WasmNotSend, N> Node<'i, T> for FutureWrapperNode<N>
|
||||
where
|
||||
N: Node<'i, T, Output: WasmNotSend> + WasmNotSend,
|
||||
{
|
||||
type Output = DynFuture<'i, N::Output>;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, input: T) -> Self::Output {
|
||||
let result = self.node.eval(input);
|
||||
Box::pin(async move { result })
|
||||
}
|
||||
#[inline(always)]
|
||||
fn reset(&self) {
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> FutureWrapperNode<N> {
|
||||
pub const fn new(node: N) -> Self {
|
||||
Self { node }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DynAnyNode<I, O, Node> {
|
||||
node: Node,
|
||||
_i: PhantomData<I>,
|
||||
_o: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode<I, O, N>
|
||||
where
|
||||
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 = std::any::type_name::<N>();
|
||||
let output = |input| {
|
||||
let result = self.node.eval(input);
|
||||
async move { Box::new(result.await) as Any<'input> }
|
||||
};
|
||||
match dyn_any::downcast(input) {
|
||||
Ok(input) => Box::pin(output(*input)),
|
||||
Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&self) {
|
||||
self.node.reset();
|
||||
}
|
||||
|
||||
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 + StaticType,
|
||||
O: 'input + StaticType,
|
||||
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
|
||||
{
|
||||
pub const fn new(node: N) -> Self {
|
||||
Self {
|
||||
node,
|
||||
_i: PhantomData,
|
||||
_o: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct PanicNode<I: WasmNotSend, O: WasmNotSend>(PhantomData<I>, PhantomData<O>);
|
||||
|
||||
impl<'i, I: 'i + WasmNotSend, O: 'i + WasmNotSend> Node<'i, I> for PanicNode<I, O> {
|
||||
type Output = O;
|
||||
fn eval(&'i self, _: I) -> Self::Output {
|
||||
unimplemented!("This node should never be evaluated")
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: WasmNotSend, O: WasmNotSend> PanicNode<I, O> {
|
||||
pub const fn new() -> Self {
|
||||
Self(PhantomData, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: WasmNotSend, O: WasmNotSend> Default for PanicNode<I, O> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Evaluate safety
|
||||
unsafe impl<I: WasmNotSend, O: WasmNotSend> Sync for PanicNode<I, O> {}
|
||||
21
node-graph/libraries/core-types/src/render_complexity.rs
Normal file
21
node-graph/libraries/core-types/src/render_complexity.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Raster types moved to raster-types crate
|
||||
use crate::Color;
|
||||
use crate::table::Table;
|
||||
|
||||
pub trait RenderComplexity {
|
||||
fn render_complexity(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RenderComplexity> RenderComplexity for Table<T> {
|
||||
fn render_complexity(&self) -> usize {
|
||||
self.iter().map(|row| row.element.render_complexity()).fold(0, usize::saturating_add)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Color {
|
||||
fn render_complexity(&self) -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
324
node-graph/libraries/core-types/src/table.rs
Normal file
324
node-graph/libraries/core-types/src/table.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use crate::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use crate::transform::ApplyTransform;
|
||||
use crate::uuid::NodeId;
|
||||
use crate::{AlphaBlending, math::quad::Quad};
|
||||
use dyn_any::{StaticType, StaticTypeSized};
|
||||
use glam::DAffine2;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Table<T> {
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
transform: Vec<DAffine2>,
|
||||
alpha_blending: Vec<AlphaBlending>,
|
||||
source_node_id: Vec<Option<NodeId>>,
|
||||
}
|
||||
|
||||
impl<T> Table<T> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
element: Vec::with_capacity(capacity),
|
||||
transform: Vec::with_capacity(capacity),
|
||||
alpha_blending: Vec::with_capacity(capacity),
|
||||
source_node_id: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_element(element: T) -> Self {
|
||||
Self {
|
||||
element: vec![element],
|
||||
transform: vec![DAffine2::IDENTITY],
|
||||
alpha_blending: vec![AlphaBlending::default()],
|
||||
source_node_id: vec![None],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_row(row: TableRow<T>) -> Self {
|
||||
Self {
|
||||
element: vec![row.element],
|
||||
transform: vec![row.transform],
|
||||
alpha_blending: vec![row.alpha_blending],
|
||||
source_node_id: vec![row.source_node_id],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, row: TableRow<T>) {
|
||||
self.element.push(row.element);
|
||||
self.transform.push(row.transform);
|
||||
self.alpha_blending.push(row.alpha_blending);
|
||||
self.source_node_id.push(row.source_node_id);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, table: Table<T>) {
|
||||
self.element.extend(table.element);
|
||||
self.transform.extend(table.transform);
|
||||
self.alpha_blending.extend(table.alpha_blending);
|
||||
self.source_node_id.extend(table.source_node_id);
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<TableRowRef<'_, T>> {
|
||||
if index >= self.element.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(TableRowRef {
|
||||
element: &self.element[index],
|
||||
transform: &self.transform[index],
|
||||
alpha_blending: &self.alpha_blending[index],
|
||||
source_node_id: &self.source_node_id[index],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<TableRowMut<'_, T>> {
|
||||
if index >= self.element.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(TableRowMut {
|
||||
element: &mut self.element[index],
|
||||
transform: &mut self.transform[index],
|
||||
alpha_blending: &mut self.alpha_blending[index],
|
||||
source_node_id: &mut self.source_node_id[index],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.element.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.element.is_empty()
|
||||
}
|
||||
|
||||
/// Borrows a [`Table`] and returns an iterator of [`TableRowRef`]s, each containing references to the data of the respective row from the table.
|
||||
pub fn iter(&self) -> impl DoubleEndedIterator<Item = TableRowRef<'_, T>> + Clone {
|
||||
self.element
|
||||
.iter()
|
||||
.zip(self.transform.iter())
|
||||
.zip(self.alpha_blending.iter())
|
||||
.zip(self.source_node_id.iter())
|
||||
.map(|(((element, transform), alpha_blending), source_node_id)| TableRowRef {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Mutably borrows a [`Table`] and returns an iterator of [`TableRowMut`]s, each containing mutable references to the data of the respective row from the table.
|
||||
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = TableRowMut<'_, T>> {
|
||||
self.element
|
||||
.iter_mut()
|
||||
.zip(self.transform.iter_mut())
|
||||
.zip(self.alpha_blending.iter_mut())
|
||||
.zip(self.source_node_id.iter_mut())
|
||||
.map(|(((element, transform), alpha_blending), source_node_id)| TableRowMut {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BoundingBox> BoundingBox for Table<T> {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
let mut combined_bounds = None;
|
||||
|
||||
for row in self.iter() {
|
||||
match row.element.bounding_box(transform * *row.transform, include_stroke) {
|
||||
RenderBoundingBox::None => continue,
|
||||
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
|
||||
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
||||
Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)),
|
||||
None => combined_bounds = Some(bounds),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
match combined_bounds {
|
||||
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for Table<T> {
|
||||
type Item = TableRow<T>;
|
||||
type IntoIter = TableRowIter<T>;
|
||||
|
||||
/// Consumes a [`Table`] and returns an iterator of [`TableRow`]s, each containing the owned data of the respective row from the original table.
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
TableRowIter {
|
||||
element: self.element.into_iter(),
|
||||
transform: self.transform.into_iter(),
|
||||
alpha_blending: self.alpha_blending.into_iter(),
|
||||
source_node_id: self.source_node_id.into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableRowIter<T> {
|
||||
element: std::vec::IntoIter<T>,
|
||||
transform: std::vec::IntoIter<DAffine2>,
|
||||
alpha_blending: std::vec::IntoIter<AlphaBlending>,
|
||||
source_node_id: std::vec::IntoIter<Option<NodeId>>,
|
||||
}
|
||||
impl<T> Iterator for TableRowIter<T> {
|
||||
type Item = TableRow<T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let element = self.element.next()?;
|
||||
let transform = self.transform.next()?;
|
||||
let alpha_blending = self.alpha_blending.next()?;
|
||||
let source_node_id = self.source_node_id.next()?;
|
||||
|
||||
Some(TableRow {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Table<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
element: Vec::new(),
|
||||
transform: Vec::new(),
|
||||
alpha_blending: Vec::new(),
|
||||
source_node_id: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash> Hash for Table<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
for element in &self.element {
|
||||
element.hash(state);
|
||||
}
|
||||
for transform in &self.transform {
|
||||
transform.to_cols_array().map(|x| x.to_bits()).hash(state);
|
||||
}
|
||||
for alpha_blending in &self.alpha_blending {
|
||||
alpha_blending.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Table<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.element == other.element && self.transform == other.transform && self.alpha_blending == other.alpha_blending
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ApplyTransform for Table<T> {
|
||||
fn apply_transform(&mut self, modification: &DAffine2) {
|
||||
for transform in &mut self.transform {
|
||||
*transform *= *modification;
|
||||
}
|
||||
}
|
||||
|
||||
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
||||
for transform in &mut self.transform {
|
||||
*transform = *modification * *transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: StaticTypeSized> StaticType for Table<T> {
|
||||
type Static = Table<T::Static>;
|
||||
}
|
||||
|
||||
impl<T> FromIterator<TableRow<T>> for Table<T> {
|
||||
fn from_iter<I: IntoIterator<Item = TableRow<T>>>(iter: I) -> Self {
|
||||
let iter = iter.into_iter();
|
||||
let (lower, _) = iter.size_hint();
|
||||
let mut table = Self::with_capacity(lower);
|
||||
for row in iter {
|
||||
table.push(row);
|
||||
}
|
||||
table
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TableRow<T> {
|
||||
#[serde(alias = "instance")]
|
||||
pub element: T,
|
||||
pub transform: DAffine2,
|
||||
pub alpha_blending: AlphaBlending,
|
||||
pub source_node_id: Option<NodeId>,
|
||||
}
|
||||
|
||||
impl<T> TableRow<T> {
|
||||
pub fn new_from_element(element: T) -> Self {
|
||||
Self {
|
||||
element,
|
||||
transform: DAffine2::IDENTITY,
|
||||
alpha_blending: AlphaBlending::default(),
|
||||
source_node_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_ref(&self) -> TableRowRef<'_, T> {
|
||||
TableRowRef {
|
||||
element: &self.element,
|
||||
transform: &self.transform,
|
||||
alpha_blending: &self.alpha_blending,
|
||||
source_node_id: &self.source_node_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_mut(&mut self) -> TableRowMut<'_, T> {
|
||||
TableRowMut {
|
||||
element: &mut self.element,
|
||||
transform: &mut self.transform,
|
||||
alpha_blending: &mut self.alpha_blending,
|
||||
source_node_id: &mut self.source_node_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct TableRowRef<'a, T> {
|
||||
pub element: &'a T,
|
||||
pub transform: &'a DAffine2,
|
||||
pub alpha_blending: &'a AlphaBlending,
|
||||
pub source_node_id: &'a Option<NodeId>,
|
||||
}
|
||||
|
||||
impl<T> TableRowRef<'_, T> {
|
||||
pub fn into_cloned(self) -> TableRow<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
TableRow {
|
||||
element: self.element.clone(),
|
||||
transform: *self.transform,
|
||||
alpha_blending: *self.alpha_blending,
|
||||
source_node_id: *self.source_node_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TableRowMut<'a, T> {
|
||||
pub element: &'a mut T,
|
||||
pub transform: &'a mut DAffine2,
|
||||
pub alpha_blending: &'a mut AlphaBlending,
|
||||
pub source_node_id: &'a mut Option<NodeId>,
|
||||
}
|
||||
|
||||
// Conversion from Table<Color> to Option<Color> - extracts first element
|
||||
impl From<Table<crate::Color>> for Option<crate::Color> {
|
||||
fn from(table: Table<crate::Color>) -> Self {
|
||||
table.iter().nth(0).map(|row| row.element).copied()
|
||||
}
|
||||
}
|
||||
59
node-graph/libraries/core-types/src/text.rs
Normal file
59
node-graph/libraries/core-types/src/text.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
mod font_cache;
|
||||
mod path_builder;
|
||||
mod text_context;
|
||||
mod to_path;
|
||||
|
||||
use dyn_any::DynAny;
|
||||
pub use font_cache::*;
|
||||
pub use text_context::TextContext;
|
||||
pub use to_path::*;
|
||||
|
||||
/// Alignment of lines of type within a text block.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum TextAlign {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
#[label("Justify")]
|
||||
JustifyLeft,
|
||||
// TODO: JustifyCenter, JustifyRight, JustifyAll
|
||||
}
|
||||
|
||||
impl From<TextAlign> for parley::Alignment {
|
||||
fn from(val: TextAlign) -> Self {
|
||||
match val {
|
||||
TextAlign::Left => parley::Alignment::Left,
|
||||
TextAlign::Center => parley::Alignment::Middle,
|
||||
TextAlign::Right => parley::Alignment::Right,
|
||||
TextAlign::JustifyLeft => parley::Alignment::Justified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TypesettingConfig {
|
||||
pub font_size: f64,
|
||||
pub line_height_ratio: f64,
|
||||
pub character_spacing: f64,
|
||||
pub max_width: Option<f64>,
|
||||
pub max_height: Option<f64>,
|
||||
pub tilt: f64,
|
||||
pub align: TextAlign,
|
||||
}
|
||||
|
||||
impl Default for TypesettingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
172
node-graph/libraries/core-types/src/transform.rs
Normal file
172
node-graph/libraries/core-types/src/transform.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use crate::math::bbox::AxisAlignedBbox;
|
||||
use core::f64;
|
||||
use glam::{DAffine2, DMat2, DVec2, UVec2};
|
||||
|
||||
pub trait Transform {
|
||||
fn transform(&self) -> DAffine2;
|
||||
|
||||
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
|
||||
pivot
|
||||
}
|
||||
|
||||
fn decompose_scale(&self) -> DVec2 {
|
||||
DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length())
|
||||
}
|
||||
|
||||
/// Requires that the transform does not contain any skew.
|
||||
fn decompose_rotation(&self) -> f64 {
|
||||
let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2;
|
||||
let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X);
|
||||
if rotation == -0. { 0. } else { rotation }
|
||||
}
|
||||
|
||||
/// Detects if the transform contains skew by checking if the transformation matrix
|
||||
/// deviates from a pure rotation + uniform scale + translation.
|
||||
///
|
||||
/// Returns true if the matrix columns are not orthogonal or have different lengths,
|
||||
/// indicating the presence of skew or non-uniform scaling.
|
||||
fn has_skew(&self) -> bool {
|
||||
let mat2 = self.transform().matrix2;
|
||||
let col0 = mat2.x_axis;
|
||||
let col1 = mat2.y_axis;
|
||||
|
||||
const EPSILON: f64 = 1e-10;
|
||||
|
||||
// Check if columns are orthogonal (dot product should be ~0) and equal length
|
||||
// Non-orthogonal columns or different lengths indicate skew/non-uniform scaling
|
||||
col0.dot(col1).abs() > EPSILON || (col0.length() - col1.length()).abs() > EPSILON
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TransformMut: Transform {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2;
|
||||
fn translate(&mut self, offset: DVec2) {
|
||||
*self.transform_mut() = DAffine2::from_translation(offset) * self.transform();
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation for references to anything that implements Transform
|
||||
impl<T: Transform> Transform for &T {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
(*self).transform()
|
||||
}
|
||||
}
|
||||
|
||||
// Implementations for DAffine2
|
||||
impl Transform for DAffine2 {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl TransformMut for DAffine2 {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Implementations for Footprint
|
||||
impl Transform for Footprint {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
self.transform
|
||||
}
|
||||
}
|
||||
impl TransformMut for Footprint {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RenderQuality {
|
||||
/// Low quality, fast rendering
|
||||
Preview,
|
||||
/// Ensure that the render is available with at least the specified quality
|
||||
/// A value of 0.5 means that the render is available with at least 50% of the final image resolution
|
||||
Scale(f32),
|
||||
/// Flip a coin to decide if the render should be available with the current quality or done at full quality
|
||||
/// This should be used to gradually update the render quality of a cached node
|
||||
Probability(f32),
|
||||
/// Render at full quality
|
||||
Full,
|
||||
}
|
||||
#[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,
|
||||
/// Resolution of the target output area in pixels
|
||||
pub resolution: UVec2,
|
||||
/// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient
|
||||
pub quality: RenderQuality,
|
||||
}
|
||||
|
||||
impl Default for Footprint {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl Footprint {
|
||||
pub const DEFAULT: Self = Self {
|
||||
transform: DAffine2::IDENTITY,
|
||||
resolution: UVec2::new(1920, 1080),
|
||||
quality: RenderQuality::Full,
|
||||
};
|
||||
|
||||
pub const BOUNDLESS: Self = Self {
|
||||
transform: DAffine2 {
|
||||
matrix2: DMat2::from_diagonal(DVec2::splat(f64::INFINITY)),
|
||||
translation: DVec2::ZERO,
|
||||
},
|
||||
resolution: UVec2::ZERO,
|
||||
quality: RenderQuality::Full,
|
||||
};
|
||||
|
||||
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
|
||||
let inverse = self.transform.inverse();
|
||||
let start = inverse.transform_point2((0., 0.).into());
|
||||
let end = inverse.transform_point2(self.resolution.as_dvec2());
|
||||
AxisAlignedBbox { start, end }
|
||||
}
|
||||
|
||||
pub fn scale(&self) -> DVec2 {
|
||||
self.transform.decompose_scale()
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> DVec2 {
|
||||
self.transform.transform_point2(DVec2::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for Footprint {
|
||||
fn from(_: ()) -> Self {
|
||||
Footprint::default()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ApplyTransform {
|
||||
fn apply_transform(&mut self, modification: &DAffine2);
|
||||
fn left_apply_transform(&mut self, modification: &DAffine2);
|
||||
}
|
||||
impl<T: TransformMut> ApplyTransform for T {
|
||||
fn apply_transform(&mut self, &modification: &DAffine2) {
|
||||
*self.transform_mut() = self.transform() * modification
|
||||
}
|
||||
fn left_apply_transform(&mut self, &modification: &DAffine2) {
|
||||
*self.transform_mut() = modification * self.transform()
|
||||
}
|
||||
}
|
||||
impl ApplyTransform for DVec2 {
|
||||
fn apply_transform(&mut self, modification: &DAffine2) {
|
||||
*self = modification.transform_point2(*self);
|
||||
}
|
||||
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
||||
*self = modification.inverse().transform_point2(*self);
|
||||
}
|
||||
}
|
||||
391
node-graph/libraries/core-types/src/types.rs
Normal file
391
node-graph/libraries/core-types/src/types.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use std::borrow::Cow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! concrete {
|
||||
($type:ty) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
|
||||
alias: None,
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
($type:ty, $name:ty) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed(std::any::type_name::<$type>()),
|
||||
alias: Some($crate::Cow::Borrowed(stringify!($name))),
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! concrete_with_name {
|
||||
($type:ty, $name:expr_2021) => {
|
||||
$crate::Type::Concrete($crate::TypeDescriptor {
|
||||
id: Some(std::any::TypeId::of::<$type>()),
|
||||
name: $crate::Cow::Borrowed($name),
|
||||
alias: None,
|
||||
size: std::mem::size_of::<$type>(),
|
||||
align: std::mem::align_of::<$type>(),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! generic {
|
||||
($type:ty) => {{ $crate::Type::Generic($crate::Cow::Borrowed(stringify!($type))) }};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! future {
|
||||
($type:ty) => {{ $crate::Type::Future(Box::new(concrete!($type))) }};
|
||||
($type:ty, $name:ty) => {
|
||||
$crate::Type::Future(Box::new(concrete!($type, $name)))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fn_type {
|
||||
($type:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!(())), Box::new(concrete!($type)))
|
||||
};
|
||||
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type, $outname)))
|
||||
};
|
||||
($in_type:ty, $type:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type)))
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
macro_rules! fn_type_fut {
|
||||
($type:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type)))
|
||||
};
|
||||
($in_type:ty, $type:ty, alias: $outname:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type, $outname)))
|
||||
};
|
||||
($in_type:ty, $type:ty) => {
|
||||
$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type)))
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodeIOTypes {
|
||||
pub call_argument: Type,
|
||||
pub return_value: Type,
|
||||
pub inputs: Vec<Type>,
|
||||
}
|
||||
|
||||
impl NodeIOTypes {
|
||||
pub const fn new(call_argument: Type, return_value: Type, inputs: Vec<Type>) -> Self {
|
||||
Self { call_argument, return_value, inputs }
|
||||
}
|
||||
|
||||
pub const fn empty() -> Self {
|
||||
let tds1 = TypeDescriptor {
|
||||
id: None,
|
||||
name: Cow::Borrowed("()"),
|
||||
alias: None,
|
||||
size: 0,
|
||||
align: 0,
|
||||
};
|
||||
let tds2 = TypeDescriptor {
|
||||
id: None,
|
||||
name: Cow::Borrowed("()"),
|
||||
alias: None,
|
||||
size: 0,
|
||||
align: 0,
|
||||
};
|
||||
Self {
|
||||
call_argument: Type::Concrete(tds1),
|
||||
return_value: Type::Concrete(tds2),
|
||||
inputs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ty(&self) -> Type {
|
||||
Type::Fn(Box::new(self.call_argument.clone()), Box::new(self.return_value.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
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(", "),
|
||||
self.return_value
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProtoNodeIdentifier {
|
||||
pub name: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl From<String> for ProtoNodeIdentifier {
|
||||
fn from(value: String) -> Self {
|
||||
Self { name: Cow::Owned(value) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for ProtoNodeIdentifier {
|
||||
fn from(s: &'static str) -> Self {
|
||||
ProtoNodeIdentifier { name: Cow::Borrowed(s) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoNodeIdentifier {
|
||||
pub const fn new(name: &'static str) -> Self {
|
||||
ProtoNodeIdentifier { name: Cow::Borrowed(name) }
|
||||
}
|
||||
|
||||
pub const fn with_owned_string(name: String) -> Self {
|
||||
ProtoNodeIdentifier { name: Cow::Owned(name) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ProtoNodeIdentifier {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.name.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ProtoNodeIdentifier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("ProtoNodeIdentifier").field(&self.name).finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Cow<'static, str>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
let name = String::deserialize(deserializer)?;
|
||||
let name = match name.as_str() {
|
||||
"f32" => "f64".to_string(),
|
||||
"grahpene_core::transform::Footprint" => "std::option::Option<std::sync::Arc<grahpene_core::context::OwnedContextImpl>>".to_string(),
|
||||
"grahpene_core::graphic_element::GraphicGroup" => "grahpene_core::table::Table<grahpene_core::graphic_types::Graphic>".to_string(),
|
||||
"grahpene_core::raster::image::ImageFrame<Color>"
|
||||
| "grahpene_core::raster::image::ImageFrame<grahpene_core::raster::color::Color>"
|
||||
| "grahpene_core::instances::Instances<grahpene_core::raster::image::ImageFrame<Color>>"
|
||||
| "grahpene_core::instances::Instances<grahpene_core::raster::image::ImageFrame<grahpene_core::raster::color::Color>>"
|
||||
| "grahpene_core::instances::Instances<grahpene_core::raster::image::Image<grahpene_core::raster::color::Color>>" => {
|
||||
"grahpene_core::table::Table<grahpene_core::raster::image::Image<grahpene_core::raster::color::Color>>".to_string()
|
||||
}
|
||||
"grahpene_core::vector::vector_data::VectorData"
|
||||
| "grahpene_core::instances::Instances<grahpene_core::vector::vector_data::VectorData>"
|
||||
| "grahpene_core::table::Table<grahpene_core::vector::vector_data::VectorData>"
|
||||
| "grahpene_core::table::Table<grahpene_core::vector::vector_data::Vector>" => "grahpene_core::table::Table<grahpene_core::vector::vector_types::Vector>".to_string(),
|
||||
"grahpene_core::instances::Instances<grahpene_core::graphic_element::Artboard>" => "grahpene_core::table::Table<grahpene_core::artboard::Artboard>".to_string(),
|
||||
"grahpene_core::vector::vector_data::modification::VectorModification" => "grahpene_core::vector::vector_modification::VectorModification".to_string(),
|
||||
"grahpene_core::table::Table<grahpene_core::graphic_element::Graphic>" => "grahpene_core::table::Table<grahpene_core::graphic_types::Graphic>".to_string(),
|
||||
_ => name,
|
||||
};
|
||||
|
||||
Ok(Cow::Owned(name))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TypeDescriptor {
|
||||
#[serde(skip)]
|
||||
#[specta(skip)]
|
||||
pub id: Option<TypeId>,
|
||||
#[serde(deserialize_with = "migrate_type_descriptor_names")]
|
||||
pub name: Cow<'static, str>,
|
||||
#[serde(default)]
|
||||
pub alias: Option<Cow<'static, str>>,
|
||||
#[serde(skip)]
|
||||
pub size: usize,
|
||||
#[serde(skip)]
|
||||
pub align: usize,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for TypeDescriptor {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TypeDescriptor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self.id, other.id) {
|
||||
(Some(id), Some(other_id)) => id == other_id,
|
||||
_ => {
|
||||
// TODO: Add a flag to disable this warning
|
||||
// warn!("TypeDescriptor::eq: comparing types without ids based on name");
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Graph runtime type information used for type inference.
|
||||
#[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>),
|
||||
/// A wrapper around the Rust type id for any concrete Rust type. Allows us to do equality comparisons, like checking if a String == a String.
|
||||
Concrete(TypeDescriptor),
|
||||
/// Runtime type information for a function. Given some input, gives some output.
|
||||
Fn(Box<Type>, Box<Type>),
|
||||
/// Represents a future which promises to return the inner type.
|
||||
Future(Box<Type>),
|
||||
}
|
||||
|
||||
impl Default for Type {
|
||||
fn default() -> Self {
|
||||
concrete!(())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl dyn_any::StaticType for Type {
|
||||
type Static = Self;
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn is_generic(&self) -> bool {
|
||||
matches!(self, Type::Generic(_))
|
||||
}
|
||||
|
||||
pub fn is_concrete(&self) -> bool {
|
||||
matches!(self, Type::Concrete(_))
|
||||
}
|
||||
|
||||
pub fn is_fn(&self) -> bool {
|
||||
matches!(self, Type::Fn(_, _))
|
||||
}
|
||||
|
||||
pub fn is_value(&self) -> bool {
|
||||
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
|
||||
}
|
||||
|
||||
pub fn is_unit(&self) -> bool {
|
||||
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
|
||||
}
|
||||
|
||||
pub fn is_generic_or_fn(&self) -> bool {
|
||||
matches!(self, Type::Fn(_, _) | Type::Generic(_))
|
||||
}
|
||||
|
||||
pub fn fn_input(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Type::Fn(first, _) => Some(first),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fn_output(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Type::Fn(_, second) => Some(second),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn function(input: &Type, output: &Type) -> Type {
|
||||
Type::Fn(Box::new(input.clone()), Box::new(output.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn new<T: dyn_any::StaticType + Sized>() -> Self {
|
||||
Self::Concrete(TypeDescriptor {
|
||||
id: Some(TypeId::of::<T::Static>()),
|
||||
name: Cow::Borrowed(std::any::type_name::<T::Static>()),
|
||||
alias: None,
|
||||
size: size_of::<T>(),
|
||||
align: align_of::<T>(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Generic(_) => None,
|
||||
Self::Concrete(ty) => Some(ty.size),
|
||||
Self::Fn(_, _) => None,
|
||||
Self::Future(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Generic(_) => None,
|
||||
Self::Concrete(ty) => Some(ty.align),
|
||||
Self::Fn(_, _) => None,
|
||||
Self::Future(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nested_type(&self) -> &Type {
|
||||
match self {
|
||||
Self::Generic(_) => self,
|
||||
Self::Concrete(_) => self,
|
||||
Self::Fn(_, output) => output.nested_type(),
|
||||
Self::Future(output) => output.nested_type(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_nested(&mut self, f: impl Fn(&Type) -> Option<Type>) -> Option<Type> {
|
||||
if let Some(replacement) = f(self) {
|
||||
return Some(std::mem::replace(self, replacement));
|
||||
}
|
||||
match self {
|
||||
Self::Generic(_) => None,
|
||||
Self::Concrete(_) => None,
|
||||
Self::Fn(_, output) => output.replace_nested(f),
|
||||
Self::Future(output) => output.replace_nested(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_cow_string(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Type::Generic(name) => name.clone(),
|
||||
_ => Cow::Owned(self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_type(ty: &str) -> String {
|
||||
ty.split('<')
|
||||
.map(|path| path.split(',').map(|path| path.split("::").last().unwrap_or(path)).collect::<Vec<_>>().join(","))
|
||||
.collect::<Vec<_>>()
|
||||
.join("<")
|
||||
}
|
||||
|
||||
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")]
|
||||
Self::Concrete(ty) => format!("Concrete<{}, {:?}>", ty.name, ty.id),
|
||||
#[cfg(not(feature = "type_id_logging"))]
|
||||
Self::Concrete(ty) => format_type(&ty.name),
|
||||
Self::Fn(call_arg, return_value) => format!("{return_value:?} called with {call_arg:?}"),
|
||||
Self::Future(ty) => format!("{ty:?}"),
|
||||
};
|
||||
let result = result.replace("Option<Arc<OwnedContextImpl>>", "Context");
|
||||
write!(f, "{result}")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let result = match self {
|
||||
Type::Generic(name) => name.to_string(),
|
||||
Type::Concrete(ty) => format_type(&ty.name),
|
||||
Type::Fn(call_arg, return_value) => format!("{return_value} called with {call_arg}"),
|
||||
Type::Future(ty) => ty.to_string(),
|
||||
};
|
||||
let result = result.replace("Option<Arc<OwnedContextImpl>>", "Context");
|
||||
write!(f, "{result}")
|
||||
}
|
||||
}
|
||||
86
node-graph/libraries/core-types/src/uuid.rs
Normal file
86
node-graph/libraries/core-types/src/uuid.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use dyn_any::DynAny;
|
||||
pub use uuid_generation::*;
|
||||
|
||||
#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct Uuid(
|
||||
#[serde(with = "u64_string")]
|
||||
#[specta(type = String)]
|
||||
u64,
|
||||
);
|
||||
|
||||
mod u64_string {
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use std::str::FromStr;
|
||||
|
||||
// The signature of a serialize_with function must follow the pattern:
|
||||
//
|
||||
// fn serialize<S>(&T, S) -> Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer
|
||||
//
|
||||
// although it may also be generic over the input types T.
|
||||
pub fn serialize<S>(value: &u64, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&value.to_string())
|
||||
}
|
||||
|
||||
// The signature of a deserialize_with function must follow the pattern:
|
||||
//
|
||||
// fn deserialize<'de, D>(D) -> Result<T, D::Error>
|
||||
// where
|
||||
// D: Deserializer<'de>
|
||||
//
|
||||
// although it may also be generic over the output types T.
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
u64::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
mod uuid_generation {
|
||||
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);
|
||||
thread_local! {
|
||||
pub static UUID_SEED: Cell<Option<u64>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
pub fn set_uuid_seed(random_seed: u64) {
|
||||
UUID_SEED.with(|seed| seed.set(Some(random_seed)))
|
||||
}
|
||||
|
||||
pub fn generate_uuid() -> u64 {
|
||||
let Ok(mut lock) = RNG.lock() else { panic!("UUID mutex poisoned") };
|
||||
if lock.is_none() {
|
||||
UUID_SEED.with(|seed| {
|
||||
let random_seed = seed.get().unwrap_or(42);
|
||||
*lock = Some(ChaCha20Rng::seed_from_u64(random_seed));
|
||||
})
|
||||
}
|
||||
lock.as_mut().map(ChaCha20Rng::next_u64).expect("UUID mutex poisoned")
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type, DynAny)]
|
||||
pub struct NodeId(pub u64);
|
||||
|
||||
impl NodeId {
|
||||
pub fn new() -> Self {
|
||||
Self(generate_uuid())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NodeId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
211
node-graph/libraries/core-types/src/value.rs
Normal file
211
node-graph/libraries/core-types/src/value.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::Node;
|
||||
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>;
|
||||
|
||||
impl<'i, const N: u32, I> Node<'i, I> for IntNode<N> {
|
||||
type Output = u32;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
N
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct ValueNode<T>(pub T);
|
||||
|
||||
impl<'i, T: 'i, I> Node<'i, I> for ValueNode<T> {
|
||||
type Output = &'i T;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ValueNode<T> {
|
||||
pub const fn new(value: T) -> ValueNode<T> {
|
||||
ValueNode(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for ValueNode<T> {
|
||||
fn from(value: T) -> Self {
|
||||
ValueNode::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct AsRefNode<T: AsRef<U>, U>(pub T, PhantomData<U>);
|
||||
|
||||
impl<'i, T: 'i + AsRef<U>, U: 'i> Node<'i, ()> for AsRefNode<T, U> {
|
||||
type Output = &'i U;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: ()) -> Self::Output {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<U>, U> AsRefNode<T, U> {
|
||||
pub const fn new(value: T) -> AsRefNode<T, U> {
|
||||
AsRefNode(value, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct RefCellMutNode<T>(pub RefCell<T>);
|
||||
|
||||
impl<'i, T: 'i> Node<'i, ()> for RefCellMutNode<T> {
|
||||
type Output = RefMut<'i, T>;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: ()) -> Self::Output {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RefCellMutNode<T> {
|
||||
pub const fn new(value: T) -> RefCellMutNode<T> {
|
||||
RefCellMutNode(RefCell::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OnceCellNode<T>(pub Cell<T>);
|
||||
|
||||
impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode<T> {
|
||||
type Output = T;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
self.0.replace(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OnceCellNode<T> {
|
||||
pub const fn new(value: T) -> OnceCellNode<T> {
|
||||
OnceCellNode(Cell::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ClonedNode<T: Clone>(pub T);
|
||||
|
||||
impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode<T> {
|
||||
type Output = T;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> ClonedNode<T> {
|
||||
pub const fn new(value: T) -> ClonedNode<T> {
|
||||
ClonedNode(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> From<T> for ClonedNode<T> {
|
||||
fn from(value: T) -> Self {
|
||||
ClonedNode::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// The DebugClonedNode logs every time it is evaluated.
|
||||
/// This is useful for debugging.
|
||||
pub struct DebugClonedNode<T: Clone>(pub T);
|
||||
|
||||
impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
|
||||
type Output = T;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: ()) -> Self::Output {
|
||||
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
|
||||
log::debug!("DebugClonedNode::eval");
|
||||
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> DebugClonedNode<T> {
|
||||
pub const fn new(value: T) -> DebugClonedNode<T> {
|
||||
DebugClonedNode(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CopiedNode<T: Copy>(pub T);
|
||||
|
||||
impl<'i, T: Copy + 'i, I> Node<'i, I> for CopiedNode<T> {
|
||||
type Output = T;
|
||||
#[inline(always)]
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> CopiedNode<T> {
|
||||
pub const fn new(value: T) -> CopiedNode<T> {
|
||||
CopiedNode(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultNode<T>(PhantomData<T>);
|
||||
|
||||
impl<'i, T: Default + 'i, I> Node<'i, I> for DefaultNode<T> {
|
||||
type Output = T;
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
T::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DefaultNode<T> {
|
||||
pub fn new() -> Self {
|
||||
Self(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
/// Return the unit value
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct ForgetNode;
|
||||
|
||||
impl<'i, T: 'i> Node<'i, T> for ForgetNode {
|
||||
type Output = ();
|
||||
fn eval(&'i self, _input: T) -> Self::Output {}
|
||||
}
|
||||
|
||||
impl ForgetNode {
|
||||
pub const fn new() -> Self {
|
||||
ForgetNode
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_int_node() {
|
||||
let node = IntNode::<5>;
|
||||
assert_eq!(node.eval(()), 5);
|
||||
}
|
||||
#[test]
|
||||
fn test_value_node() {
|
||||
let node = ValueNode::new(5);
|
||||
assert_eq!(node.eval(()), &5);
|
||||
let type_erased = &node as &dyn for<'a> Node<'a, (), Output = &'a i32>;
|
||||
assert_eq!(type_erased.eval(()), &5);
|
||||
}
|
||||
#[test]
|
||||
fn test_default_node() {
|
||||
let node = DefaultNode::<u32>::new();
|
||||
assert_eq!(node.eval(42), 0);
|
||||
}
|
||||
#[test]
|
||||
#[allow(clippy::unit_cmp)]
|
||||
fn test_unit_node() {
|
||||
let node = ForgetNode::new();
|
||||
assert_eq!(node.eval(()), ());
|
||||
}
|
||||
}
|
||||
26
node-graph/libraries/graphic-types/Cargo.toml
Normal file
26
node-graph/libraries/graphic-types/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "graphic-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Graphic types for Graphene - combines vector types with core infrastructure"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
raster-types = { workspace = true, features = ["wgpu"] }
|
||||
vector-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
128
node-graph/libraries/graphic-types/src/artboard.rs
Normal file
128
node-graph/libraries/graphic-types/src/artboard.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::graphic::Graphic;
|
||||
use core_types::Color;
|
||||
use core_types::blending::AlphaBlending;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::math::quad::Quad;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use core_types::transform::Transform;
|
||||
use core_types::uuid::NodeId;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use std::hash::Hash;
|
||||
|
||||
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Artboard {
|
||||
pub content: Table<Graphic>,
|
||||
pub label: String,
|
||||
pub location: IVec2,
|
||||
pub dimensions: IVec2,
|
||||
pub background: Color,
|
||||
pub clip: bool,
|
||||
}
|
||||
|
||||
impl Default for Artboard {
|
||||
fn default() -> Self {
|
||||
Self::new(IVec2::ZERO, IVec2::new(1920, 1080))
|
||||
}
|
||||
}
|
||||
|
||||
impl Artboard {
|
||||
pub fn new(location: IVec2, dimensions: IVec2) -> Self {
|
||||
Self {
|
||||
content: Table::new(),
|
||||
label: "Artboard".to_string(),
|
||||
location: location.min(location + dimensions),
|
||||
dimensions: dimensions.abs(),
|
||||
background: Color::WHITE,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Artboard {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
let artboard_bounds = || (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
|
||||
|
||||
if self.clip {
|
||||
return RenderBoundingBox::Rectangle(artboard_bounds());
|
||||
}
|
||||
|
||||
match self.content.bounding_box(transform, include_stroke) {
|
||||
RenderBoundingBox::Rectangle(content_bounds) => RenderBoundingBox::Rectangle(Quad::combine_bounds(content_bounds, artboard_bounds())),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Artboard {
|
||||
fn render_complexity(&self) -> usize {
|
||||
self.content.render_complexity()
|
||||
}
|
||||
}
|
||||
|
||||
// Implementations for Artboard
|
||||
impl Transform for Artboard {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
DAffine2::from_translation(self.location.as_dvec2())
|
||||
}
|
||||
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
|
||||
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ArtboardGroup {
|
||||
pub artboards: Vec<(Artboard, Option<NodeId>)>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ArtboardFormat {
|
||||
ArtboardGroup(ArtboardGroup),
|
||||
OldArtboardTable(OldTable<Artboard>),
|
||||
ArtboardTable(Table<Artboard>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldTable<T> {
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
transform: Vec<DAffine2>,
|
||||
alpha_blending: Vec<AlphaBlending>,
|
||||
}
|
||||
|
||||
Ok(match ArtboardFormat::deserialize(deserializer)? {
|
||||
ArtboardFormat::ArtboardGroup(artboard_group) => {
|
||||
let mut table = Table::new();
|
||||
for (artboard, source_node_id) in artboard_group.artboards {
|
||||
table.push(TableRow {
|
||||
element: artboard,
|
||||
transform: DAffine2::IDENTITY,
|
||||
alpha_blending: AlphaBlending::default(),
|
||||
source_node_id,
|
||||
});
|
||||
}
|
||||
table
|
||||
}
|
||||
ArtboardFormat::OldArtboardTable(old_table) => old_table
|
||||
.element
|
||||
.into_iter()
|
||||
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
|
||||
.map(|(element, (transform, alpha_blending))| TableRow {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id: None,
|
||||
})
|
||||
.collect(),
|
||||
ArtboardFormat::ArtboardTable(artboard_table) => artboard_table,
|
||||
})
|
||||
}
|
||||
|
||||
// Node definitions moved to graphic-nodes crate
|
||||
421
node-graph/libraries/graphic-types/src/graphic.rs
Normal file
421
node-graph/libraries/graphic-types/src/graphic.rs
Normal file
@@ -0,0 +1,421 @@
|
||||
use core_types::Color;
|
||||
use core_types::blending::AlphaBlending;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::ops::TableConvert;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use core_types::uuid::NodeId;
|
||||
use dyn_any::DynAny;
|
||||
use glam::DAffine2;
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use std::hash::Hash;
|
||||
use vector_types::GradientStops;
|
||||
// use vector_types::Vector;
|
||||
|
||||
pub type Vector = vector_types::Vector<Option<Table<Graphic>>>;
|
||||
|
||||
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Graphic {
|
||||
Graphic(Table<Graphic>),
|
||||
Vector(Table<Vector>),
|
||||
RasterCPU(Table<Raster<CPU>>),
|
||||
RasterGPU(Table<Raster<GPU>>),
|
||||
Color(Table<Color>),
|
||||
Gradient(Table<GradientStops>),
|
||||
}
|
||||
|
||||
impl Default for Graphic {
|
||||
fn default() -> Self {
|
||||
Self::Graphic(Table::new())
|
||||
}
|
||||
}
|
||||
|
||||
// Graphic
|
||||
impl From<Table<Graphic>> for Graphic {
|
||||
fn from(graphic: Table<Graphic>) -> Self {
|
||||
Graphic::Graphic(graphic)
|
||||
}
|
||||
}
|
||||
|
||||
// Vector
|
||||
impl From<Vector> for Graphic {
|
||||
fn from(vector: Vector) -> Self {
|
||||
Graphic::Vector(Table::new_from_element(vector))
|
||||
}
|
||||
}
|
||||
impl From<Table<Vector>> for Graphic {
|
||||
fn from(vector: Table<Vector>) -> Self {
|
||||
Graphic::Vector(vector)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Table<Vector> -> Table<Graphic> conversion handled by blanket impl in gcore
|
||||
|
||||
// Raster<CPU>
|
||||
impl From<Raster<CPU>> for Graphic {
|
||||
fn from(raster: Raster<CPU>) -> Self {
|
||||
Graphic::RasterCPU(Table::new_from_element(raster))
|
||||
}
|
||||
}
|
||||
impl From<Table<Raster<CPU>>> for Graphic {
|
||||
fn from(raster: Table<Raster<CPU>>) -> Self {
|
||||
Graphic::RasterCPU(raster)
|
||||
}
|
||||
}
|
||||
// Note: Table conversions handled by blanket impl in gcore
|
||||
|
||||
// Raster<GPU>
|
||||
impl From<Raster<GPU>> for Graphic {
|
||||
fn from(raster: Raster<GPU>) -> Self {
|
||||
Graphic::RasterGPU(Table::new_from_element(raster))
|
||||
}
|
||||
}
|
||||
impl From<Table<Raster<GPU>>> for Graphic {
|
||||
fn from(raster: Table<Raster<GPU>>) -> Self {
|
||||
Graphic::RasterGPU(raster)
|
||||
}
|
||||
}
|
||||
// Note: Table conversions handled by blanket impl in gcore
|
||||
|
||||
// Color
|
||||
impl From<Color> for Graphic {
|
||||
fn from(color: Color) -> Self {
|
||||
Graphic::Color(Table::new_from_element(color))
|
||||
}
|
||||
}
|
||||
impl From<Table<Color>> for Graphic {
|
||||
fn from(color: Table<Color>) -> Self {
|
||||
Graphic::Color(color)
|
||||
}
|
||||
}
|
||||
// Note: Table conversions handled by blanket impl in gcore
|
||||
|
||||
// Option<Color>
|
||||
impl From<Option<Color>> for Graphic {
|
||||
fn from(color: Option<Color>) -> Self {
|
||||
if let Some(color) = color {
|
||||
Graphic::Color(Table::new_from_element(color))
|
||||
} else {
|
||||
Graphic::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: Table conversions handled by blanket impl in gcore
|
||||
// Note: Table<Color> -> Option<Color> is in gcore (Color is defined there)
|
||||
|
||||
// GradientStops
|
||||
impl From<GradientStops> for Graphic {
|
||||
fn from(gradient: GradientStops) -> Self {
|
||||
Graphic::Gradient(Table::new_from_element(gradient))
|
||||
}
|
||||
}
|
||||
impl From<Table<GradientStops>> for Graphic {
|
||||
fn from(gradient: Table<GradientStops>) -> Self {
|
||||
Graphic::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
// Local trait to convert types to Table<Graphic> (avoids orphan rule issues)
|
||||
pub trait IntoGraphicTable {
|
||||
fn into_graphic_table(self) -> Table<Graphic>;
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<Graphic> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<Vector> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::Vector(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<Raster<CPU>> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::RasterCPU(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<Raster<GPU>> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::RasterGPU(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<Color> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::Color(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for Table<GradientStops> {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::Gradient(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGraphicTable for DAffine2 {
|
||||
fn into_graphic_table(self) -> Table<Graphic> {
|
||||
Table::new_from_element(Graphic::default())
|
||||
}
|
||||
}
|
||||
|
||||
// DAffine2
|
||||
impl From<DAffine2> for Graphic {
|
||||
fn from(_: DAffine2) -> Self {
|
||||
Graphic::default()
|
||||
}
|
||||
}
|
||||
// Note: Table conversions handled by blanket impl in gcore
|
||||
|
||||
impl Graphic {
|
||||
pub fn as_graphic(&self) -> Option<&Table<Graphic>> {
|
||||
match self {
|
||||
Graphic::Graphic(graphic) => Some(graphic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_graphic_mut(&mut self) -> Option<&mut Table<Graphic>> {
|
||||
match self {
|
||||
Graphic::Graphic(graphic) => Some(graphic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector(&self) -> Option<&Table<Vector>> {
|
||||
match self {
|
||||
Graphic::Vector(vector) => Some(vector),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector_mut(&mut self) -> Option<&mut Table<Vector>> {
|
||||
match self {
|
||||
Graphic::Vector(vector) => Some(vector),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_raster(&self) -> Option<&Table<Raster<CPU>>> {
|
||||
match self {
|
||||
Graphic::RasterCPU(raster) => Some(raster),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_raster_mut(&mut self) -> Option<&mut Table<Raster<CPU>>> {
|
||||
match self {
|
||||
Graphic::RasterCPU(raster) => Some(raster),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn had_clip_enabled(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending.clip),
|
||||
Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending.clip),
|
||||
Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
|
||||
Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
|
||||
Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip),
|
||||
Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector.iter().all(|row| {
|
||||
let style = &row.element.style;
|
||||
let alpha_blending = &row.alpha_blending;
|
||||
(alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Graphic {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector.bounding_box(transform, include_stroke),
|
||||
Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke),
|
||||
Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke),
|
||||
Graphic::Color(color) => color.bounding_box(transform, include_stroke),
|
||||
Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableConvert<Graphic> for Vector {
|
||||
fn convert_row(self) -> Graphic {
|
||||
Graphic::Vector(Table::new_from_element(self))
|
||||
}
|
||||
}
|
||||
impl TableConvert<Graphic> for Raster<CPU> {
|
||||
fn convert_row(self) -> Graphic {
|
||||
Graphic::RasterCPU(Table::new_from_element(self))
|
||||
}
|
||||
}
|
||||
impl TableConvert<Graphic> for Raster<GPU> {
|
||||
fn convert_row(self) -> Graphic {
|
||||
Graphic::RasterGPU(Table::new_from_element(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Graphic {
|
||||
fn render_complexity(&self) -> usize {
|
||||
match self {
|
||||
Self::Graphic(table) => table.render_complexity(),
|
||||
Self::Vector(table) => table.render_complexity(),
|
||||
Self::RasterCPU(table) => table.render_complexity(),
|
||||
Self::RasterGPU(table) => table.render_complexity(),
|
||||
Self::Color(table) => table.render_complexity(),
|
||||
Self::Gradient(table) => table.render_complexity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions moved to graphic-nodes crate
|
||||
|
||||
pub trait AtIndex {
|
||||
type Output;
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output>;
|
||||
}
|
||||
impl<T: Clone> AtIndex for Vec<T> {
|
||||
type Output = T;
|
||||
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output> {
|
||||
self.get(index).cloned()
|
||||
}
|
||||
}
|
||||
impl<T: Clone> AtIndex for Table<T> {
|
||||
type Output = Table<T>;
|
||||
|
||||
fn at_index(&self, index: usize) -> Option<Self::Output> {
|
||||
let mut result_table = Self::default();
|
||||
if let Some(row) = self.iter().nth(index) {
|
||||
result_table.push(row.into_cloned());
|
||||
Some(result_table)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldGraphicGroup {
|
||||
elements: Vec<(Graphic, Option<NodeId>)>,
|
||||
transform: DAffine2,
|
||||
alpha_blending: AlphaBlending,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GraphicGroup {
|
||||
elements: Vec<(Graphic, Option<NodeId>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OlderTable<T> {
|
||||
id: Vec<u64>,
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldTable<T> {
|
||||
id: Vec<u64>,
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
transform: Vec<DAffine2>,
|
||||
alpha_blending: Vec<AlphaBlending>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum GraphicFormat {
|
||||
OldGraphicGroup(OldGraphicGroup),
|
||||
OlderTableOldGraphicGroup(OlderTable<OldGraphicGroup>),
|
||||
OldTableOldGraphicGroup(OldTable<OldGraphicGroup>),
|
||||
OldTableGraphicGroup(OldTable<GraphicGroup>),
|
||||
Table(serde_json::Value),
|
||||
}
|
||||
|
||||
Ok(match GraphicFormat::deserialize(deserializer)? {
|
||||
GraphicFormat::OldGraphicGroup(old) => {
|
||||
let mut graphic_table = Table::new();
|
||||
for (graphic, source_node_id) in old.elements {
|
||||
graphic_table.push(TableRow {
|
||||
element: graphic,
|
||||
transform: old.transform,
|
||||
alpha_blending: old.alpha_blending,
|
||||
source_node_id,
|
||||
});
|
||||
}
|
||||
graphic_table
|
||||
}
|
||||
GraphicFormat::OlderTableOldGraphicGroup(old) => old
|
||||
.element
|
||||
.into_iter()
|
||||
.flat_map(|element| {
|
||||
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
|
||||
element: graphic,
|
||||
transform: element.transform,
|
||||
alpha_blending: element.alpha_blending,
|
||||
source_node_id,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
GraphicFormat::OldTableOldGraphicGroup(old) => old
|
||||
.element
|
||||
.into_iter()
|
||||
.flat_map(|element| {
|
||||
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
|
||||
element: graphic,
|
||||
transform: element.transform,
|
||||
alpha_blending: element.alpha_blending,
|
||||
source_node_id,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
GraphicFormat::OldTableGraphicGroup(old) => old
|
||||
.element
|
||||
.into_iter()
|
||||
.flat_map(|element| {
|
||||
element.elements.into_iter().map(move |(graphic, source_node_id)| TableRow {
|
||||
element: graphic,
|
||||
transform: Default::default(),
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
GraphicFormat::Table(value) => {
|
||||
// Try to deserialize as either table format
|
||||
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
|
||||
let mut graphic_table = Table::new();
|
||||
for row in old_table.iter() {
|
||||
for (graphic, source_node_id) in &row.element.elements {
|
||||
graphic_table.push(TableRow {
|
||||
element: graphic.clone(),
|
||||
transform: *row.transform,
|
||||
alpha_blending: *row.alpha_blending,
|
||||
source_node_id: *source_node_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
graphic_table
|
||||
} else if let Ok(new_table) = serde_json::from_value::<Table<Graphic>>(value) {
|
||||
new_table
|
||||
} else {
|
||||
return Err(serde::de::Error::custom("Failed to deserialize Table<Graphic>"));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
100
node-graph/libraries/graphic-types/src/lib.rs
Normal file
100
node-graph/libraries/graphic-types/src/lib.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
pub mod artboard;
|
||||
pub mod graphic;
|
||||
|
||||
// Re-export all transitive dependencies so downstream crates only need to depend on graphic-types
|
||||
pub use core_types;
|
||||
pub use raster_types;
|
||||
pub use vector_types;
|
||||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use artboard::Artboard;
|
||||
pub use graphic::{Graphic, IntoGraphicTable, Vector};
|
||||
|
||||
pub mod migrations {
|
||||
use core_types::{
|
||||
AlphaBlending,
|
||||
table::{Table, TableRow},
|
||||
};
|
||||
use dyn_any::DynAny;
|
||||
use glam::DAffine2;
|
||||
use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId};
|
||||
|
||||
use crate::{Graphic, Vector};
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Vector>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldVectorData {
|
||||
pub transform: DAffine2,
|
||||
pub alpha_blending: AlphaBlending,
|
||||
|
||||
pub style: PathStyle,
|
||||
|
||||
pub colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
|
||||
pub point_domain: PointDomain,
|
||||
pub segment_domain: SegmentDomain,
|
||||
pub region_domain: RegionDomain,
|
||||
|
||||
pub upstream_graphic_group: Option<Table<Graphic>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldTable<T> {
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
transform: Vec<DAffine2>,
|
||||
alpha_blending: Vec<AlphaBlending>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OlderTable<T> {
|
||||
id: Vec<u64>,
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum VectorFormat {
|
||||
Vector(Vector),
|
||||
OldVectorData(OldVectorData),
|
||||
OldVectorTable(OldTable<Vector>),
|
||||
OlderVectorTable(OlderTable<Vector>),
|
||||
VectorTable(Table<Vector>),
|
||||
}
|
||||
|
||||
Ok(match VectorFormat::deserialize(deserializer)? {
|
||||
VectorFormat::Vector(vector) => Table::new_from_element(vector),
|
||||
VectorFormat::OldVectorData(old) => {
|
||||
let mut vector_table = Table::new_from_element(Vector {
|
||||
style: old.style,
|
||||
colinear_manipulators: old.colinear_manipulators,
|
||||
point_domain: old.point_domain,
|
||||
segment_domain: old.segment_domain,
|
||||
region_domain: old.region_domain,
|
||||
upstream_data: old.upstream_graphic_group,
|
||||
});
|
||||
*vector_table.iter_mut().next().unwrap().transform = old.transform;
|
||||
*vector_table.iter_mut().next().unwrap().alpha_blending = old.alpha_blending;
|
||||
vector_table
|
||||
}
|
||||
VectorFormat::OlderVectorTable(older_table) => older_table.element.into_iter().map(|element| TableRow { element, ..Default::default() }).collect(),
|
||||
VectorFormat::OldVectorTable(old_table) => old_table
|
||||
.element
|
||||
.into_iter()
|
||||
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
|
||||
.map(|(element, (transform, alpha_blending))| TableRow {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id: None,
|
||||
})
|
||||
.collect(),
|
||||
VectorFormat::VectorTable(vector_table) => vector_table,
|
||||
})
|
||||
}
|
||||
}
|
||||
60
node-graph/libraries/no-std-types/Cargo.toml
Normal file
60
node-graph/libraries/no-std-types/Cargo.toml
Normal file
@@ -0,0 +1,60 @@
|
||||
[package]
|
||||
name = "no-std-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "no_std types for Graphene (shader-compatible)"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
# any feature that
|
||||
# * must be usable in shaders
|
||||
# * but requires std
|
||||
# * and should be on by default
|
||||
# should be in this list instead of `[workspace.dependency]`
|
||||
std = [
|
||||
"dep:dyn-any",
|
||||
"dep:serde",
|
||||
"dep:specta",
|
||||
"dep:log",
|
||||
"glam/debug-glam-assert",
|
||||
"glam/std",
|
||||
"glam/serde",
|
||||
"half/std",
|
||||
"half/serde",
|
||||
"num-traits/std",
|
||||
"num_enum/std",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Local std dependencies
|
||||
dyn-any = { workspace = true, optional = true }
|
||||
|
||||
# Workspace dependencies
|
||||
bytemuck = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
half = { workspace = true, default-features = false }
|
||||
num-derive = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
spirv-std = { workspace = true }
|
||||
|
||||
# Workspace std dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
specta = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
core-types = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
# the spirv target is not in the list of common cfgs so must be added manually
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(target_arch, values("spirv"))',
|
||||
] }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["core-types"]
|
||||
250
node-graph/libraries/no-std-types/src/blending.rs
Normal file
250
node-graph/libraries/no-std-types/src/blending.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use core::fmt::Display;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use node_macro::BufferStruct;
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::float::Float;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, BufferStruct)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", 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: 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 Display for AlphaBlending {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::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 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opacity(&self, mask: bool) -> f32 {
|
||||
self.opacity * if mask { 1. } else { self.fill }
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
|
||||
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.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn render(&self) -> String {
|
||||
format!(
|
||||
r#" mix-blend-mode: {};"#,
|
||||
self.to_svg_style_name().unwrap_or_else(|| {
|
||||
log::warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
26
node-graph/libraries/no-std-types/src/choice_type.rs
Normal file
26
node-graph/libraries/no-std-types/src/choice_type.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
|
||||
const WIDGET_HINT: ChoiceWidgetHint;
|
||||
const DESCRIPTION: Option<&'static str>;
|
||||
fn list() -> &'static [&'static [(Self, VariantMetadata)]];
|
||||
}
|
||||
|
||||
pub enum ChoiceWidgetHint {
|
||||
Dropdown,
|
||||
RadioButtons,
|
||||
}
|
||||
|
||||
/// Translation struct between macro and definition.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VariantMetadata {
|
||||
/// Name as declared in source code.
|
||||
pub name: &'static str,
|
||||
|
||||
/// Name to be displayed in UI.
|
||||
pub label: &'static str,
|
||||
|
||||
/// User-facing documentation text.
|
||||
pub docstring: Option<&'static str>,
|
||||
|
||||
/// Name of icon to display in radio buttons and such.
|
||||
pub icon: Option<&'static str>,
|
||||
}
|
||||
195
node-graph/libraries/no-std-types/src/color/color_traits.rs
Normal file
195
node-graph/libraries/no-std-types/src/color/color_traits.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
pub use crate::blending::*;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core::fmt::Debug;
|
||||
use glam::DVec2;
|
||||
use num_derive::*;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use num_traits::float::Float;
|
||||
|
||||
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 {}
|
||||
|
||||
#[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 {}
|
||||
|
||||
// TODO: Come up with a better name for this trait
|
||||
pub trait Pixel: Clone + Pod + Zeroable + Default {
|
||||
#[cfg(feature = "std")]
|
||||
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)
|
||||
}
|
||||
}
|
||||
1134
node-graph/libraries/no-std-types/src/color/color_types.rs
Normal file
1134
node-graph/libraries/no-std-types/src/color/color_types.rs
Normal file
File diff suppressed because it is too large
Load Diff
178
node-graph/libraries/no-std-types/src/color/discrete_srgb.rs
Normal file
178
node-graph/libraries/no-std-types/src/color/discrete_srgb.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
#![allow(clippy::neg_cmp_op_on_partial_ord)]
|
||||
//! Fast conversions between u8 sRGB and linear float.
|
||||
|
||||
// Inspired by https://gist.github.com/rygorous/2203834, but with a slightly
|
||||
// modified method, custom derived constants and error correction for perfect
|
||||
// accuracy in accordance with the D3D11 spec:
|
||||
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB.
|
||||
|
||||
/// CRITICAL_POINTS[i] is the last float value such that it maps to i after
|
||||
/// conversion to integer sRGB. So if x > CRITICAL_POINTS[i] you know you need
|
||||
/// to increment i.
|
||||
#[rustfmt::skip]
|
||||
const CRITICAL_POINTS: [f32; 256] = [
|
||||
0.00015176347, 0.00045529046, 0.0007588174, 0.0010623443, 0.0013658714, 0.0016693983, 0.0019729252, 0.0022764523,
|
||||
0.0025799791, 0.0028835062, 0.0031883009, 0.003509259, 0.003848315, 0.004205748, 0.0045818323, 0.0049768374,
|
||||
0.005391024, 0.00582465, 0.0062779686, 0.0067512267, 0.0072446675, 0.0077585294, 0.008293047, 0.008848451,
|
||||
0.0094249705, 0.010022825, 0.010642236, 0.01128342, 0.011946591, 0.012631957, 0.013339729, 0.014070111,
|
||||
0.0148233045, 0.015599505, 0.01639891, 0.017221717, 0.018068114, 0.018938294, 0.019832445, 0.020750746,
|
||||
0.021693384, 0.022660539, 0.02365239, 0.024669115, 0.025710886, 0.026777886, 0.027870273, 0.028988222,
|
||||
0.030131903, 0.03130148, 0.032497127, 0.033718992, 0.034967244, 0.03624204, 0.03754355, 0.03887192,
|
||||
0.040227327, 0.041609894, 0.04301979, 0.044457167, 0.04592218, 0.04741497, 0.04893569, 0.050484486,
|
||||
0.05206151, 0.053666897, 0.055300802, 0.056963358, 0.058654714, 0.060375024, 0.062124394, 0.06390298,
|
||||
0.065710925, 0.06754836, 0.06941542, 0.07131224, 0.07323896, 0.07519571, 0.07718261, 0.07919981,
|
||||
0.08124744, 0.08332562, 0.08543448, 0.08757417, 0.08974478, 0.091946445, 0.09417931, 0.09644348,
|
||||
0.098739095, 0.10106628, 0.10342514, 0.105815805, 0.1082384, 0.110693045, 0.11317986, 0.11569896,
|
||||
0.118250474, 0.12083454, 0.12345121, 0.12610064, 0.12878296, 0.13149826, 0.13424668, 0.1370283,
|
||||
0.13984327, 0.14269169, 0.14557366, 0.1484893, 0.15143873, 0.15442204, 0.15743938, 0.16049084,
|
||||
0.1635765, 0.16669647, 0.16985092, 0.1730399, 0.17626354, 0.17952198, 0.18281525, 0.1861435,
|
||||
0.18950681, 0.19290532, 0.19633913, 0.19980833, 0.20331302, 0.20685332, 0.21042931, 0.21404111,
|
||||
0.21768881, 0.22137253, 0.22509235, 0.22884844, 0.23264077, 0.23646952, 0.24033478, 0.24423665,
|
||||
0.24817522, 0.25215057, 0.25616285, 0.26021212, 0.26429847, 0.26842204, 0.27258286, 0.27678108,
|
||||
0.2810168, 0.28529006, 0.289601, 0.2939497, 0.29833627, 0.30276078, 0.30722332, 0.311724,
|
||||
0.31626293, 0.32084015, 0.32545578, 0.33010995, 0.3348027, 0.3395341, 0.34430432, 0.34911346,
|
||||
0.3539615, 0.35884857, 0.3637748, 0.36874023, 0.373745, 0.37878913, 0.38387278, 0.388996,
|
||||
0.39415887, 0.39936152, 0.404604, 0.4098864, 0.41520882, 0.42057133, 0.425974, 0.431417,
|
||||
0.43690032, 0.4424241, 0.44798836, 0.45359328, 0.45923886, 0.46492523, 0.47065246, 0.47642064,
|
||||
0.48222986, 0.48808017, 0.4939718, 0.49990457, 0.5058787, 0.5118943, 0.5179514, 0.5240501,
|
||||
0.5301905, 0.5363727, 0.5425967, 0.54886264, 0.5551706, 0.56152064, 0.5679129, 0.5743473,
|
||||
0.5808241, 0.5873433, 0.593905, 0.60050917, 0.60715604, 0.61384565, 0.62057805, 0.6273533,
|
||||
0.63417155, 0.6410328, 0.6479372, 0.65488476, 0.66187555, 0.6689097, 0.6759874, 0.68310845,
|
||||
0.6902731, 0.6974814, 0.7047334, 0.71202916, 0.7193688, 0.7267524, 0.73418003, 0.7416518,
|
||||
0.7491677, 0.7567278, 0.76433223, 0.7719811, 0.7796744, 0.7874122, 0.7951947, 0.80302185,
|
||||
0.8108938, 0.81881046, 0.82677215, 0.8347787, 0.8428304, 0.8509272, 0.85906917, 0.8672564,
|
||||
0.875489, 0.8837671, 0.89209044, 0.9004596, 0.9088741, 0.91733456, 0.9258405, 0.9343926,
|
||||
0.94299024, 0.95163417, 0.96032387, 0.96906, 0.977842, 0.9866705, 0.9955452, 1.,
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
const FLOAT_SRGB_LERP: [u32; 27] = [
|
||||
0x66f, 0x66f063b, 0xcaa0515, 0x11c00773, 0x193305dc, 0x1f1004f3, 0x24030481, 0x28850773,
|
||||
0x2ff9065e, 0x365805a1, 0x3bfa0547, 0x414108f7, 0x4a3907d8, 0x52110709, 0x591b06aa, 0x5fc50b70,
|
||||
0x6b350a18, 0x754e091c, 0x7e6b08aa, 0x87160ef1, 0x96070d3e, 0xa3460bfc, 0xaf430b6c, 0xbaaf13bd,
|
||||
0xce6d1187, 0xdff40fe3, 0xefd70f28,
|
||||
];
|
||||
|
||||
#[inline]
|
||||
pub fn float_to_srgb_u8(mut f: f32) -> u8 {
|
||||
// Clamp f to [0, 1], with a negated condition to handle NaNs as 0.
|
||||
if !(f >= 0.) {
|
||||
f = 0.;
|
||||
} else if f > 1. {
|
||||
f = 1.;
|
||||
}
|
||||
|
||||
// Shift away slightly from 0.0 to reduce exponent range.
|
||||
const C: f32 = 0.009842521f32;
|
||||
let u = (f + C).to_bits() - C.to_bits();
|
||||
if u > (1. + C).to_bits() - C.to_bits() {
|
||||
// We clamped f to [0, 1], and the integer representations
|
||||
// of the positive finite non-NaN floats are monotonic.
|
||||
// This makes the later LUT lookup panicless.
|
||||
unsafe { core::hint::unreachable_unchecked() }
|
||||
}
|
||||
|
||||
// Compute a piecewise linear interpolation that is always
|
||||
// the correct answer, or one less than it.
|
||||
let u16mask = (1 << 16) - 1;
|
||||
let lut_idx = u >> 21;
|
||||
let lerp_idx = (u >> 5) & u16mask;
|
||||
let bias_mult = FLOAT_SRGB_LERP[lut_idx as usize];
|
||||
let bias = (bias_mult >> 16) << 16;
|
||||
let mult = bias_mult & u16mask;
|
||||
// I don't believe this wraps, but since we test in release mode,
|
||||
// better make sure debug mode behaves the same.
|
||||
let lerp = bias.wrapping_add(mult * lerp_idx) >> 24;
|
||||
|
||||
// Adjust linear interpolation to the correct value.
|
||||
if f > CRITICAL_POINTS[lerp as usize] { lerp as u8 + 1 } else { lerp as u8 }
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const FROM_SRGB_U8: [f32; 256] = [
|
||||
0., 0.000303527, 0.000607054, 0.00091058103, 0.001214108, 0.001517635, 0.0018211621, 0.002124689,
|
||||
0.002428216, 0.002731743, 0.00303527, 0.0033465356, 0.003676507, 0.004024717, 0.004391442,
|
||||
0.0047769533, 0.005181517, 0.0056053917, 0.0060488326, 0.006512091, 0.00699541, 0.0074990317,
|
||||
0.008023192, 0.008568125, 0.009134057, 0.009721218, 0.010329823, 0.010960094, 0.011612245,
|
||||
0.012286487, 0.012983031, 0.013702081, 0.014443844, 0.015208514, 0.015996292, 0.016807375,
|
||||
0.017641952, 0.018500218, 0.019382361, 0.020288562, 0.02121901, 0.022173883, 0.023153365,
|
||||
0.02415763, 0.025186857, 0.026241222, 0.027320892, 0.028426038, 0.029556843, 0.03071345, 0.03189604,
|
||||
0.033104774, 0.03433981, 0.035601325, 0.036889452, 0.038204376, 0.039546248, 0.04091521, 0.042311423,
|
||||
0.043735042, 0.045186214, 0.046665095, 0.048171833, 0.049706575, 0.051269468, 0.052860655, 0.05448028,
|
||||
0.056128494, 0.057805434, 0.05951124, 0.06124607, 0.06301003, 0.06480328, 0.06662595, 0.06847818,
|
||||
0.07036011, 0.07227186, 0.07421358, 0.07618539, 0.07818743, 0.08021983, 0.082282715, 0.084376216,
|
||||
0.086500466, 0.088655606, 0.09084173, 0.09305898, 0.095307484, 0.09758736, 0.09989874, 0.10224175,
|
||||
0.10461649, 0.10702311, 0.10946172, 0.111932434, 0.11443538, 0.116970696, 0.11953845, 0.12213881,
|
||||
0.12477186, 0.12743773, 0.13013652, 0.13286836, 0.13563336, 0.13843165, 0.14126332, 0.1441285,
|
||||
0.1470273, 0.14995982, 0.15292618, 0.1559265, 0.15896086, 0.16202943, 0.16513224, 0.16826946,
|
||||
0.17144115, 0.17464745, 0.17788847, 0.1811643, 0.18447503, 0.1878208, 0.19120172, 0.19461787,
|
||||
0.19806935, 0.2015563, 0.20507877, 0.2086369, 0.21223079, 0.21586053, 0.21952623, 0.22322798,
|
||||
0.22696589, 0.23074007, 0.23455065, 0.23839766, 0.2422812, 0.2462014, 0.25015837, 0.25415218,
|
||||
0.2581829, 0.26225072, 0.26635566, 0.27049786, 0.27467737, 0.27889434, 0.2831488, 0.2874409,
|
||||
0.2917707, 0.29613832, 0.30054384, 0.30498737, 0.30946895, 0.31398875, 0.31854683, 0.32314324,
|
||||
0.32777813, 0.33245158, 0.33716366, 0.34191445, 0.3467041, 0.3515327, 0.35640025, 0.36130688,
|
||||
0.3662527, 0.37123778, 0.37626222, 0.3813261, 0.38642952, 0.39157256, 0.3967553, 0.40197787,
|
||||
0.4072403, 0.4125427, 0.41788515, 0.42326775, 0.42869055, 0.4341537, 0.43965724, 0.44520125,
|
||||
0.45078585, 0.45641106, 0.46207705, 0.46778384, 0.47353154, 0.47932023, 0.48514998, 0.4910209,
|
||||
0.49693304, 0.5028866, 0.50888145, 0.5149178, 0.5209957, 0.52711535, 0.5332766, 0.5394797,
|
||||
0.5457247, 0.5520116, 0.5583406, 0.5647117, 0.57112503, 0.57758063, 0.5840786, 0.590619, 0.597202,
|
||||
0.60382754, 0.61049575, 0.61720675, 0.62396055, 0.63075733, 0.637597, 0.6444799, 0.6514058,
|
||||
0.65837497, 0.66538745, 0.67244333, 0.6795426, 0.68668544, 0.69387203, 0.70110214, 0.70837605,
|
||||
0.7156938, 0.72305536, 0.730461, 0.7379107, 0.7454045, 0.75294244, 0.76052475, 0.7681514, 0.77582246,
|
||||
0.78353804, 0.79129815, 0.79910296, 0.8069525, 0.8148468, 0.822786, 0.8307701, 0.83879924, 0.84687346,
|
||||
0.8549928, 0.8631574, 0.87136734, 0.8796226, 0.8879232, 0.89626956, 0.90466136, 0.913099, 0.92158204,
|
||||
0.93011117, 0.9386859, 0.9473069, 0.9559735, 0.9646866, 0.9734455, 0.98225087, 0.9911022, 1.,
|
||||
];
|
||||
|
||||
#[inline]
|
||||
pub fn srgb_u8_to_float(c: u8) -> f32 {
|
||||
FROM_SRGB_U8[c as usize]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#FLOATtoSRGB
|
||||
fn float_to_srgb_ref(f: f32) -> f32 {
|
||||
if !(f > 0_f32) {
|
||||
0_f32
|
||||
} else if f <= 0.0031308f32 {
|
||||
12.92_f32 * f
|
||||
} else if f < 1_f32 {
|
||||
1.055f32 * f.powf(1_f32 / 2.4_f32) - 0.055f32
|
||||
} else {
|
||||
1_f32
|
||||
}
|
||||
}
|
||||
|
||||
fn float_to_srgb_u8_ref(f: f32) -> u8 {
|
||||
(float_to_srgb_ref(f) * 255_f32 + 0.5_f32) as u8
|
||||
}
|
||||
|
||||
// https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#SRGBtoFLOAT
|
||||
fn srgb_to_float_ref(f: f32) -> f32 {
|
||||
if f <= 0.04045f32 { f / 12.92f32 } else { ((f + 0.055f32) / 1.055f32).powf(2.4_f32) }
|
||||
}
|
||||
|
||||
fn srgb_u8_to_float_ref(c: u8) -> f32 {
|
||||
srgb_to_float_ref(c as f32 * (1_f32 / 255_f32))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_to_srgb_u8() {
|
||||
for u in 0..=u8::MAX {
|
||||
assert!(srgb_u8_to_float(u) == srgb_u8_to_float_ref(u));
|
||||
}
|
||||
}
|
||||
|
||||
#[ignore = "expensive, test in release mode"]
|
||||
#[test]
|
||||
fn test_srgb_u8_to_float() {
|
||||
// Simply... check all float values.
|
||||
for u in 0..=u32::MAX {
|
||||
let f = f32::from_bits(u);
|
||||
assert!(float_to_srgb_u8(f) == float_to_srgb_u8_ref(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
7
node-graph/libraries/no-std-types/src/color/mod.rs
Normal file
7
node-graph/libraries/no-std-types/src/color/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod color_traits;
|
||||
mod color_types;
|
||||
mod discrete_srgb;
|
||||
|
||||
pub use color_traits::*;
|
||||
pub use color_types::*;
|
||||
pub use discrete_srgb::*;
|
||||
9
node-graph/libraries/no-std-types/src/context.rs
Normal file
9
node-graph/libraries/no-std-types/src/context.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub trait Ctx: Clone + Send {}
|
||||
|
||||
impl<T: Ctx> Ctx for Option<T> {}
|
||||
impl<T: Ctx + Sync> Ctx for &T {}
|
||||
impl Ctx for () {}
|
||||
|
||||
pub trait ArcCtx: Send + Sync {}
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: ArcCtx> Ctx for std::sync::Arc<T> {}
|
||||
20
node-graph/libraries/no-std-types/src/lib.rs
Normal file
20
node-graph/libraries/no-std-types/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub mod blending;
|
||||
pub mod choice_type;
|
||||
pub mod color;
|
||||
pub mod context;
|
||||
pub mod registry;
|
||||
pub mod shaders;
|
||||
|
||||
pub use context::Ctx;
|
||||
pub use glam;
|
||||
|
||||
pub trait AsU32 {
|
||||
fn as_u32(&self) -> u32;
|
||||
}
|
||||
impl AsU32 for u32 {
|
||||
fn as_u32(&self) -> u32 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
31
node-graph/libraries/no-std-types/src/registry.rs
Normal file
31
node-graph/libraries/no-std-types/src/registry.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub mod types {
|
||||
/// 0% - 100%
|
||||
pub type Percentage = f64;
|
||||
/// 0% - 100%
|
||||
pub type PercentageF32 = f32;
|
||||
/// -100% - 100%
|
||||
pub type SignedPercentage = f64;
|
||||
/// -100% - 100%
|
||||
pub type SignedPercentageF32 = f32;
|
||||
/// -180° - 180°
|
||||
pub type Angle = f64;
|
||||
/// -180° - 180°
|
||||
pub type AngleF32 = f32;
|
||||
/// Ends in the unit of x
|
||||
pub type Multiplier = f64;
|
||||
/// Non-negative integer with px unit
|
||||
pub type PixelLength = f64;
|
||||
/// Non-negative
|
||||
pub type Length = f64;
|
||||
/// 0 to 1
|
||||
pub type Fraction = f64;
|
||||
/// Unsigned integer
|
||||
pub type IntegerCount = u32;
|
||||
/// Unsigned integer to be used for random seeds
|
||||
pub type SeedValue = u32;
|
||||
/// DVec2 with px unit
|
||||
pub type PixelSize = glam::DVec2;
|
||||
/// String with one or more than one line
|
||||
#[cfg(feature = "std")]
|
||||
pub type TextArea = String;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::shaders::buffer_struct::BufferStruct;
|
||||
|
||||
macro_rules! glam_array {
|
||||
($t:ty, $a:ty) => {
|
||||
unsafe impl BufferStruct for $t {
|
||||
type Buffer = $a;
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
<$t>::to_array(&from)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
<$t>::from_array(from)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! glam_cols_array {
|
||||
($t:ty, $a:ty) => {
|
||||
unsafe impl BufferStruct for $t {
|
||||
type Buffer = $a;
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
<$t>::to_cols_array(&from)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
<$t>::from_cols_array(&from)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
glam_array!(glam::Vec2, [f32; 2]);
|
||||
glam_array!(glam::Vec3, [f32; 3]);
|
||||
// glam_array!(Vec3A, [f32; 4]);
|
||||
glam_array!(glam::Vec4, [f32; 4]);
|
||||
glam_array!(glam::Quat, [f32; 4]);
|
||||
glam_cols_array!(glam::Mat2, [f32; 4]);
|
||||
glam_cols_array!(glam::Mat3, [f32; 9]);
|
||||
// glam_cols_array!(Mat3A, [f32; 4]);
|
||||
glam_cols_array!(glam::Mat4, [f32; 16]);
|
||||
glam_cols_array!(glam::Affine2, [f32; 6]);
|
||||
glam_cols_array!(glam::Affine3A, [f32; 12]);
|
||||
|
||||
glam_array!(glam::DVec2, [f64; 2]);
|
||||
glam_array!(glam::DVec3, [f64; 3]);
|
||||
glam_array!(glam::DVec4, [f64; 4]);
|
||||
glam_array!(glam::DQuat, [f64; 4]);
|
||||
glam_cols_array!(glam::DMat2, [f64; 4]);
|
||||
glam_cols_array!(glam::DMat3, [f64; 9]);
|
||||
glam_cols_array!(glam::DMat4, [f64; 16]);
|
||||
glam_cols_array!(glam::DAffine2, [f64; 6]);
|
||||
glam_cols_array!(glam::DAffine3, [f64; 12]);
|
||||
|
||||
glam_array!(glam::I16Vec2, [i16; 2]);
|
||||
glam_array!(glam::I16Vec3, [i16; 3]);
|
||||
glam_array!(glam::I16Vec4, [i16; 4]);
|
||||
|
||||
glam_array!(glam::U16Vec2, [u16; 2]);
|
||||
glam_array!(glam::U16Vec3, [u16; 3]);
|
||||
glam_array!(glam::U16Vec4, [u16; 4]);
|
||||
|
||||
glam_array!(glam::IVec2, [i32; 2]);
|
||||
glam_array!(glam::IVec3, [i32; 3]);
|
||||
glam_array!(glam::IVec4, [i32; 4]);
|
||||
|
||||
glam_array!(glam::UVec2, [u32; 2]);
|
||||
glam_array!(glam::UVec3, [u32; 3]);
|
||||
glam_array!(glam::UVec4, [u32; 4]);
|
||||
|
||||
glam_array!(glam::I64Vec2, [i64; 2]);
|
||||
glam_array!(glam::I64Vec3, [i64; 3]);
|
||||
glam_array!(glam::I64Vec4, [i64; 4]);
|
||||
|
||||
glam_array!(glam::U64Vec2, [u64; 2]);
|
||||
glam_array!(glam::U64Vec3, [u64; 3]);
|
||||
glam_array!(glam::U64Vec4, [u64; 4]);
|
||||
|
||||
unsafe impl BufferStruct for glam::Vec3A {
|
||||
type Buffer = [f32; 4];
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
glam::Vec4::to_array(&from.extend(0.))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
glam::Vec3A::from_vec4(glam::Vec4::from_array(from))
|
||||
}
|
||||
}
|
||||
|
||||
/// do NOT use slices, otherwise spirv will fail to compile
|
||||
unsafe impl BufferStruct for glam::Mat3A {
|
||||
type Buffer = [f32; 12];
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
let a = from.to_cols_array();
|
||||
[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], 0., 0., 0.]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
let a = from;
|
||||
glam::Mat3A::from_cols_array(&[a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! I (@firestar99) copied this entire mod from one of my projects, as I haven't uploaded that lib to crates. Hopefully
|
||||
//! rust-gpu improves and this entire thing becomes unnecessary in the future.
|
||||
//!
|
||||
//! https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
|
||||
|
||||
use bytemuck::Pod;
|
||||
|
||||
mod glam;
|
||||
mod primitive;
|
||||
|
||||
/// A BufferStruct is a "parallel representation" of the original struct with some fundamental types remapped. This
|
||||
/// struct hierarchy represents how data is stored in GPU Buffers, where all types must be [`Pod`] to allow
|
||||
/// transmuting them to `&[u8]` with [`bytemuck`].
|
||||
///
|
||||
/// Notable type remappings (original: buffer):
|
||||
/// * bool: u32 of 0 or 1
|
||||
/// * any repr(u32) enum: u32 with remapping via [`num_enum`]
|
||||
///
|
||||
/// By adding `#[derive(ShaderStruct)]` to your struct (or enum), a parallel `{name}Buffer` struct is created with all
|
||||
/// the members of the original struct, but with their types using the associated remapped types as specified by this
|
||||
/// trait.
|
||||
///
|
||||
/// # Origin
|
||||
/// I (@firestar99) copied this entire mod from my [Nanite-at-home] project, specifically the [buffer-content] crate
|
||||
/// and the [buffer_struct] proc macro. The variant here has quite some modifications, to both cleaned up some of the
|
||||
/// mistakes my implementation has and to customize it a bit for graphite.
|
||||
///
|
||||
/// Hopefully rust-gpu improves to the point where this remapping becomes unnecessary.
|
||||
///
|
||||
/// [Nanite-at-home]: https://github.com/Firestar99/nanite-at-home
|
||||
/// [buffer-content]: https://github.com/Firestar99/nanite-at-home/tree/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/buffer-content
|
||||
/// [buffer_struct]: https://github.com/Firestar99/nanite-at-home/blob/008dac8df656959c71efeddd2d3ddabcb801771c/rust-gpu-bindless/crates/macros/src/buffer_struct.rs
|
||||
///
|
||||
/// # Safety
|
||||
/// The associated type Transfer must be the same on all targets. Writing followed by reading back a value must result
|
||||
/// in the same value.
|
||||
pub unsafe trait BufferStruct: Copy + Send + Sync + 'static {
|
||||
type Buffer: Pod + Send + Sync;
|
||||
|
||||
fn write(from: Self) -> Self::Buffer;
|
||||
|
||||
fn read(from: Self::Buffer) -> Self;
|
||||
}
|
||||
|
||||
/// Trait marking all [`BufferStruct`] whose read and write methods are identity. While [`BufferStruct`] only
|
||||
/// requires `t == read(write(t))`, this trait additionally requires `t == read(t) == write(t)`. As this removes the
|
||||
/// conversion requirement for writing to or reading from a buffer, one can acquire slices from buffers created of these
|
||||
/// types.
|
||||
///
|
||||
/// Implementing this type is completely safe due to the [`Pod`] requirement.
|
||||
pub trait BufferStructIdentity: Pod + Send + Sync {}
|
||||
|
||||
unsafe impl<T: BufferStructIdentity> BufferStruct for T {
|
||||
type Buffer = Self;
|
||||
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
from
|
||||
}
|
||||
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
from
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use crate::shaders::buffer_struct::{BufferStruct, BufferStructIdentity};
|
||||
use bytemuck::Pod;
|
||||
use core::marker::PhantomData;
|
||||
use core::num::Wrapping;
|
||||
use spirv_std::arch::IndexUnchecked;
|
||||
|
||||
macro_rules! identity {
|
||||
($t:ty) => {
|
||||
impl BufferStructIdentity for $t {}
|
||||
};
|
||||
}
|
||||
|
||||
identity!(());
|
||||
identity!(u8);
|
||||
identity!(u16);
|
||||
identity!(u32);
|
||||
identity!(u64);
|
||||
identity!(u128);
|
||||
identity!(usize);
|
||||
identity!(i8);
|
||||
identity!(i16);
|
||||
identity!(i32);
|
||||
identity!(i64);
|
||||
identity!(i128);
|
||||
identity!(isize);
|
||||
identity!(f32);
|
||||
identity!(f64);
|
||||
|
||||
identity!(spirv_std::arch::SubgroupMask);
|
||||
identity!(spirv_std::memory::Semantics);
|
||||
identity!(spirv_std::ray_tracing::RayFlags);
|
||||
identity!(spirv_std::indirect_command::DrawIndirectCommand);
|
||||
identity!(spirv_std::indirect_command::DrawIndexedIndirectCommand);
|
||||
identity!(spirv_std::indirect_command::DispatchIndirectCommand);
|
||||
identity!(spirv_std::indirect_command::DrawMeshTasksIndirectCommandEXT);
|
||||
identity!(spirv_std::indirect_command::TraceRaysIndirectCommandKHR);
|
||||
// not pod
|
||||
// identity!(spirv_std::indirect_command::TraceRaysIndirectCommand2KHR);
|
||||
|
||||
unsafe impl BufferStruct for bool {
|
||||
type Buffer = u32;
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
from as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
from != 0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: BufferStruct> BufferStruct for Wrapping<T>
|
||||
where
|
||||
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
|
||||
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
|
||||
// see https://github.com/Lokathor/bytemuck/issues/164
|
||||
T::Buffer: Pod,
|
||||
{
|
||||
type Buffer = Wrapping<T::Buffer>;
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
Wrapping(T::write(from.0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
Wrapping(T::read(from.0))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: BufferStruct + 'static> BufferStruct for PhantomData<T> {
|
||||
type Buffer = PhantomData<T>;
|
||||
|
||||
#[inline]
|
||||
fn write(_: Self) -> Self::Buffer {
|
||||
PhantomData {}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(_: Self::Buffer) -> Self {
|
||||
PhantomData {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Potential problem: you can't impl this for an array of BufferStruct, as it'll conflict with this impl due to the
|
||||
/// blanket impl on all BufferStructPlain types.
|
||||
unsafe impl<T: BufferStruct, const N: usize> BufferStruct for [T; N]
|
||||
where
|
||||
// rust-gpu does not like `[T; N].map()` nor `core::array::from_fn()` nor transmuting arrays with a const generic
|
||||
// length, so for now we need to require T: Default and T::Transfer: Default for all arrays.
|
||||
T: Default,
|
||||
// unfortunately has to be Pod, even though AnyBitPattern would be sufficient,
|
||||
// due to bytemuck doing `impl<T: Pod> AnyBitPattern for T {}`
|
||||
// see https://github.com/Lokathor/bytemuck/issues/164
|
||||
T::Buffer: Pod + Default,
|
||||
{
|
||||
type Buffer = [T::Buffer; N];
|
||||
|
||||
#[inline]
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
unsafe {
|
||||
let mut ret = [T::Buffer::default(); N];
|
||||
for i in 0..N {
|
||||
*ret.index_unchecked_mut(i) = T::write(*from.index_unchecked(i));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
unsafe {
|
||||
let mut ret = [T::default(); N];
|
||||
for i in 0..N {
|
||||
*ret.index_unchecked_mut(i) = T::read(*from.index_unchecked(i));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_bool() {
|
||||
for x in [false, true] {
|
||||
assert_eq!(x, <bool as BufferStruct>::read(<bool as BufferStruct>::write(x)));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
node-graph/libraries/no-std-types/src/shaders/mod.rs
Normal file
10
node-graph/libraries/no-std-types/src/shaders/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! supporting infrastructure for shaders
|
||||
|
||||
pub mod buffer_struct;
|
||||
|
||||
pub mod __private {
|
||||
pub use bytemuck;
|
||||
pub use glam;
|
||||
pub use num_enum;
|
||||
pub use spirv_std;
|
||||
}
|
||||
29
node-graph/libraries/raster-types/Cargo.toml
Normal file
29
node-graph/libraries/raster-types/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "raster-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Raster data types for Graphene node system"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
wgpu = ["dep:wgpu"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
image = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
wgpu = { workspace = true, optional = true }
|
||||
541
node-graph/libraries/raster-types/src/image.rs
Normal file
541
node-graph/libraries/raster-types/src/image.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
use crate::raster_types::{CPU, Raster};
|
||||
use crate::{Bitmap, BitmapMut};
|
||||
use core_types::AlphaBlending;
|
||||
use core_types::Color;
|
||||
use core_types::color::float_to_srgb_u8;
|
||||
use core_types::table::{Table, TableRow};
|
||||
// use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently
|
||||
use core::hash::{Hash, Hasher};
|
||||
use core_types::color::*;
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::vec::Vec;
|
||||
|
||||
mod base64_serde {
|
||||
//! Basic wrapper for [`serde`] to perform [`base64`] encoding
|
||||
|
||||
use base64::Engine;
|
||||
use core_types::color::*;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn as_base64<S: Serializer, P: Pixel>(key: &[P], serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let u8_data = bytemuck::cast_slice(key);
|
||||
let string = base64::engine::general_purpose::STANDARD.encode(u8_data);
|
||||
(key.len() as u64, string).serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn from_base64<'a, D: Deserializer<'a>, P: Pixel>(deserializer: D) -> Result<Vec<P>, D::Error> {
|
||||
use serde::de::Error;
|
||||
<(u64, &[u8])>::deserialize(deserializer)
|
||||
.and_then(|(len, str)| {
|
||||
let mut output: Vec<P> = vec![P::zeroed(); len as usize];
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode_slice(str, bytemuck::cast_slice_mut(output.as_mut_slice()))
|
||||
.map_err(|err| Error::custom(err.to_string()))?;
|
||||
|
||||
Ok(output)
|
||||
})
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Default, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Image<P: Pixel> {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
#[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.
|
||||
#[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).
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct TransformImage(pub DAffine2);
|
||||
|
||||
impl Hash for TransformImage {
|
||||
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
|
||||
}
|
||||
|
||||
impl<P: Pixel + std::fmt::Debug> std::fmt::Debug for Image<P> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let length = self.data.len();
|
||||
f.debug_struct("Image")
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
.field("data", if length < 100 { &self.data } else { &length })
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P> StaticType for Image<P>
|
||||
where
|
||||
P: dyn_any::StaticTypeSized + Pixel,
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = Image<P::Static>;
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> Bitmap for Image<P> {
|
||||
type Pixel = P;
|
||||
#[inline(always)]
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
|
||||
self.data.get((x + y * self.width) as usize).copied()
|
||||
}
|
||||
#[inline(always)]
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
#[inline(always)]
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> BitmapMut for Image<P> {
|
||||
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut P> {
|
||||
self.data.get_mut((x + y * self.width) as usize)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Evaluate if this will be a problem for our use case.
|
||||
/// Warning: This is an approximation of a hash, and is not guaranteed to not collide.
|
||||
impl<P: Hash + Pixel> Hash for Image<P> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
const HASH_SAMPLES: u64 = 1000;
|
||||
let data_length = self.data.len() as u64;
|
||||
self.width.hash(state);
|
||||
self.height.hash(state);
|
||||
for i in 0..HASH_SAMPLES.min(data_length) {
|
||||
self.data[(i * data_length / HASH_SAMPLES) as usize].hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Pixel> Image<P> {
|
||||
pub fn new(width: u32, height: u32, color: P) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
data: vec![color; (width * height) as usize],
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Image<Color> {
|
||||
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
||||
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
||||
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect();
|
||||
Image {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_png(&self) -> Vec<u8> {
|
||||
use ::image::ImageEncoder;
|
||||
let (data, width, height) = self.to_flat_u8();
|
||||
let mut png = Vec::new();
|
||||
let encoder = ::image::codecs::png::PngEncoder::new(&mut png);
|
||||
encoder.write_image(&data, width, height, ::image::ExtendedColorType::Rgba8).expect("failed to encode image as png");
|
||||
png
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
|
||||
where
|
||||
P::ColorChannel: Linear,
|
||||
<P as Alpha>::AlphaChannel: Linear,
|
||||
{
|
||||
/// Flattens each channel cast to a u8
|
||||
pub fn to_flat_u8(&self) -> (Vec<u8>, u32, u32) {
|
||||
let Image { width, height, data, .. } = self;
|
||||
assert_eq!(data.len(), *width as usize * *height as usize);
|
||||
|
||||
// Cache the last sRGB value we computed, speeds up fills.
|
||||
let mut last_r = 0.;
|
||||
let mut last_r_srgb = 0u8;
|
||||
let mut last_g = 0.;
|
||||
let mut last_g_srgb = 0u8;
|
||||
let mut last_b = 0.;
|
||||
let mut last_b_srgb = 0u8;
|
||||
|
||||
let mut result = vec![0; data.len() * 4];
|
||||
let mut i = 0;
|
||||
for color in data {
|
||||
let a = color.a().to_f32();
|
||||
// Smaller alpha values than this would map to fully transparent
|
||||
// anyway, avoid expensive encoding.
|
||||
if a >= 0.5 / 255. {
|
||||
let undo_premultiply = 1. / a;
|
||||
let r = color.r().to_f32() * undo_premultiply;
|
||||
let g = color.g().to_f32() * undo_premultiply;
|
||||
let b = color.b().to_f32() * undo_premultiply;
|
||||
|
||||
// Compute new sRGB value if necessary.
|
||||
if r != last_r {
|
||||
last_r = r;
|
||||
last_r_srgb = float_to_srgb_u8(r);
|
||||
}
|
||||
if g != last_g {
|
||||
last_g = g;
|
||||
last_g_srgb = float_to_srgb_u8(g);
|
||||
}
|
||||
if b != last_b {
|
||||
last_b = b;
|
||||
last_b_srgb = float_to_srgb_u8(b);
|
||||
}
|
||||
|
||||
result[i] = last_r_srgb;
|
||||
result[i + 1] = last_g_srgb;
|
||||
result[i + 2] = last_b_srgb;
|
||||
result[i + 3] = (a * 255. + 0.5) as u8;
|
||||
}
|
||||
|
||||
i += 4;
|
||||
}
|
||||
|
||||
(result, *width, *height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Pixel> IntoIterator for Image<P> {
|
||||
type Item = P;
|
||||
type IntoIter = std::vec::IntoIter<P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
enum RasterFrame {
|
||||
ImageFrame(Table<Image<Color>>),
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for RasterFrame {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Ok(RasterFrame::ImageFrame(Table::new_from_element(Image::deserialize(deserializer)?)))
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for RasterFrame {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
RasterFrame::ImageFrame(table) => table.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GraphicElement {
|
||||
GraphicGroup(Table<GraphicElement>),
|
||||
RasterFrame(RasterFrame),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ImageFrame<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
}
|
||||
impl From<ImageFrame<Color>> for GraphicElement {
|
||||
fn from(image_frame: ImageFrame<Color>) -> Self {
|
||||
GraphicElement::RasterFrame(RasterFrame::ImageFrame(Table::new_from_element(image_frame.image)))
|
||||
}
|
||||
}
|
||||
impl From<GraphicElement> for ImageFrame<Color> {
|
||||
fn from(element: GraphicElement) -> Self {
|
||||
match element {
|
||||
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
|
||||
image: image.iter().next().unwrap().element.clone(),
|
||||
},
|
||||
_ => panic!("Expected Image, found {element:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P> StaticType for ImageFrame<P>
|
||||
where
|
||||
P: dyn_any::StaticTypeSized + Pixel,
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldImageFrame<P: Pixel> {
|
||||
image: Image<P>,
|
||||
transform: DAffine2,
|
||||
alpha_blending: AlphaBlending,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum FormatVersions {
|
||||
Image(Image<Color>),
|
||||
OldImageFrame(OldImageFrame<Color>),
|
||||
OlderImageFrameTable(OlderTable<ImageFrame<Color>>),
|
||||
OldImageFrameTable(OldTable<ImageFrame<Color>>),
|
||||
OldImageTable(OldTable<Image<Color>>),
|
||||
OldRasterTable(OldTable<Raster<CPU>>),
|
||||
ImageFrameTable(Table<ImageFrame<Color>>),
|
||||
ImageTable(Table<Image<Color>>),
|
||||
RasterTable(Table<Raster<CPU>>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldTable<T> {
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
transform: Vec<DAffine2>,
|
||||
alpha_blending: Vec<AlphaBlending>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OlderTable<T> {
|
||||
id: Vec<u64>,
|
||||
#[serde(alias = "instances", alias = "instance")]
|
||||
element: Vec<T>,
|
||||
}
|
||||
|
||||
fn from_image_table(table: Table<Image<Color>>) -> Table<Raster<CPU>> {
|
||||
Table::new_from_element(Raster::new_cpu(table.iter().next().unwrap().element.clone()))
|
||||
}
|
||||
|
||||
fn old_table_to_new_table<T>(old_table: OldTable<T>) -> Table<T> {
|
||||
old_table
|
||||
.element
|
||||
.into_iter()
|
||||
.zip(old_table.transform.into_iter().zip(old_table.alpha_blending))
|
||||
.map(|(element, (transform, alpha_blending))| TableRow {
|
||||
element,
|
||||
transform,
|
||||
alpha_blending,
|
||||
source_node_id: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn older_table_to_new_table<T>(old_table: OlderTable<T>) -> Table<T> {
|
||||
old_table
|
||||
.element
|
||||
.into_iter()
|
||||
.map(|element| TableRow {
|
||||
element,
|
||||
transform: DAffine2::IDENTITY,
|
||||
alpha_blending: AlphaBlending::default(),
|
||||
source_node_id: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_image_frame_table(image_frame: Table<ImageFrame<Color>>) -> Table<Raster<CPU>> {
|
||||
Table::new_from_element(Raster::new_cpu(
|
||||
image_frame
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(Table::new_from_element(ImageFrame::default()).iter().next().unwrap())
|
||||
.element
|
||||
.image
|
||||
.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
Ok(match FormatVersions::deserialize(deserializer)? {
|
||||
FormatVersions::Image(image) => Table::new_from_element(Raster::new_cpu(image)),
|
||||
FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => {
|
||||
let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image));
|
||||
*image_frame_table.iter_mut().next().unwrap().transform = transform;
|
||||
*image_frame_table.iter_mut().next().unwrap().alpha_blending = alpha_blending;
|
||||
image_frame_table
|
||||
}
|
||||
FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)),
|
||||
FormatVersions::OldImageFrameTable(old_table) => from_image_frame_table(old_table_to_new_table(old_table)),
|
||||
FormatVersions::OldImageTable(old_table) => from_image_table(old_table_to_new_table(old_table)),
|
||||
FormatVersions::OldRasterTable(old_table) => old_table_to_new_table(old_table),
|
||||
FormatVersions::ImageFrameTable(image_frame) => from_image_frame_table(image_frame),
|
||||
FormatVersions::ImageTable(table) => from_image_table(table),
|
||||
FormatVersions::RasterTable(table) => table,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<TableRow<Raster<CPU>>, D::Error> {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
enum RasterFrame {
|
||||
/// A CPU-based bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
|
||||
ImageFrame(Table<Image<Color>>),
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for RasterFrame {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Ok(RasterFrame::ImageFrame(Table::new_from_element(Image::deserialize(deserializer)?)))
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for RasterFrame {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
RasterFrame::ImageFrame(table) => table.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(Table<GraphicElement>),
|
||||
RasterFrame(RasterFrame),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ImageFrame<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
}
|
||||
impl From<ImageFrame<Color>> for GraphicElement {
|
||||
fn from(image_frame: ImageFrame<Color>) -> Self {
|
||||
GraphicElement::RasterFrame(RasterFrame::ImageFrame(Table::new_from_element(image_frame.image)))
|
||||
}
|
||||
}
|
||||
impl From<GraphicElement> for ImageFrame<Color> {
|
||||
fn from(element: GraphicElement) -> Self {
|
||||
match element {
|
||||
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
|
||||
image: image.iter().next().unwrap().element.clone(),
|
||||
},
|
||||
_ => panic!("Expected Image, found {element:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P> StaticType for ImageFrame<P>
|
||||
where
|
||||
P: dyn_any::StaticTypeSized + Pixel,
|
||||
P::Static: Pixel,
|
||||
{
|
||||
type Static = ImageFrame<P::Static>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldImageFrame<P: Pixel> {
|
||||
image: Image<P>,
|
||||
transform: DAffine2,
|
||||
alpha_blending: AlphaBlending,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum FormatVersions {
|
||||
Image(Image<Color>),
|
||||
OldImageFrame(OldImageFrame<Color>),
|
||||
ImageFrameTable(Table<ImageFrame<Color>>),
|
||||
RasterTable(Table<Raster<CPU>>),
|
||||
RasterTableRow(TableRow<Raster<CPU>>),
|
||||
}
|
||||
|
||||
Ok(match FormatVersions::deserialize(deserializer)? {
|
||||
FormatVersions::Image(image) => TableRow {
|
||||
element: Raster::new_cpu(image),
|
||||
..Default::default()
|
||||
},
|
||||
FormatVersions::OldImageFrame(image_frame_with_transform_and_blending) => TableRow {
|
||||
element: Raster::new_cpu(image_frame_with_transform_and_blending.image),
|
||||
transform: image_frame_with_transform_and_blending.transform,
|
||||
alpha_blending: image_frame_with_transform_and_blending.alpha_blending,
|
||||
source_node_id: None,
|
||||
},
|
||||
FormatVersions::ImageFrameTable(image_frame) => TableRow {
|
||||
element: Raster::new_cpu(image_frame.iter().next().unwrap().element.image.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
FormatVersions::RasterTable(image_frame_table) => image_frame_table.into_iter().next().unwrap_or_default(),
|
||||
FormatVersions::RasterTableRow(image_table_row) => image_table_row,
|
||||
})
|
||||
}
|
||||
|
||||
impl<P: std::fmt::Debug + Copy + Pixel> Sample for Image<P> {
|
||||
type Pixel = P;
|
||||
|
||||
// TODO: Improve sampling logic
|
||||
#[inline(always)]
|
||||
fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> {
|
||||
let image_size = DVec2::new(self.width() as f64, self.height() as f64);
|
||||
if pos.x < 0. || pos.y < 0. || pos.x >= image_size.x || pos.y >= image_size.y {
|
||||
return None;
|
||||
}
|
||||
self.get_pixel(pos.x as u32, pos.y as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Copy + Pixel> Image<P> {
|
||||
pub fn get_mut(&mut self, x: usize, y: usize) -> &mut P {
|
||||
&mut self.data[y * (self.width as usize) + x]
|
||||
}
|
||||
|
||||
/// Clamps the provided point to ((0, 0), (ImageSize.x, ImageSize.y)) and returns the closest pixel
|
||||
pub fn sample(&self, position: DVec2) -> P {
|
||||
let x = position.x.clamp(0., self.width as f64 - 1.) as usize;
|
||||
let y = position.y.clamp(0., self.height as f64 - 1.) as usize;
|
||||
|
||||
self.data[x + y * self.width as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Pixel> AsRef<Image<P>> for Image<P> {
|
||||
fn as_ref(&self) -> &Image<P> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Image<Color>> for Image<SRGBA8> {
|
||||
fn from(image: Image<Color>) -> Self {
|
||||
let data = image.data.into_iter().map(|x| x.into()).collect();
|
||||
Self {
|
||||
data,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Image<SRGBA8>> for Image<Color> {
|
||||
fn from(image: Image<SRGBA8>) -> Self {
|
||||
let data = image.data.into_iter().map(|x| x.into()).collect();
|
||||
Self {
|
||||
data,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
base64_string: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_image_serialization_roundtrip() {
|
||||
use super::*;
|
||||
use crate::Color;
|
||||
let image = Image {
|
||||
width: 2,
|
||||
height: 2,
|
||||
data: vec![Color::WHITE, Color::BLACK, Color::RED, Color::GREEN],
|
||||
base64_string: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string(&image).unwrap();
|
||||
println!("{serialized}");
|
||||
let deserialized: Image<Color> = serde_json::from_str(&serialized).unwrap();
|
||||
println!("{deserialized:?}");
|
||||
|
||||
assert_eq!(image, deserialized);
|
||||
}
|
||||
}
|
||||
82
node-graph/libraries/raster-types/src/lib.rs
Normal file
82
node-graph/libraries/raster-types/src/lib.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
pub mod image;
|
||||
pub mod raster_types;
|
||||
|
||||
// Re-exports for convenience
|
||||
pub use image::Image;
|
||||
pub use raster_types::*;
|
||||
|
||||
// Re-export color types from no-std-types
|
||||
pub use core_types::color::*;
|
||||
|
||||
/// as to not yet rename all references
|
||||
pub mod color {
|
||||
pub use super::*;
|
||||
}
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub trait Bitmap {
|
||||
type Pixel: Pixel;
|
||||
fn width(&self) -> u32;
|
||||
fn height(&self) -> u32;
|
||||
fn dimensions(&self) -> (u32, u32) {
|
||||
(self.width(), self.height())
|
||||
}
|
||||
fn dim(&self) -> (u32, u32) {
|
||||
self.dimensions()
|
||||
}
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel>;
|
||||
}
|
||||
|
||||
impl<T: Bitmap> Bitmap for &T {
|
||||
type Pixel = T::Pixel;
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
(**self).width()
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
(**self).height()
|
||||
}
|
||||
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel> {
|
||||
(**self).get_pixel(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Bitmap> Bitmap for &mut T {
|
||||
type Pixel = T::Pixel;
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
(**self).width()
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
(**self).height()
|
||||
}
|
||||
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel> {
|
||||
(**self).get_pixel(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BitmapMut: Bitmap {
|
||||
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel>;
|
||||
fn set_pixel(&mut self, x: u32, y: u32, pixel: Self::Pixel) {
|
||||
*self.get_pixel_mut(x, y).unwrap() = pixel;
|
||||
}
|
||||
fn map_pixels<F: Fn(Self::Pixel) -> Self::Pixel>(&mut self, map_fn: F) {
|
||||
for y in 0..self.height() {
|
||||
for x in 0..self.width() {
|
||||
let pixel = self.get_pixel(x, y).unwrap();
|
||||
self.set_pixel(x, y, map_fn(pixel));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
|
||||
fn get_pixel_mut(&mut self, x: u32, y: u32) -> Option<&mut Self::Pixel> {
|
||||
(*self).get_pixel_mut(x, y)
|
||||
}
|
||||
}
|
||||
227
node-graph/libraries/raster-types/src/raster_types.rs
Normal file
227
node-graph/libraries/raster-types/src/raster_types.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use crate::image::Image;
|
||||
use core::ops::Deref;
|
||||
use core_types::Color;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::math::quad::Quad;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
mod __private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub trait Storage: __private::Sealed + Clone + Debug + 'static {
|
||||
fn is_empty(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash, Default)]
|
||||
pub struct Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
storage: T,
|
||||
}
|
||||
|
||||
unsafe impl<T> dyn_any::StaticType for Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
type Static = Raster<T>;
|
||||
}
|
||||
|
||||
impl<T> Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
pub fn new(t: T) -> Self {
|
||||
Self { storage: t }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.storage
|
||||
}
|
||||
}
|
||||
|
||||
pub use cpu::CPU;
|
||||
|
||||
mod cpu {
|
||||
use super::*;
|
||||
use crate::raster_types::__private::Sealed;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
|
||||
pub struct CPU(Image<Color>);
|
||||
|
||||
impl Sealed for Raster<CPU> {}
|
||||
|
||||
impl Storage for Raster<CPU> {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.height == 0 || self.0.width == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Raster<CPU> {
|
||||
pub fn new_cpu(image: Image<Color>) -> Self {
|
||||
Self::new(CPU(image))
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &Image<Color> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self) -> &mut Image<Color> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_data(self) -> Image<Color> {
|
||||
self.storage.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for CPU {
|
||||
type Target = Image<Color>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for CPU {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Raster<CPU> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(Raster::new_cpu(Image::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Raster<CPU> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use gpu::GPU;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod gpu {
|
||||
use super::*;
|
||||
use crate::raster_types::__private::Sealed;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub struct GPU {
|
||||
pub texture: wgpu::Texture,
|
||||
}
|
||||
|
||||
impl Sealed for Raster<GPU> {}
|
||||
|
||||
impl Storage for Raster<GPU> {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.texture.width() == 0 || self.texture.height() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Raster<GPU> {
|
||||
pub fn new_gpu(texture: wgpu::Texture) -> Self {
|
||||
Self::new(GPU { texture })
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &wgpu::Texture {
|
||||
&self.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
mod gpu {
|
||||
use super::*;
|
||||
use crate::raster_types::__private::Sealed;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||
pub struct GPU;
|
||||
|
||||
impl Sealed for Raster<GPU> {}
|
||||
|
||||
impl Storage for Raster<GPU> {
|
||||
fn is_empty(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod gpu_common {
|
||||
use super::*;
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Raster<GPU> {
|
||||
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Raster<GPU> {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BoundingBox for Raster<T>
|
||||
where
|
||||
Raster<T>: Storage,
|
||||
{
|
||||
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
if self.is_empty() || transform.matrix2.determinant() == 0. {
|
||||
return RenderBoundingBox::None;
|
||||
}
|
||||
|
||||
let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box())
|
||||
}
|
||||
}
|
||||
|
||||
// RenderComplexity trait implementations
|
||||
impl core_types::render_complexity::RenderComplexity for Raster<CPU> {
|
||||
fn render_complexity(&self) -> usize {
|
||||
(self.width * self.height / 500) as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl core_types::render_complexity::RenderComplexity for Raster<GPU> {
|
||||
fn render_complexity(&self) -> usize {
|
||||
// GPU textures currently can't have a thumbnail
|
||||
usize::MAX
|
||||
}
|
||||
}
|
||||
27
node-graph/libraries/rendering/Cargo.toml
Normal file
27
node-graph/libraries/rendering/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "rendering"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "SVG rendering for Graphene"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
glam = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
log = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
|
||||
|
||||
# Workspace dependencies
|
||||
vello = { workspace = true }
|
||||
47
node-graph/libraries/rendering/src/convert_usvg_path.rs
Normal file
47
node-graph/libraries/rendering/src/convert_usvg_path.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use glam::DVec2;
|
||||
use vector_types::subpath::{ManipulatorGroup, Subpath};
|
||||
use vector_types::vector::PointId;
|
||||
|
||||
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
|
||||
let mut subpaths = Vec::new();
|
||||
let mut manipulators_list = Vec::new();
|
||||
|
||||
let mut points = path.data().points().iter();
|
||||
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
|
||||
|
||||
for verb in path.data().verbs() {
|
||||
match verb {
|
||||
usvg::tiny_skia_path::PathVerb::Move => {
|
||||
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false));
|
||||
let Some(start) = points.next().map(to_vec) else { continue };
|
||||
manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Line => {
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Quad => {
|
||||
let Some(handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
if let Some(last) = manipulators_list.last_mut() {
|
||||
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
|
||||
}
|
||||
manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Cubic => {
|
||||
let Some(first_handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(second_handle) = points.next().map(to_vec) else { continue };
|
||||
let Some(end) = points.next().map(to_vec) else { continue };
|
||||
if let Some(last) = manipulators_list.last_mut() {
|
||||
last.out_handle = Some(first_handle);
|
||||
}
|
||||
manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
|
||||
}
|
||||
usvg::tiny_skia_path::PathVerb::Close => {
|
||||
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
subpaths.push(Subpath::new(manipulators_list, false));
|
||||
subpaths
|
||||
}
|
||||
6
node-graph/libraries/rendering/src/lib.rs
Normal file
6
node-graph/libraries/rendering/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod convert_usvg_path;
|
||||
pub mod render_ext;
|
||||
mod renderer;
|
||||
pub mod to_peniko;
|
||||
|
||||
pub use renderer::*;
|
||||
187
node-graph/libraries/rendering/src/render_ext.rs
Normal file
187
node-graph/libraries/rendering/src/render_ext.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use crate::renderer::{RenderParams, format_transform_matrix};
|
||||
use core_types::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use core_types::uuid::generate_uuid;
|
||||
use glam::DAffine2;
|
||||
use graphic_types::vector_types::gradient::{Gradient, GradientType};
|
||||
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, PathStyle, RenderMode, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use std::fmt::Write;
|
||||
|
||||
pub trait RenderExt {
|
||||
type Output;
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> Self::Output;
|
||||
}
|
||||
|
||||
impl RenderExt for Gradient {
|
||||
type Output = u64;
|
||||
|
||||
// /// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, _render_params: &RenderParams) -> Self::Output {
|
||||
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 transform_points = element_transform * stroke_transform * bounds;
|
||||
let start = transform_points.transform_point2(self.start);
|
||||
let end = transform_points.transform_point2(self.end);
|
||||
|
||||
let gradient_transform = if transformed_bounds.matrix2.determinant() != 0. {
|
||||
transformed_bounds.inverse()
|
||||
} else {
|
||||
DAffine2::IDENTITY // Ignore if the transform cannot be inverted (the bounds are zero). See issue #1944.
|
||||
};
|
||||
let gradient_transform = format_transform_matrix(gradient_transform);
|
||||
let gradient_transform = if gradient_transform.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(r#" gradientTransform="{gradient_transform}""#)
|
||||
};
|
||||
|
||||
let gradient_id = generate_uuid();
|
||||
|
||||
match self.gradient_type {
|
||||
GradientType::Linear => {
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
|
||||
gradient_id, start.x, start.y, end.x, 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
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderExt for Fill {
|
||||
type Output = String;
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> Self::Output {
|
||||
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(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderExt for Stroke {
|
||||
type Output = String;
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
fn render(
|
||||
&self,
|
||||
_svg_defs: &mut String,
|
||||
_element_transform: DAffine2,
|
||||
_stroke_transform: DAffine2,
|
||||
_bounds: DAffine2,
|
||||
_transformed_bounds: DAffine2,
|
||||
render_params: &RenderParams,
|
||||
) -> Self::Output {
|
||||
// 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 || render_params.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() && render_params.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
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderExt for PathStyle {
|
||||
type Output = String;
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, render_params: &RenderParams) -> String {
|
||||
let render_mode = render_params.render_mode;
|
||||
match render_mode {
|
||||
RenderMode::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(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, 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(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params))
|
||||
.unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1609
node-graph/libraries/rendering/src/renderer.rs
Normal file
1609
node-graph/libraries/rendering/src/renderer.rs
Normal file
File diff suppressed because it is too large
Load Diff
36
node-graph/libraries/rendering/src/to_peniko.rs
Normal file
36
node-graph/libraries/rendering/src/to_peniko.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use core_types::BlendMode;
|
||||
use vello::peniko;
|
||||
|
||||
pub trait BlendModeExt {
|
||||
fn to_peniko(&self) -> peniko::Mix;
|
||||
}
|
||||
|
||||
impl BlendModeExt for BlendMode {
|
||||
fn to_peniko(&self) -> peniko::Mix {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => peniko::Mix::Normal,
|
||||
// Darken group
|
||||
BlendMode::Darken => peniko::Mix::Darken,
|
||||
BlendMode::Multiply => peniko::Mix::Multiply,
|
||||
BlendMode::ColorBurn => peniko::Mix::ColorBurn,
|
||||
// Lighten group
|
||||
BlendMode::Lighten => peniko::Mix::Lighten,
|
||||
BlendMode::Screen => peniko::Mix::Screen,
|
||||
BlendMode::ColorDodge => peniko::Mix::ColorDodge,
|
||||
// Contrast group
|
||||
BlendMode::Overlay => peniko::Mix::Overlay,
|
||||
BlendMode::SoftLight => peniko::Mix::SoftLight,
|
||||
BlendMode::HardLight => peniko::Mix::HardLight,
|
||||
// Inversion group
|
||||
BlendMode::Difference => peniko::Mix::Difference,
|
||||
BlendMode::Exclusion => peniko::Mix::Exclusion,
|
||||
// Component group
|
||||
BlendMode::Hue => peniko::Mix::Hue,
|
||||
BlendMode::Saturation => peniko::Mix::Saturation,
|
||||
BlendMode::Color => peniko::Mix::Color,
|
||||
BlendMode::Luminosity => peniko::Mix::Luminosity,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
33
node-graph/libraries/vector-types/Cargo.toml
Normal file
33
node-graph/libraries/vector-types/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "vector-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Vector graphics types and algorithms for Graphene"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
core-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
bitflags = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
lyon_geom = { workspace = true }
|
||||
dyn-any = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
log = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
polycool = { workspace = true }
|
||||
tinyvec = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
246
node-graph/libraries/vector-types/src/gradient.rs
Normal file
246
node-graph/libraries/vector-types/src/gradient.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use core_types::{Color, render_complexity::RenderComplexity};
|
||||
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 RenderComplexity for GradientStops {
|
||||
fn render_complexity(&self) -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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, gradient_type: GradientType) -> Self {
|
||||
let stops = GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]);
|
||||
|
||||
Self { start, end, stops, 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 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, 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl core_types::bounds::BoundingBox for GradientStops {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||
core_types::bounds::RenderBoundingBox::Infinite
|
||||
}
|
||||
}
|
||||
20
node-graph/libraries/vector-types/src/lib.rs
Normal file
20
node-graph/libraries/vector-types/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod gradient;
|
||||
pub mod math;
|
||||
pub mod subpath;
|
||||
pub mod vector;
|
||||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use core_types as gcore;
|
||||
pub use gradient::{GradientStops, GradientType};
|
||||
pub use math::{QuadExt, RectExt};
|
||||
pub use subpath::Subpath;
|
||||
pub use vector::Vector;
|
||||
pub use vector::reference_point::ReferencePoint;
|
||||
|
||||
// Re-export dependencies that users of this crate will need
|
||||
pub use dyn_any;
|
||||
pub use glam;
|
||||
pub use kurbo;
|
||||
32
node-graph/libraries/vector-types/src/math/mod.rs
Normal file
32
node-graph/libraries/vector-types/src/math/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::subpath::Bezier;
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use core_types::math::quad::Quad;
|
||||
use core_types::math::rect::Rect;
|
||||
use kurbo::{Line, PathSeg};
|
||||
|
||||
pub trait QuadExt {
|
||||
/// Get all the edges in the rect as linear bezier curves
|
||||
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
|
||||
fn to_lines(&self) -> impl Iterator<Item = PathSeg>;
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn to_lines(&self) -> impl Iterator<Item = PathSeg> {
|
||||
self.all_edges().into_iter().map(|[start, end]| PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(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/libraries/vector-types/src/subpath/consts.rs
Normal file
4
node-graph/libraries/vector-types/src/subpath/consts.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
// Implementation constants
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
440
node-graph/libraries/vector-types/src/subpath/core.rs
Normal file
440
node-graph/libraries/vector-types/src/subpath/core.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
use super::consts::*;
|
||||
use super::*;
|
||||
use crate::vector::misc::{SpiralType, point_to_dvec2};
|
||||
use glam::DVec2;
|
||||
use kurbo::PathSeg;
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
pub struct PathSegPoints {
|
||||
pub p0: DVec2,
|
||||
pub p1: Option<DVec2>,
|
||||
pub p2: Option<DVec2>,
|
||||
pub p3: DVec2,
|
||||
}
|
||||
|
||||
impl PathSegPoints {
|
||||
pub fn new(p0: DVec2, p1: Option<DVec2>, p2: Option<DVec2>, p3: DVec2) -> Self {
|
||||
Self { p0, p1, p2, p3 }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pathseg_points(segment: PathSeg) -> PathSegPoints {
|
||||
match segment {
|
||||
PathSeg::Line(line) => PathSegPoints::new(point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)),
|
||||
PathSeg::Quad(quad) => PathSegPoints::new(point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p2)),
|
||||
PathSeg::Cubic(cube) => PathSegPoints::new(point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p3)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||
impl<PointId: Identifier> Subpath<PointId> {
|
||||
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
|
||||
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
|
||||
#[track_caller]
|
||||
pub fn new(manipulator_groups: Vec<ManipulatorGroup<PointId>>, closed: bool) -> Self {
|
||||
assert!(!closed || !manipulator_groups.is_empty(), "A closed Subpath must contain more than 0 ManipulatorGroups.");
|
||||
Self { manipulator_groups, closed }
|
||||
}
|
||||
|
||||
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||
pub fn from_bezier(segment: PathSeg) -> Self {
|
||||
let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(segment);
|
||||
Subpath::new(vec![ManipulatorGroup::new(p0, None, p1), ManipulatorGroup::new(p3, p2, None)], false)
|
||||
}
|
||||
|
||||
/// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function
|
||||
/// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group.
|
||||
pub fn from_beziers(beziers: &[PathSeg], closed: bool) -> Self {
|
||||
assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier.");
|
||||
if beziers.is_empty() {
|
||||
return Subpath::new(vec![], closed);
|
||||
}
|
||||
|
||||
let beziers: Vec<_> = beziers.iter().map(|b| pathseg_points(*b)).collect();
|
||||
|
||||
let first = beziers.first().unwrap();
|
||||
let mut manipulator_groups = vec![ManipulatorGroup {
|
||||
anchor: first.p0,
|
||||
in_handle: None,
|
||||
out_handle: first.p1,
|
||||
id: PointId::new(),
|
||||
}];
|
||||
let mut inner_groups: Vec<ManipulatorGroup<PointId>> = beziers
|
||||
.windows(2)
|
||||
.map(|bezier_pair| ManipulatorGroup {
|
||||
anchor: bezier_pair[1].p0,
|
||||
in_handle: bezier_pair[0].p2,
|
||||
out_handle: bezier_pair[1].p1,
|
||||
id: PointId::new(),
|
||||
})
|
||||
.collect::<Vec<ManipulatorGroup<PointId>>>();
|
||||
manipulator_groups.append(&mut inner_groups);
|
||||
|
||||
let last = beziers.last().unwrap();
|
||||
if !closed {
|
||||
manipulator_groups.push(ManipulatorGroup {
|
||||
anchor: last.p3,
|
||||
in_handle: last.p2,
|
||||
out_handle: None,
|
||||
id: PointId::new(),
|
||||
});
|
||||
return Subpath::new(manipulator_groups, false);
|
||||
}
|
||||
|
||||
manipulator_groups[0].in_handle = last.p2;
|
||||
Subpath::new(manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.manipulator_groups.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of [ManipulatorGroup]s contained within the `Subpath`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.manipulator_groups.len()
|
||||
}
|
||||
|
||||
/// Returns the number of segments contained within the `Subpath`.
|
||||
pub fn len_segments(&self) -> usize {
|
||||
let mut number_of_curves = self.len();
|
||||
if !self.closed && number_of_curves > 0 {
|
||||
number_of_curves -= 1
|
||||
}
|
||||
number_of_curves
|
||||
}
|
||||
|
||||
/// Returns a copy of the bezier segment at the given segment index, if this segment exists.
|
||||
pub fn get_segment(&self, segment_index: usize) -> Option<PathSeg> {
|
||||
if segment_index >= self.len_segments() {
|
||||
return None;
|
||||
}
|
||||
Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()]))
|
||||
}
|
||||
|
||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||
pub fn iter(&self) -> SubpathIter<'_, PointId> {
|
||||
SubpathIter {
|
||||
subpath: self,
|
||||
index: 0,
|
||||
is_always_closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath.
|
||||
pub fn iter_closed(&self) -> SubpathIter<'_, PointId> {
|
||||
SubpathIter {
|
||||
subpath: self,
|
||||
index: 0,
|
||||
is_always_closed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a slice of the [ManipulatorGroup]s in the `Subpath`.
|
||||
pub fn manipulator_groups(&self) -> &[ManipulatorGroup<PointId>] {
|
||||
&self.manipulator_groups
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`.
|
||||
pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<PointId>> {
|
||||
&mut self.manipulator_groups
|
||||
}
|
||||
|
||||
/// Returns a vector of all the anchors (DVec2) for this `Subpath`.
|
||||
pub fn anchors(&self) -> Vec<DVec2> {
|
||||
self.manipulator_groups().iter().map(|group| group.anchor).collect()
|
||||
}
|
||||
|
||||
/// Returns if the Subpath is equivalent to a single point.
|
||||
pub fn is_point(&self) -> bool {
|
||||
if self.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let point = self.manipulator_groups[0].anchor;
|
||||
self.manipulator_groups
|
||||
.iter()
|
||||
.all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
/// Construct a [Subpath] from an iter of anchor positions.
|
||||
pub fn from_anchors(anchor_positions: impl IntoIterator<Item = DVec2>, closed: bool) -> Self {
|
||||
Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor(anchor)).collect(), closed)
|
||||
}
|
||||
|
||||
pub fn from_anchors_linear(anchor_positions: impl IntoIterator<Item = DVec2>, closed: bool) -> Self {
|
||||
Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor_linear(anchor)).collect(), closed)
|
||||
}
|
||||
|
||||
/// Constructs a rectangle with `corner1` and `corner2` as the two corners.
|
||||
pub fn new_rect(corner1: DVec2, corner2: DVec2) -> Self {
|
||||
Self::from_anchors_linear([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true)
|
||||
}
|
||||
|
||||
/// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`.
|
||||
pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self {
|
||||
if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) {
|
||||
return Self::new_rect(corner1, corner2);
|
||||
}
|
||||
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
||||
|
||||
let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec<ManipulatorGroup<PointId>> {
|
||||
let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
|
||||
let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2;
|
||||
if radius == 0. {
|
||||
return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)];
|
||||
}
|
||||
|
||||
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
|
||||
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
|
||||
let handle_offset = radius * HANDLE_OFFSET_FACTOR;
|
||||
vec![
|
||||
ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())),
|
||||
ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None),
|
||||
]
|
||||
};
|
||||
Self::new(
|
||||
[
|
||||
new_arc(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]),
|
||||
new_arc(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]),
|
||||
new_arc(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]),
|
||||
new_arc(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]),
|
||||
]
|
||||
.concat(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box.
|
||||
pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self {
|
||||
let size = (corner1 - corner2).abs();
|
||||
let center = (corner1 + corner2) / 2.;
|
||||
let top = DVec2::new(center.x, corner1.y);
|
||||
let bottom = DVec2::new(center.x, corner2.y);
|
||||
let left = DVec2::new(corner1.x, center.y);
|
||||
let right = DVec2::new(corner2.x, center.y);
|
||||
|
||||
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
|
||||
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
|
||||
let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5;
|
||||
|
||||
let manipulator_groups = vec![
|
||||
ManipulatorGroup::new(top, Some(top - handle_offset * DVec2::X), Some(top + handle_offset * DVec2::X)),
|
||||
ManipulatorGroup::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)),
|
||||
ManipulatorGroup::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)),
|
||||
ManipulatorGroup::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)),
|
||||
];
|
||||
Self::new(manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Constructs an arc by a `radius`, `angle_start` and `angle_size`. Angles must be in radians. Slice option makes it look like pie or pacman.
|
||||
pub fn new_arc(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> Self {
|
||||
// Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute
|
||||
let start_angle = start_angle % (std::f64::consts::TAU * 2.);
|
||||
let sweep_angle = sweep_angle % (std::f64::consts::TAU * 2.);
|
||||
|
||||
let original_start_angle = start_angle;
|
||||
let sweep_angle_sign = sweep_angle.signum();
|
||||
|
||||
let mut start_angle = 0.;
|
||||
let mut sweep_angle = sweep_angle.abs();
|
||||
|
||||
if ((sweep_angle / std::f64::consts::TAU).floor() as u32).is_multiple_of(2) {
|
||||
sweep_angle %= std::f64::consts::TAU;
|
||||
} else {
|
||||
start_angle = sweep_angle % std::f64::consts::TAU;
|
||||
sweep_angle = std::f64::consts::TAU - start_angle;
|
||||
}
|
||||
|
||||
sweep_angle *= sweep_angle_sign;
|
||||
start_angle *= sweep_angle_sign;
|
||||
start_angle += original_start_angle;
|
||||
|
||||
let closed = arc_type == ArcType::Closed;
|
||||
let slice = arc_type == ArcType::PieSlice;
|
||||
|
||||
let center = DVec2::new(0., 0.);
|
||||
let segments = (sweep_angle.abs() / (std::f64::consts::PI / 4.)).ceil().max(1.) as usize;
|
||||
let step = sweep_angle / segments as f64;
|
||||
let factor = 4. / 3. * (step / 2.).sin() / (1. + (step / 2.).cos());
|
||||
|
||||
let mut manipulator_groups = Vec::with_capacity(segments);
|
||||
let mut prev_in_handle = None;
|
||||
let mut prev_end = DVec2::new(0., 0.);
|
||||
|
||||
for i in 0..segments {
|
||||
let start_angle = start_angle + step * i as f64;
|
||||
let end_angle = start_angle + step;
|
||||
let start_vec = DVec2::from_angle(start_angle);
|
||||
let end_vec = DVec2::from_angle(end_angle);
|
||||
|
||||
let start = center + radius * start_vec;
|
||||
let end = center + radius * end_vec;
|
||||
|
||||
let handle_start = start + start_vec.perp() * radius * factor;
|
||||
let handle_end = end - end_vec.perp() * radius * factor;
|
||||
|
||||
manipulator_groups.push(ManipulatorGroup::new(start, prev_in_handle, Some(handle_start)));
|
||||
prev_in_handle = Some(handle_end);
|
||||
prev_end = end;
|
||||
}
|
||||
manipulator_groups.push(ManipulatorGroup::new(prev_end, prev_in_handle, None));
|
||||
|
||||
if slice {
|
||||
manipulator_groups.push(ManipulatorGroup::new(center, None, None));
|
||||
}
|
||||
|
||||
Self::new(manipulator_groups, closed || slice)
|
||||
}
|
||||
|
||||
/// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex.
|
||||
pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self {
|
||||
let sides = sides.max(3);
|
||||
let angle_increment = std::f64::consts::TAU / (sides as f64);
|
||||
let anchor_positions = (0..sides).map(|i| {
|
||||
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
|
||||
let center = center + DVec2::ONE * radius;
|
||||
DVec2::new(center.x + radius * f64::cos(angle), center.y + radius * f64::sin(angle)) * 0.5
|
||||
});
|
||||
Self::from_anchors(anchor_positions, true)
|
||||
}
|
||||
|
||||
/// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`.
|
||||
pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self {
|
||||
let sides = sides.max(2);
|
||||
let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64);
|
||||
let anchor_positions = (0..sides * 2).map(|i| {
|
||||
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
|
||||
let center = center + DVec2::ONE * radius;
|
||||
let r = if i % 2 == 0 { radius } else { inner_radius };
|
||||
DVec2::new(center.x + r * f64::cos(angle), center.y + r * f64::sin(angle)) * 0.5
|
||||
});
|
||||
Self::from_anchors(anchor_positions, true)
|
||||
}
|
||||
|
||||
/// Constructs a line from `p1` to `p2`
|
||||
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
|
||||
Self::from_anchors([p1, p2], false)
|
||||
}
|
||||
|
||||
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
|
||||
let mut manipulator_groups = Vec::new();
|
||||
let mut prev_in_handle = None;
|
||||
let theta_end = turns * std::f64::consts::TAU + start_angle;
|
||||
|
||||
let b = calculate_b(a, turns, outer_radius, spiral_type);
|
||||
|
||||
let mut theta = start_angle;
|
||||
while theta < theta_end {
|
||||
let theta_next = f64::min(theta + delta_theta, theta_end);
|
||||
|
||||
let p0 = spiral_point(theta, a, b, spiral_type);
|
||||
let p3 = spiral_point(theta_next, a, b, spiral_type);
|
||||
let t0 = spiral_tangent(theta, a, b, spiral_type);
|
||||
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
|
||||
|
||||
let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type);
|
||||
let d = arc_len / 3.;
|
||||
|
||||
let p1 = p0 + d * t0;
|
||||
let p2 = p3 - d * t1;
|
||||
|
||||
manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1)));
|
||||
prev_in_handle = Some(p2);
|
||||
|
||||
// If final segment, end with anchor at theta_end
|
||||
if (theta_next - theta_end).abs() < f64::EPSILON {
|
||||
manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None));
|
||||
break;
|
||||
}
|
||||
|
||||
theta = theta_next;
|
||||
}
|
||||
|
||||
Self::new(manipulator_groups, false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
|
||||
match spiral_type {
|
||||
SpiralType::Archimedean => {
|
||||
let total_theta = turns * TAU;
|
||||
(outer_radius - a) / total_theta
|
||||
}
|
||||
SpiralType::Logarithmic => {
|
||||
let total_theta = turns * TAU;
|
||||
((outer_radius.abs() / a).ln()) / total_theta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a point on the given spiral type at angle `theta`.
|
||||
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||
match spiral_type {
|
||||
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
|
||||
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tangent direction at angle `theta` for the given spiral type.
|
||||
pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||
match spiral_type {
|
||||
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
|
||||
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes arc length between two angles for the given spiral type.
|
||||
pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
|
||||
match spiral_type {
|
||||
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
|
||||
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a point on a logarithmic spiral at angle `theta`.
|
||||
pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||
let r = a * (b * theta).exp(); // a * e^(bθ)
|
||||
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||
}
|
||||
|
||||
/// Computes arc length along a logarithmic spiral between two angles.
|
||||
pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||
let factor = (1. + b * b).sqrt();
|
||||
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
|
||||
}
|
||||
|
||||
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
|
||||
pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||
let r = a * (b * theta).exp();
|
||||
let dx = r * (b * theta.cos() - theta.sin());
|
||||
let dy = r * (b * theta.sin() + theta.cos());
|
||||
|
||||
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||
}
|
||||
|
||||
/// Returns a point on an Archimedean spiral at angle `theta`.
|
||||
pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||
let r = a + b * theta;
|
||||
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||
}
|
||||
|
||||
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
|
||||
pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||
let r = a + b * theta;
|
||||
let dx = b * theta.cos() - r * theta.sin();
|
||||
let dy = b * theta.sin() + r * theta.cos();
|
||||
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||
}
|
||||
|
||||
/// Computes arc length along an Archimedean spiral between two angles.
|
||||
pub fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
|
||||
}
|
||||
|
||||
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
|
||||
pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
|
||||
let r = a + b * theta;
|
||||
let sqrt_term = (r * r + b * b).sqrt();
|
||||
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
|
||||
}
|
||||
114
node-graph/libraries/vector-types/src/subpath/lookup.rs
Normal file
114
node-graph/libraries/vector-types/src/subpath/lookup.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use super::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use super::*;
|
||||
use crate::vector::algorithms::bezpath_algorithms::pathseg_length_centroid_and_length;
|
||||
use crate::vector::algorithms::intersection::{filtered_all_segment_intersections, pathseg_self_intersections};
|
||||
use core_types::math::polynomial::pathseg_to_parametric_polynomial;
|
||||
use glam::DVec2;
|
||||
|
||||
impl<PointId: Identifier> Subpath<PointId> {
|
||||
/// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point.
|
||||
/// The points will be sorted based on their index and `t` repsectively.
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
///
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
pub fn all_self_intersections(&self, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersections_vec = Vec::new();
|
||||
let err = accuracy.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
|
||||
let num_curves = self.len();
|
||||
// TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection
|
||||
self.iter_closed().enumerate().for_each(|(i, other)| {
|
||||
intersections_vec.extend(pathseg_self_intersections(other, accuracy, minimum_separation).iter().flat_map(|value| [(i, value.0), (i, value.1)]));
|
||||
self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| {
|
||||
intersections_vec.extend(
|
||||
filtered_all_segment_intersections(curve, other, accuracy, minimum_separation)
|
||||
.iter()
|
||||
.filter(|&value| (j != i + 1 || value.0 > err || (1. - value.1) > err) && (j != num_curves - 1 || i != 0 || value.1 > err || (1. - value.0) > err))
|
||||
.flat_map(|value| [(j, value.0), (i, value.1)]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
intersections_vec
|
||||
}
|
||||
|
||||
/// Return the area centroid, together with the area, of the `Subpath` always considering it as a closed subpath. The area will always be a positive value.
|
||||
///
|
||||
/// The area centroid is the center of mass for the area of a solid shape's interior.
|
||||
/// An infinitely flat material forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`.
|
||||
///
|
||||
/// Because the calculation of area and centroid for self-intersecting path requires finding the intersections, the following parameters are used:
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation` - the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
///
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
pub fn area_centroid_and_area(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Option<(DVec2, f64)> {
|
||||
let all_intersections = self.all_self_intersections(error, minimum_separation);
|
||||
let mut current_sign: f64 = 1.;
|
||||
|
||||
let (x_sum, y_sum, area) = self
|
||||
.iter_closed()
|
||||
.enumerate()
|
||||
.map(|(index, bezier)| {
|
||||
let (f_x, f_y) = pathseg_to_parametric_polynomial(bezier);
|
||||
let (f_x, f_y) = (f_x.as_size::<10>().unwrap(), f_y.as_size::<10>().unwrap());
|
||||
let f_y_prime = f_y.derivative();
|
||||
let f_x_prime = f_x.derivative();
|
||||
let f_xy = &f_x * &f_y;
|
||||
|
||||
let mut x_part = &f_xy * &f_x_prime;
|
||||
let mut y_part = &f_xy * &f_y_prime;
|
||||
let mut area_part = &f_x * &f_y_prime;
|
||||
x_part.antiderivative_mut();
|
||||
y_part.antiderivative_mut();
|
||||
area_part.antiderivative_mut();
|
||||
|
||||
let mut curve_sum_x = -current_sign * x_part.eval(0.);
|
||||
let mut curve_sum_y = -current_sign * y_part.eval(0.);
|
||||
let mut curve_sum_area = -current_sign * area_part.eval(0.);
|
||||
for (_, t) in all_intersections.iter().filter(|(i, _)| *i == index) {
|
||||
curve_sum_x += 2. * current_sign * x_part.eval(*t);
|
||||
curve_sum_y += 2. * current_sign * y_part.eval(*t);
|
||||
curve_sum_area += 2. * current_sign * area_part.eval(*t);
|
||||
current_sign *= -1.;
|
||||
}
|
||||
curve_sum_x += current_sign * x_part.eval(1.);
|
||||
curve_sum_y += current_sign * y_part.eval(1.);
|
||||
curve_sum_area += current_sign * area_part.eval(1.);
|
||||
|
||||
(-curve_sum_x, curve_sum_y, curve_sum_area)
|
||||
})
|
||||
.reduce(|(x1, y1, area1), (x2, y2, area2)| (x1 + x2, y1 + y2, area1 + area2))?;
|
||||
|
||||
if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) {
|
||||
return Some((DVec2::NAN, 0.));
|
||||
}
|
||||
|
||||
Some((DVec2::new(x_sum / area, y_sum / area), area.abs()))
|
||||
}
|
||||
|
||||
/// Return the approximation of the length centroid, together with the length, of the `Subpath`.
|
||||
///
|
||||
/// The length centroid is the center of mass for the arc length of the solid shape's perimeter.
|
||||
/// An infinitely thin wire forming the subpath's closed shape would balance at this point.
|
||||
///
|
||||
/// It will return `None` if no manipulator is present.
|
||||
/// - `accuracy` is used to approximate the curve.
|
||||
/// - `always_closed` is to consider the subpath as closed always.
|
||||
pub fn length_centroid_and_length(&self, accuracy: Option<f64>, always_closed: bool) -> Option<(DVec2, f64)> {
|
||||
if always_closed { self.iter_closed() } else { self.iter() }
|
||||
.map(|bezier| pathseg_length_centroid_and_length(bezier, accuracy))
|
||||
.map(|(centroid, length)| (centroid * length, length))
|
||||
.reduce(|(centroid_part1, length1), (centroid_part2, length2)| (centroid_part1 + centroid_part2, length1 + length2))
|
||||
.map(|(centroid_part, length)| (centroid_part / length, length))
|
||||
.map(|(centroid_part, length)| (DVec2::new(centroid_part.x, centroid_part.y), length))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// use super::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
// use super::utils::{SubpathTValue};
|
||||
use super::*;
|
||||
|
||||
impl<PointId: super::structs::Identifier> Subpath<PointId> {
|
||||
/// Get whether the subpath is closed.
|
||||
pub fn closed(&self) -> bool {
|
||||
self.closed
|
||||
}
|
||||
|
||||
/// Set whether the subpath is closed.
|
||||
pub fn set_closed(&mut self, new_closed: bool) {
|
||||
self.closed = new_closed;
|
||||
}
|
||||
|
||||
/// Access a [ManipulatorGroup] from a PointId.
|
||||
pub fn manipulator_from_id(&self, id: PointId) -> Option<&ManipulatorGroup<PointId>> {
|
||||
self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
/// Access a mutable [ManipulatorGroup] from a PointId.
|
||||
pub fn manipulator_mut_from_id(&mut self, id: PointId) -> Option<&mut ManipulatorGroup<PointId>> {
|
||||
self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
/// Access the index of a [ManipulatorGroup] from a PointId.
|
||||
pub fn manipulator_index_from_id(&self, id: PointId) -> Option<usize> {
|
||||
self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
/// Insert a manipulator group at an index.
|
||||
pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup<PointId>) {
|
||||
assert!(group.is_finite(), "Inserting non finite manipulator group");
|
||||
self.manipulator_groups.insert(index, group)
|
||||
}
|
||||
|
||||
/// Push a manipulator group to the end.
|
||||
pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<PointId>) {
|
||||
assert!(group.is_finite(), "Pushing non finite manipulator group");
|
||||
self.manipulator_groups.push(group)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the last manipulator
|
||||
pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup<PointId>> {
|
||||
self.manipulator_groups.last_mut()
|
||||
}
|
||||
|
||||
/// Remove a manipulator group at an index.
|
||||
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<PointId> {
|
||||
self.manipulator_groups.remove(index)
|
||||
}
|
||||
}
|
||||
71
node-graph/libraries/vector-types/src/subpath/mod.rs
Normal file
71
node-graph/libraries/vector-types/src/subpath/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
mod consts;
|
||||
mod core;
|
||||
mod lookup;
|
||||
mod manipulators;
|
||||
mod solvers;
|
||||
mod structs;
|
||||
mod transform;
|
||||
|
||||
pub use core::*;
|
||||
use kurbo::PathSeg;
|
||||
use std::fmt::{Debug, Formatter, Result};
|
||||
use std::ops::{Index, IndexMut};
|
||||
pub use structs::*;
|
||||
|
||||
/// Structure used to represent a path composed of [Bezier] curves.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Subpath<PointId: Identifier> {
|
||||
manipulator_groups: Vec<ManipulatorGroup<PointId>>,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||
pub struct SubpathIter<'a, PointId: Identifier> {
|
||||
index: usize,
|
||||
subpath: &'a Subpath<PointId>,
|
||||
is_always_closed: bool,
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> Index<usize> for Subpath<PointId> {
|
||||
type Output = ManipulatorGroup<PointId>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
|
||||
&self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> IndexMut<usize> for Subpath<PointId> {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
|
||||
&mut self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> Iterator for SubpathIter<'_, PointId> {
|
||||
type Item = PathSeg;
|
||||
|
||||
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.subpath.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let closed = if self.is_always_closed { true } else { self.subpath.closed };
|
||||
let len = self.subpath.len() - 1 + if closed { 1 } else { 0 };
|
||||
if self.index >= len {
|
||||
return None;
|
||||
}
|
||||
let start_index = self.index;
|
||||
let end_index = (self.index + 1) % self.subpath.len();
|
||||
self.index += 1;
|
||||
|
||||
Some(self.subpath[start_index].to_bezier(&self.subpath[end_index]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> Debug for Subpath<PointId> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
|
||||
}
|
||||
}
|
||||
83
node-graph/libraries/vector-types/src/subpath/solvers.rs
Normal file
83
node-graph/libraries/vector-types/src/subpath/solvers.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::subpath::{Identifier, Subpath};
|
||||
use crate::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use glam::DVec2;
|
||||
use kurbo::{Affine, BezPath, Shape};
|
||||
|
||||
impl<PointId: Identifier> Subpath<PointId> {
|
||||
pub fn contains_point(&self, point: DVec2) -> bool {
|
||||
self.to_bezpath().contains(dvec2_to_point(point))
|
||||
}
|
||||
|
||||
pub fn to_bezpath(&self) -> BezPath {
|
||||
let mut bezpath = kurbo::BezPath::new();
|
||||
let mut out_handle;
|
||||
|
||||
let Some(first) = self.manipulator_groups.first() else { return bezpath };
|
||||
bezpath.move_to(dvec2_to_point(first.anchor));
|
||||
out_handle = first.out_handle;
|
||||
|
||||
for manipulator in self.manipulator_groups.iter().skip(1) {
|
||||
match (out_handle, manipulator.in_handle) {
|
||||
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
|
||||
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
|
||||
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||
}
|
||||
out_handle = manipulator.out_handle;
|
||||
}
|
||||
|
||||
if self.closed {
|
||||
match (out_handle, first.in_handle) {
|
||||
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
|
||||
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
|
||||
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||
}
|
||||
bezpath.close_path();
|
||||
}
|
||||
bezpath
|
||||
}
|
||||
|
||||
/// Returns `true` if this subpath is completely inside the `other` subpath.
|
||||
pub fn is_inside_subpath(&self, other: &Subpath<PointId>, accuracy: Option<f64>, minimum_separation: Option<f64>) -> bool {
|
||||
bezpath_is_inside_bezpath(&self.to_bezpath(), &other.to_bezpath(), accuracy, minimum_separation)
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty.
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.iter()
|
||||
.map(|bezier| bezier.bounding_box())
|
||||
.map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())])
|
||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform.
|
||||
pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.iter()
|
||||
.map(|bezier| (Affine::new(transform.to_cols_array()) * bezier).bounding_box())
|
||||
.map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())])
|
||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the loose bounding box of the subpath (bounding box of all handles and anchors).
|
||||
pub fn loose_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.manipulator_groups
|
||||
.iter()
|
||||
.flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)])
|
||||
.flatten()
|
||||
.map(|pos| [pos, pos])
|
||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the loose bounding box of the subpath, after a given affine transform.
|
||||
pub fn loose_bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.manipulator_groups
|
||||
.iter()
|
||||
.flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)])
|
||||
.flatten()
|
||||
.map(|pos| transform.transform_point2(pos))
|
||||
.map(|pos| [pos, pos])
|
||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
}
|
||||
415
node-graph/libraries/vector-types/src/subpath/structs.rs
Normal file
415
node-graph/libraries/vector-types/src/subpath/structs.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
use crate::vector::algorithms::intersection::filtered_segment_intersections;
|
||||
use crate::vector::misc::{dvec2_to_point, handles_to_segment};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{CubicBez, Line, PathSeg, QuadBez, Shape};
|
||||
use std::fmt::{Debug, Formatter, Result};
|
||||
use std::hash::Hash;
|
||||
|
||||
/// An id type used for each [ManipulatorGroup].
|
||||
pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static {
|
||||
fn new() -> Self;
|
||||
}
|
||||
|
||||
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ManipulatorGroup<PointId: Identifier> {
|
||||
pub anchor: DVec2,
|
||||
pub in_handle: Option<DVec2>,
|
||||
pub out_handle: Option<DVec2>,
|
||||
pub id: PointId,
|
||||
}
|
||||
|
||||
// TODO: Remove once we no longer need to hash floats in Graphite
|
||||
impl<PointId: Identifier> Hash for ManipulatorGroup<PointId> {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.in_handle.is_some().hash(state);
|
||||
if let Some(in_handle) = self.in_handle {
|
||||
in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
}
|
||||
self.out_handle.is_some().hash(state);
|
||||
if let Some(out_handle) = self.out_handle {
|
||||
out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
}
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> Debug for ManipulatorGroup<PointId> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.debug_struct("ManipulatorGroup")
|
||||
.field("anchor", &self.anchor)
|
||||
.field("in_handle", &self.in_handle)
|
||||
.field("out_handle", &self.out_handle)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<PointId: Identifier> ManipulatorGroup<PointId> {
|
||||
/// Construct a new manipulator group from an anchor, in handle and out handle
|
||||
pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> Self {
|
||||
let id = PointId::new();
|
||||
Self { anchor, in_handle, out_handle, id }
|
||||
}
|
||||
|
||||
/// Construct a new manipulator point with just an anchor position
|
||||
pub fn new_anchor(anchor: DVec2) -> Self {
|
||||
Self::new(anchor, Some(anchor), Some(anchor))
|
||||
}
|
||||
|
||||
pub fn new_anchor_linear(anchor: DVec2) -> Self {
|
||||
Self::new(anchor, None, None)
|
||||
}
|
||||
|
||||
/// Construct a new manipulator group from an anchor, in handle, out handle and an id
|
||||
pub fn new_with_id(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>, id: PointId) -> Self {
|
||||
Self { anchor, in_handle, out_handle, id }
|
||||
}
|
||||
|
||||
/// Construct a new manipulator point with just an anchor position and an id
|
||||
pub fn new_anchor_with_id(anchor: DVec2, id: PointId) -> Self {
|
||||
Self::new_with_id(anchor, Some(anchor), Some(anchor), id)
|
||||
}
|
||||
|
||||
/// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group.
|
||||
pub fn to_bezier(&self, end_group: &ManipulatorGroup<PointId>) -> PathSeg {
|
||||
let start = self.anchor;
|
||||
let end = end_group.anchor;
|
||||
let out_handle = self.out_handle;
|
||||
let in_handle = end_group.in_handle;
|
||||
|
||||
match (out_handle, in_handle) {
|
||||
(Some(handle1), Some(handle2)) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle1), dvec2_to_point(handle2), dvec2_to_point(end))),
|
||||
(Some(handle), None) | (None, Some(handle)) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(handle), dvec2_to_point(end))),
|
||||
(None, None) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a transformation to all of the [ManipulatorGroup] points
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
self.anchor = affine_transform.transform_point2(self.anchor);
|
||||
self.in_handle = self.in_handle.map(|in_handle| affine_transform.transform_point2(in_handle));
|
||||
self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle));
|
||||
}
|
||||
|
||||
/// Are all handles at finite positions
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.anchor.is_finite() && self.in_handle.is_none_or(|handle| handle.is_finite()) && self.out_handle.is_none_or(|handle| handle.is_finite())
|
||||
}
|
||||
|
||||
/// Reverse directions of handles
|
||||
pub fn flip(mut self) -> Self {
|
||||
std::mem::swap(&mut self.in_handle, &mut self.out_handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_in_handle(&self) -> bool {
|
||||
self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn has_out_handle(&self) -> bool {
|
||||
self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn has_handle(anchor: DVec2, handle: DVec2) -> bool {
|
||||
!((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum AppendType {
|
||||
IgnoreStart,
|
||||
SmoothJoin(f64),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ArcType {
|
||||
Open,
|
||||
Closed,
|
||||
PieSlice,
|
||||
}
|
||||
|
||||
/// Representation of the handle point(s) in a bezier segment.
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum BezierHandles {
|
||||
Linear,
|
||||
/// Handles for a quadratic curve.
|
||||
Quadratic {
|
||||
/// Point representing the location of the single handle.
|
||||
handle: DVec2,
|
||||
},
|
||||
/// Handles for a cubic curve.
|
||||
Cubic {
|
||||
/// Point representing the location of the handle associated to the start point.
|
||||
handle_start: DVec2,
|
||||
/// Point representing the location of the handle associated to the end point.
|
||||
handle_end: DVec2,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::hash::Hash for BezierHandles {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::mem::discriminant(self).hash(state);
|
||||
match self {
|
||||
BezierHandles::Linear => {}
|
||||
BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BezierHandles {
|
||||
pub fn is_cubic(&self) -> bool {
|
||||
matches!(self, Self::Cubic { .. })
|
||||
}
|
||||
|
||||
pub fn is_finite(&self) -> bool {
|
||||
match self {
|
||||
BezierHandles::Linear => true,
|
||||
BezierHandles::Quadratic { handle } => handle.is_finite(),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => handle_start.is_finite() && handle_end.is_finite(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
|
||||
pub fn start(&self) -> Option<DVec2> {
|
||||
match *self {
|
||||
BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
|
||||
pub fn end(&self) -> Option<DVec2> {
|
||||
match *self {
|
||||
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_start(&mut self, delta: DVec2) {
|
||||
if let BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } = self {
|
||||
*handle_start += delta
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_end(&mut self, delta: DVec2) {
|
||||
if let BezierHandles::Cubic { handle_end, .. } = self {
|
||||
*handle_end += delta
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier.
|
||||
#[must_use]
|
||||
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self {
|
||||
match *self {
|
||||
BezierHandles::Linear => Self::Linear,
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let handle = transformation_function(handle);
|
||||
Self::Quadratic { handle }
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let handle_start = transformation_function(handle_start);
|
||||
let handle_end = transformation_function(handle_end);
|
||||
Self::Cubic { handle_start, handle_end }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reversed(self) -> Self {
|
||||
match self {
|
||||
BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic {
|
||||
handle_start: handle_end,
|
||||
handle_end: handle_start,
|
||||
},
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a bezier curve with 2D points.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Bezier {
|
||||
/// Start point of the bezier curve.
|
||||
pub start: DVec2,
|
||||
/// End point of the bezier curve.
|
||||
pub end: DVec2,
|
||||
/// Handles of the bezier curve.
|
||||
pub handles: BezierHandles,
|
||||
}
|
||||
|
||||
impl Debug for Bezier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
let mut debug_struct = f.debug_struct("Bezier");
|
||||
let mut debug_struct_ref = debug_struct.field("start", &self.start);
|
||||
debug_struct_ref = match self.handles {
|
||||
BezierHandles::Linear => debug_struct_ref,
|
||||
BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end),
|
||||
};
|
||||
debug_struct_ref.field("end", &self.end).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Functionality for the getters and setters of the various points in a Bezier
|
||||
impl Bezier {
|
||||
/// Set the coordinates of the start point.
|
||||
pub fn set_start(&mut self, s: DVec2) {
|
||||
self.start = s;
|
||||
}
|
||||
|
||||
/// Set the coordinates of the end point.
|
||||
pub fn set_end(&mut self, e: DVec2) {
|
||||
self.end = e;
|
||||
}
|
||||
|
||||
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic.
|
||||
pub fn set_handle_start(&mut self, h1: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => {
|
||||
self.handles = BezierHandles::Quadratic { handle: h1 };
|
||||
}
|
||||
BezierHandles::Quadratic { ref mut handle } => {
|
||||
*handle = h1;
|
||||
}
|
||||
BezierHandles::Cubic { ref mut handle_start, .. } => {
|
||||
*handle_start = h1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point.
|
||||
pub fn set_handle_end(&mut self, h2: DVec2) {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => {
|
||||
self.handles = BezierHandles::Cubic {
|
||||
handle_start: self.start,
|
||||
handle_end: h2,
|
||||
};
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
|
||||
}
|
||||
BezierHandles::Cubic { ref mut handle_end, .. } => {
|
||||
*handle_end = h2;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the coordinates of the bezier segment's start point.
|
||||
pub fn start(&self) -> DVec2 {
|
||||
self.start
|
||||
}
|
||||
|
||||
/// Get the coordinates of the bezier segment's end point.
|
||||
pub fn end(&self) -> DVec2 {
|
||||
self.end
|
||||
}
|
||||
|
||||
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
|
||||
pub fn handle_start(&self) -> Option<DVec2> {
|
||||
self.handles.start()
|
||||
}
|
||||
|
||||
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
|
||||
pub fn handle_end(&self) -> Option<DVec2> {
|
||||
self.handles.end()
|
||||
}
|
||||
|
||||
/// Get an iterator over the coordinates of all points in a vector.
|
||||
/// - For a linear segment, the order of the points will be: `start`, `end`.
|
||||
/// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`.
|
||||
/// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
|
||||
pub fn get_points(&self) -> impl Iterator<Item = DVec2> + use<> {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2),
|
||||
BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider removing this function
|
||||
/// Create a linear bezier using the provided coordinates as the start and end points.
|
||||
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Linear,
|
||||
end: DVec2::new(x2, y2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a linear bezier using the provided DVec2s as the start and end points.
|
||||
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Linear,
|
||||
end: p2,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider removing this function
|
||||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
|
||||
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) },
|
||||
end: DVec2::new(x3, y3),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
|
||||
pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Quadratic { handle: p2 },
|
||||
end: p3,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider removing this function
|
||||
/// Create a cubic bezier using the provided coordinates as the start, handles, and end points.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
|
||||
Bezier {
|
||||
start: DVec2::new(x1, y1),
|
||||
handles: BezierHandles::Cubic {
|
||||
handle_start: DVec2::new(x2, y2),
|
||||
handle_end: DVec2::new(x3, y3),
|
||||
},
|
||||
end: DVec2::new(x4, y4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cubic bezier using the provided DVec2s as the start, handles, and end points.
|
||||
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
|
||||
Bezier {
|
||||
start: p1,
|
||||
handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 },
|
||||
end: p4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
|
||||
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
|
||||
Self {
|
||||
start: transformation_function(self.start),
|
||||
end: transformation_function(self.end),
|
||||
handles: self.handles.apply_transformation(transformation_function),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersections(&self, other: &Bezier, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||
let this = handles_to_segment(self.start, self.handles, self.end);
|
||||
let other = handles_to_segment(other.start, other.handles, other.end);
|
||||
filtered_segment_intersections(this, other, accuracy, minimum_separation)
|
||||
}
|
||||
|
||||
pub fn winding(&self, point: DVec2) -> i32 {
|
||||
let this = handles_to_segment(self.start, self.handles, self.end);
|
||||
this.winding(dvec2_to_point(point))
|
||||
}
|
||||
}
|
||||
62
node-graph/libraries/vector-types/src/subpath/transform.rs
Normal file
62
node-graph/libraries/vector-types/src/subpath/transform.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use super::structs::Identifier;
|
||||
use super::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
|
||||
impl<PointId: Identifier> Subpath<PointId> {
|
||||
/// Returns [ManipulatorGroup]s with a reversed winding order.
|
||||
fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>]) -> Vec<ManipulatorGroup<PointId>> {
|
||||
manipulator_groups
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|group| ManipulatorGroup {
|
||||
anchor: group.anchor,
|
||||
in_handle: group.out_handle,
|
||||
out_handle: group.in_handle,
|
||||
id: PointId::new(),
|
||||
})
|
||||
.collect::<Vec<ManipulatorGroup<PointId>>>()
|
||||
}
|
||||
|
||||
/// Returns a [Subpath] with a reversed winding order.
|
||||
/// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction
|
||||
pub fn reverse(&self) -> Subpath<PointId> {
|
||||
let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups());
|
||||
if self.closed {
|
||||
reversed.rotate_right(1);
|
||||
};
|
||||
Subpath {
|
||||
manipulator_groups: reversed,
|
||||
closed: self.closed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a transformation to all of the [ManipulatorGroup]s in the [Subpath].
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
for manipulator_group in &mut self.manipulator_groups {
|
||||
manipulator_group.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians).
|
||||
pub fn rotate(&self, angle: f64) -> Subpath<PointId> {
|
||||
let mut rotated_subpath = self.clone();
|
||||
|
||||
let affine_transform: DAffine2 = DAffine2::from_angle(angle);
|
||||
rotated_subpath.apply_transform(affine_transform);
|
||||
|
||||
rotated_subpath
|
||||
}
|
||||
|
||||
/// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians).
|
||||
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath<PointId> {
|
||||
// Translate before and after the rotation to account for the pivot
|
||||
let translate: DAffine2 = DAffine2::from_translation(pivot);
|
||||
let rotate: DAffine2 = DAffine2::from_angle(angle);
|
||||
let translate_inverse = translate.inverse();
|
||||
|
||||
let mut rotated_subpath = self.clone();
|
||||
rotated_subpath.apply_transform(translate * rotate * translate_inverse);
|
||||
rotated_subpath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
use super::intersection::bezpath_intersections;
|
||||
use super::poisson_disk::poisson_disk_sample;
|
||||
use super::util::pathseg_tangent;
|
||||
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
|
||||
use core_types::math::polynomial::pathseg_to_parametric_polynomial;
|
||||
use glam::{DMat2, DVec2};
|
||||
use kurbo::common::{solve_cubic, solve_quadratic};
|
||||
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let segment = 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))
|
||||
}
|
||||
|
||||
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t_value: TValue) -> Option<(BezPath, BezPath)> {
|
||||
if bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = eval_bezpath(bezpath, t_value, None);
|
||||
split_bezpath_at_segment(bezpath, segment_index, t)
|
||||
}
|
||||
|
||||
pub fn evaluate_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
|
||||
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
||||
}
|
||||
|
||||
pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
|
||||
match segment {
|
||||
PathSeg::Line(line) => line.deriv().eval(t),
|
||||
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// Calculate the total length of the collected segments.
|
||||
let total_length: f64 = segments_length.iter().sum();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const SAFETY_MAX_COUNT: f64 = 10_000. - 1.;
|
||||
|
||||
// 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.
|
||||
if sample_count < 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Decide how many loop-iterations: if closed, skip the last duplicate point
|
||||
let sample_count_usize = sample_count as usize;
|
||||
let max_i = if was_closed { sample_count_usize } else { sample_count_usize + 1 };
|
||||
|
||||
// Generate points along the path based on calculated intervals.
|
||||
let mut length_up_to_previous_segment = 0.;
|
||||
let mut next_segment_index = 0;
|
||||
|
||||
for count in 0..max_i {
|
||||
let fraction = count as f64 / sample_count;
|
||||
let length_up_to_next_sample_point = fraction * used_length + start_offset;
|
||||
let mut next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
|
||||
let mut next_segment_length = segments_length[next_segment_index];
|
||||
|
||||
// Keep moving to the next segment while the length up to the next sample point is greater than the length up to the current segment.
|
||||
while next_length > next_segment_length {
|
||||
if next_segment_index == segments_length.len() - 1 {
|
||||
break;
|
||||
}
|
||||
length_up_to_previous_segment += next_segment_length;
|
||||
next_length = length_up_to_next_sample_point - length_up_to_previous_segment;
|
||||
next_segment_index += 1;
|
||||
next_segment_length = segments_length[next_segment_index];
|
||||
}
|
||||
|
||||
let t = (next_length / next_segment_length).clamp(0., 1.);
|
||||
|
||||
let segment = bezpath.get_seg(next_segment_index + 1).unwrap();
|
||||
let t = eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY);
|
||||
let point = segment.eval(t);
|
||||
|
||||
if sample_bezpath.elements().is_empty() {
|
||||
sample_bezpath.move_to(point)
|
||||
} else {
|
||||
sample_bezpath.line_to(point)
|
||||
}
|
||||
}
|
||||
|
||||
if was_closed {
|
||||
sample_bezpath.close_path();
|
||||
}
|
||||
|
||||
Some(sample_bezpath)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TValue {
|
||||
Parametric(f64),
|
||||
Euclidean(f64),
|
||||
}
|
||||
|
||||
/// Default LUT step size in `compute_lookup_table` function.
|
||||
pub const DEFAULT_LUT_STEP_SIZE: usize = 10;
|
||||
|
||||
/// Return a selection of equidistant points on the bezier curve.
|
||||
/// If no value is provided for `steps`, then the function will default `steps` to be 10.
|
||||
pub fn pathseg_compute_lookup_table(segment: PathSeg, steps: Option<usize>, eucliean: bool) -> impl Iterator<Item = DVec2> {
|
||||
let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE);
|
||||
|
||||
(0..=steps).map(move |t| {
|
||||
let tvalue = if eucliean {
|
||||
TValue::Euclidean(t as f64 / steps as f64)
|
||||
} else {
|
||||
TValue::Parametric(t as f64 / steps as f64)
|
||||
};
|
||||
let t = eval_pathseg(segment, tvalue);
|
||||
point_to_dvec2(segment.eval(t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an `Iterator` containing all possible parametric `t`-values at the given `x`-coordinate.
|
||||
pub fn pathseg_find_tvalues_for_x(segment: PathSeg, x: f64) -> impl Iterator<Item = f64> + use<> {
|
||||
match segment {
|
||||
PathSeg::Line(Line { p0, p1 }) => {
|
||||
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
|
||||
let a = p1.x - p0.x;
|
||||
let b = p0.x - x;
|
||||
|
||||
// Find the roots of the linear equation `ax + b`.
|
||||
// There exist roots when `a` is not 0
|
||||
if a.abs() > MAX_ABSOLUTE_DIFFERENCE { [Some(-b / a), None, None] } else { [None; 3] }
|
||||
}
|
||||
PathSeg::Quad(QuadBez { p0, p1, p2 }) => {
|
||||
let a = p2.x - 2.0 * p1.x + p0.x;
|
||||
let b = 2.0 * (p1.x - p0.x);
|
||||
let c = p0.x - x;
|
||||
let r = solve_quadratic(c, b, a);
|
||||
[r.first().copied(), r.get(1).copied(), None]
|
||||
}
|
||||
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => {
|
||||
let a = p3.x - 3.0 * p2.x + 3.0 * p1.x - p0.x;
|
||||
let b = 3.0 * (p2.x - 2.0 * p1.x + p0.x);
|
||||
let c = 3.0 * (p1.x - p0.x);
|
||||
let d = p0.x - x;
|
||||
let r = solve_cubic(d, c, b, a);
|
||||
[r.first().copied(), r.get(1).copied(), r.get(2).copied()]
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|&t| (0.0..1.).contains(&t))
|
||||
}
|
||||
|
||||
/// Find the `t`-value(s) such that the normal(s) at `t` pass through the specified point.
|
||||
pub fn pathseg_normals_to_point(segment: PathSeg, point: Point) -> Vec<f64> {
|
||||
// We solve deriv(t) dot (self(t) - point) = 0.
|
||||
let (mut x, mut y) = pathseg_to_parametric_polynomial(segment);
|
||||
let x = x.coefficients_mut();
|
||||
let y = y.coefficients_mut();
|
||||
x[0] -= point.x;
|
||||
y[0] -= point.y;
|
||||
let poly = polycool::Poly::new([
|
||||
x[0] * x[1] + y[0] * y[1],
|
||||
x[1] * x[1] + y[1] * y[1] + 2. * (x[0] * x[2] + y[0] * y[2]),
|
||||
3. * (x[2] * x[1] + y[2] * y[1]) + 3. * (x[0] * x[3] + y[0] * y[3]),
|
||||
4. * (x[3] * x[1] + y[3] * y[1]) + 2. * (x[2] * x[2] + y[2] * y[2]),
|
||||
5. * (x[3] * x[2] + y[3] * y[2]),
|
||||
3. * (x[3] * x[3] + y[3] * y[3]),
|
||||
]);
|
||||
poly.roots_between(0., 1., 1e-8).to_vec()
|
||||
}
|
||||
|
||||
/// Find the `t`-value(s) such that the tangent(s) at `t` pass through the given point.
|
||||
pub fn pathseg_tangents_to_point(segment: PathSeg, point: Point) -> Vec<f64> {
|
||||
segment.to_cubic().tangents_to_point(point).to_vec()
|
||||
}
|
||||
|
||||
/// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`.
|
||||
pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option<PathSeg> {
|
||||
let t1 = eval_pathseg(segment, t1);
|
||||
let t2 = eval_pathseg(segment, t2);
|
||||
|
||||
if t1 > t2 { None } else { Some(segment.subsegment(t1..t2)) }
|
||||
}
|
||||
|
||||
pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 {
|
||||
match t_value {
|
||||
TValue::Parametric(t) => t,
|
||||
TValue::Euclidean(t) => eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an approximation of the length centroid, together with the length, of the bezier curve.
|
||||
///
|
||||
/// The length centroid is the center of mass for the arc length of the Bezier segment.
|
||||
/// An infinitely thin wire forming the Bezier segment's shape would balance at this point.
|
||||
///
|
||||
/// - `accuracy` is used to approximate the curve.
|
||||
pub(crate) fn pathseg_length_centroid_and_length(segment: PathSeg, accuracy: Option<f64>) -> (Vec2, f64) {
|
||||
match segment {
|
||||
PathSeg::Line(line) => ((line.start().to_vec2() + line.end().to_vec2()) / 2., (line.start().to_vec2() - line.end().to_vec2()).length()),
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let QuadBez { p0, p1, p2 } = quad_bez;
|
||||
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
|
||||
fn recurse(a0: Vec2, a1: Vec2, a2: Vec2, accuracy: f64, level: u8) -> (f64, Vec2) {
|
||||
let lower = (a2 - a1).length();
|
||||
let upper = (a1 - a0).length() + (a2 - a1).length();
|
||||
if upper - lower <= 2. * accuracy || level >= 8 {
|
||||
let length = (lower + upper) / 2.;
|
||||
return (length, length * (a0 + a1 + a2) / 3.);
|
||||
}
|
||||
|
||||
let b1 = 0.5 * (a0 + a1);
|
||||
let c1 = 0.5 * (a1 + a2);
|
||||
let b2 = 0.5 * (b1 + c1);
|
||||
|
||||
let (length1, centroid_part1) = recurse(a0, b1, b2, 0.5 * accuracy, level + 1);
|
||||
let (length2, centroid_part2) = recurse(b2, c1, a2, 0.5 * accuracy, level + 1);
|
||||
(length1 + length2, centroid_part1 + centroid_part2)
|
||||
}
|
||||
|
||||
let (length, centroid_parts) = recurse(p0.to_vec2(), p1.to_vec2(), p2.to_vec2(), accuracy.unwrap_or_default(), 0);
|
||||
(centroid_parts / length, length)
|
||||
}
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let CubicBez { p0, p1, p2, p3 } = cubic_bez;
|
||||
|
||||
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
|
||||
fn recurse(a0: Vec2, a1: Vec2, a2: Vec2, a3: Vec2, accuracy: f64, level: u8) -> (f64, Vec2) {
|
||||
let lower = (a3 - a0).length();
|
||||
let upper = (a1 - a0).length() + (a2 - a1).length() + (a3 - a2).length();
|
||||
if upper - lower <= 2. * accuracy || level >= 8 {
|
||||
let length = (lower + upper) / 2.;
|
||||
return (length, length * (a0 + a1 + a2 + a3) / 4.);
|
||||
}
|
||||
|
||||
let b1 = 0.5 * (a0 + a1);
|
||||
let t0 = 0.5 * (a1 + a2);
|
||||
let c1 = 0.5 * (a2 + a3);
|
||||
let b2 = 0.5 * (b1 + t0);
|
||||
let c2 = 0.5 * (t0 + c1);
|
||||
let b3 = 0.5 * (b2 + c2);
|
||||
|
||||
let (length1, centroid_part1) = recurse(a0, b1, b2, b3, 0.5 * accuracy, level + 1);
|
||||
let (length2, centroid_part2) = recurse(b3, c2, c1, a3, 0.5 * accuracy, level + 1);
|
||||
(length1 + length2, centroid_part1 + centroid_part2)
|
||||
}
|
||||
|
||||
let (length, centroid_parts) = recurse(p0.to_vec2(), p1.to_vec2(), p2.to_vec2(), p3.to_vec2(), accuracy.unwrap_or_default(), 0);
|
||||
(centroid_parts / length, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length.
|
||||
/// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`.
|
||||
pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 {
|
||||
let mut low_t = 0.;
|
||||
let mut mid_t = 0.5;
|
||||
let mut high_t = 1.;
|
||||
|
||||
let total_length = segment.perimeter(accuracy);
|
||||
|
||||
if !total_length.is_finite() || total_length <= f64::EPSILON {
|
||||
return 0.;
|
||||
}
|
||||
|
||||
let distance = distance.clamp(0., 1.);
|
||||
|
||||
while high_t - low_t > accuracy {
|
||||
let current_length = segment.subsegment(0.0..mid_t).perimeter(accuracy);
|
||||
let current_distance = current_length / total_length;
|
||||
|
||||
if current_distance > distance {
|
||||
high_t = mid_t;
|
||||
} else {
|
||||
low_t = mid_t;
|
||||
}
|
||||
mid_t = (high_t + low_t) / 2.;
|
||||
}
|
||||
|
||||
mid_t
|
||||
}
|
||||
|
||||
/// 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 eval_bazpath_to_euclidean(bezpath: &BezPath, global_t: f64, lengths: &[f64], total_length: f64) -> (usize, f64) {
|
||||
let mut accumulator = 0.;
|
||||
for (index, length) in lengths.iter().enumerate() {
|
||||
let length_ratio = length / total_length;
|
||||
if (index == 0 || accumulator <= global_t) && global_t <= accumulator + length_ratio {
|
||||
return (index, ((global_t - accumulator) / length_ratio).clamp(0., 1.));
|
||||
}
|
||||
accumulator += length_ratio;
|
||||
}
|
||||
(bezpath.segments().count() - 1, 1.)
|
||||
}
|
||||
|
||||
/// Convert a [TValue] to a parametric `(segment_index, t)` tuple.
|
||||
/// - Asserts that `t` values contained within the `TValue` argument lie in the range [0, 1].
|
||||
fn eval_bezpath(bezpath: &BezPath, t: TValue, precomputed_segments_length: Option<&[f64]>) -> (usize, f64) {
|
||||
let segment_count = bezpath.segments().count();
|
||||
assert!(segment_count >= 1);
|
||||
|
||||
match t {
|
||||
TValue::Euclidean(t) => {
|
||||
let computed_segments_length;
|
||||
|
||||
let segments_length = if let Some(segments_length) = precomputed_segments_length {
|
||||
segments_length
|
||||
} else {
|
||||
computed_segments_length = bezpath.segments().map(|segment| segment.perimeter(DEFAULT_ACCURACY)).collect::<Vec<f64>>();
|
||||
computed_segments_length.as_slice()
|
||||
};
|
||||
|
||||
let total_length = segments_length.iter().sum();
|
||||
|
||||
let (segment_index, t) = eval_bazpath_to_euclidean(bezpath, t, segments_length, total_length);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
(segment_index, eval_pathseg_euclidean(segment, t, DEFAULT_ACCURACY))
|
||||
}
|
||||
TValue::Parametric(t) => {
|
||||
assert!((0.0..=1.).contains(&t));
|
||||
|
||||
if t == 1. {
|
||||
return (segment_count - 1, 1.);
|
||||
}
|
||||
|
||||
let scaled_t = t * segment_count as f64;
|
||||
let segment_index = scaled_t.floor() as usize;
|
||||
let t = scaled_t - segment_index as f64;
|
||||
|
||||
(segment_index, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Randomly places points across the filled surface of this subpath (which is assumed to be closed).
|
||||
/// The `separation_disk_diameter` determines the minimum distance between all points from one another.
|
||||
/// Conceptually, this works by "throwing a dart" at the subpath's bounding box and keeping the dart only if:
|
||||
/// - It's inside the shape
|
||||
/// - It's not closer than `separation_disk_diameter` to any other point from a previous accepted dart throw
|
||||
///
|
||||
/// This repeats until accepted darts fill all possible areas between one another.
|
||||
///
|
||||
/// While the conceptual process described above asymptotically slows down and is never guaranteed to produce a maximal set in finite time,
|
||||
/// this is implemented with an algorithm that produces a maximal set in O(n) time. The slowest part is actually checking if points are inside the subpath shape.
|
||||
pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], separation_disk_diameter: f64, rng: impl FnMut() -> f64) -> Vec<DVec2> {
|
||||
let (this_bezpath, this_bbox) = bezpaths[bezpath_index].clone();
|
||||
|
||||
if this_bezpath.elements().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let point_in_shape_checker = |point: DVec2| {
|
||||
// Check against all paths the point is contained in to compute the correct winding number
|
||||
let mut number = 0;
|
||||
|
||||
for (i, (shape, bbox)) in bezpaths.iter().enumerate() {
|
||||
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
if winding == 0 && i == bezpath_index {
|
||||
return false;
|
||||
}
|
||||
number += winding;
|
||||
}
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
};
|
||||
|
||||
let line_intersect_shape_checker = |p0: (f64, f64), p1: (f64, f64)| {
|
||||
for segment in this_bezpath.segments() {
|
||||
if !segment.intersect_line(Line::new(p0, p1)).is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
};
|
||||
|
||||
let offset = DVec2::new(this_bbox.x0, this_bbox.y0);
|
||||
let width = this_bbox.width();
|
||||
let height = this_bbox.height();
|
||||
|
||||
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
|
||||
}
|
||||
|
||||
/// Returns true if the Bezier curve is equivalent to a line.
|
||||
///
|
||||
/// **NOTE**: This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
|
||||
pub fn is_linear(segment: &PathSeg) -> bool {
|
||||
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
|
||||
|
||||
match *segment {
|
||||
PathSeg::Line(_) => true,
|
||||
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
|
||||
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
|
||||
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
|
||||
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
|
||||
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
|
||||
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
|
||||
// Split the first subpath at its last intersection
|
||||
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
|
||||
if subpath_1_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = *subpath_1_intersections.last()?;
|
||||
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;
|
||||
|
||||
// Split the second subpath at its first intersection
|
||||
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
|
||||
if subpath_2_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = subpath_2_intersections[0];
|
||||
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;
|
||||
|
||||
Some((clipped_subpath1, clipped_subpath2))
|
||||
}
|
||||
|
||||
/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
|
||||
///
|
||||
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
|
||||
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
|
||||
/// When the limit is exceeded, no [`PathEl`] will be returned.
|
||||
/// This value should be greater than 0. If not, the default of 4 will be used.
|
||||
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
|
||||
let miter_limit = match miter_limit {
|
||||
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
|
||||
_ => 4.,
|
||||
};
|
||||
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
|
||||
let in_segment = bezpath1.segments().last()?;
|
||||
let out_segment = bezpath2.segments().next()?;
|
||||
|
||||
let in_tangent = pathseg_tangent(in_segment, 1.);
|
||||
let out_tangent = pathseg_tangent(out_segment, 0.);
|
||||
|
||||
if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
|
||||
// Avoid panic from normalizing zero vectors
|
||||
// TODO: Besides returning None, is there a more appropriate way to handle this?
|
||||
return None;
|
||||
}
|
||||
|
||||
let angle = (in_tangent * -1.).angle_to(out_tangent).abs();
|
||||
|
||||
if angle.to_degrees() < miter_limit {
|
||||
return None;
|
||||
}
|
||||
|
||||
let p1 = in_segment.end();
|
||||
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
|
||||
let line1 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
let p1 = out_segment.start();
|
||||
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
|
||||
let line2 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
|
||||
// Otherwise, we return the element to create the join.
|
||||
let intersection = line1.crossing_point(line2)?;
|
||||
|
||||
Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
|
||||
}
|
||||
|
||||
/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
|
||||
/// By default, the angle is assumed to be 180 degrees.
|
||||
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
|
||||
let center_to_arc_point = arc_point - center;
|
||||
|
||||
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
|
||||
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
|
||||
|
||||
let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(arc_point);
|
||||
|
||||
let first_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(right);
|
||||
|
||||
let second_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
[first_half, second_half]
|
||||
}
|
||||
|
||||
/// Returns two [`PathEl`] to create a round join with the provided center.
|
||||
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
|
||||
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
let center_to_right = right - center;
|
||||
let center_to_left = left - center;
|
||||
|
||||
let in_segment = bezpath1.segments().last();
|
||||
let in_tangent = in_segment.map(|in_segment| pathseg_tangent(in_segment, 1.));
|
||||
|
||||
let mut angle = center_to_right.angle_to(center_to_left) / 2.;
|
||||
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
|
||||
if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
|
||||
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
|
||||
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
}
|
||||
|
||||
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
|
||||
}
|
||||
|
||||
/// Returns `true` if the `bezpath1` is completely inside the `bezpath2`.
|
||||
/// NOTE: `bezpath2` must be a closed path to get correct results.
|
||||
pub fn bezpath_is_inside_bezpath(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> bool {
|
||||
// Eliminate any possibility of one being inside the other, if either of them are empty
|
||||
if bezpath1.is_empty() || bezpath2.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let inner_bbox = bezpath1.bounding_box();
|
||||
let outer_bbox = bezpath2.bounding_box();
|
||||
|
||||
// Eliminate bezpath1 if its bounding box is not completely inside the bezpath2's bounding box.
|
||||
// Reasoning:
|
||||
// If the inner bezpath bounding box is larger than the outer bezpath bounding box in any direction
|
||||
// then the inner bezpath is intersecting with or outside the outer bezpath.
|
||||
if !outer_bbox.contains_rect(inner_bbox) && outer_bbox.intersect(inner_bbox).is_zero_area() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Eliminate bezpath1 if any of its anchor points are outside the bezpath2.
|
||||
if !bezpath1.elements().iter().filter_map(|el| el.end_point()).all(|point| bezpath2.contains(point)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Eliminate this subpath if it intersects with the other subpath.
|
||||
if !bezpath_intersections(bezpath1, bezpath2, accuracy, minimum_separation).is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// At this point:
|
||||
// (1) This subpath's bounding box is inside the other subpath's bounding box,
|
||||
// (2) Its anchors are inside the other subpath, and
|
||||
// (3) It is not intersecting with the other subpath.
|
||||
// Hence, this subpath is completely inside the given other subpath.
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO: add more intersection tests
|
||||
|
||||
use super::bezpath_is_inside_bezpath;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Rect, Shape};
|
||||
|
||||
#[test]
|
||||
fn is_inside_subpath() {
|
||||
let boundary_polygon = Rect::new(100., 100., 500., 500.).to_path(DEFAULT_ACCURACY);
|
||||
|
||||
let mut curve_intersection = BezPath::new();
|
||||
curve_intersection.move_to(Point::new(189., 289.));
|
||||
curve_intersection.quad_to(Point::new(9., 286.), Point::new(45., 410.));
|
||||
assert!(!bezpath_is_inside_bezpath(&curve_intersection, &boundary_polygon, None, None));
|
||||
|
||||
let mut curve_outside = BezPath::new();
|
||||
curve_outside.move_to(Point::new(115., 37.));
|
||||
curve_outside.quad_to(Point::new(51.4, 91.8), Point::new(76.5, 242.));
|
||||
assert!(!bezpath_is_inside_bezpath(&curve_outside, &boundary_polygon, None, None));
|
||||
|
||||
let mut curve_inside = BezPath::new();
|
||||
curve_inside.move_to(Point::new(210.1, 133.5));
|
||||
curve_inside.curve_to(Point::new(150.2, 436.9), Point::new(436., 285.), Point::new(247.6, 240.7));
|
||||
assert!(bezpath_is_inside_bezpath(&curve_inside, &boundary_polygon, None, None));
|
||||
|
||||
let line_inside = Line::new(Point::new(101., 101.5), Point::new(150.2, 499.)).to_path(DEFAULT_ACCURACY);
|
||||
assert!(bezpath_is_inside_bezpath(&line_inside, &boundary_polygon, None, None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
|
||||
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
#[cfg(test)]
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
@@ -0,0 +1,496 @@
|
||||
use super::contants::MIN_SEPARATION_VALUE;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
|
||||
use lyon_geom::{CubicBezierSegment, Point};
|
||||
|
||||
/// Converts a kurbo cubic bezier to a lyon_geom CubicBezierSegment
|
||||
fn kurbo_cubic_to_lyon(cubic: kurbo::CubicBez) -> CubicBezierSegment<f64> {
|
||||
CubicBezierSegment {
|
||||
from: Point::new(cubic.p0.x, cubic.p0.y),
|
||||
ctrl1: Point::new(cubic.p1.x, cubic.p1.y),
|
||||
ctrl2: Point::new(cubic.p2.x, cubic.p2.y),
|
||||
to: Point::new(cubic.p3.x, cubic.p3.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast cubic-cubic intersection using lyon_geom's analytical approach
|
||||
fn cubic_cubic_intersections_lyon(cubic1: kurbo::CubicBez, cubic2: kurbo::CubicBez) -> Vec<(f64, f64)> {
|
||||
let lyon_cubic1 = kurbo_cubic_to_lyon(cubic1);
|
||||
let lyon_cubic2 = kurbo_cubic_to_lyon(cubic2);
|
||||
|
||||
lyon_cubic1.cubic_intersections_t(&lyon_cubic2).to_vec()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to
|
||||
/// that segment where the intersection occurred.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference that two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
pub fn bezpath_and_segment_intersections(bezpath: &BezPath, segment: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
bezpath
|
||||
.segments()
|
||||
.enumerate()
|
||||
.flat_map(|(index, this_segment)| {
|
||||
filtered_segment_intersections(this_segment, segment, accuracy, minimum_separation)
|
||||
.into_iter()
|
||||
.map(|t| (index, t))
|
||||
.collect::<Vec<(usize, f64)>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the bezpath has with another given bezpath and returns a list of parametric `t`-values.
|
||||
pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = bezpath2
|
||||
.segments()
|
||||
.flat_map(|bezier| bezpath_and_segment_intersections(bezpath1, bezier, accuracy, minimum_separation))
|
||||
.collect();
|
||||
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the segment has with another given segment and returns a list of parametric `t`-values with given accuracy.
|
||||
pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY);
|
||||
|
||||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
// Fast path for cubic-cubic intersections using lyon_geom
|
||||
(PathSeg::Cubic(cubic1), PathSeg::Cubic(cubic2)) => cubic_cubic_intersections_lyon(cubic1, cubic2),
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections);
|
||||
intersections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subsegment_intersections(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY);
|
||||
|
||||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
// Fast path for cubic-cubic intersections using lyon_geom with subsegment parameters
|
||||
(PathSeg::Cubic(cubic1), PathSeg::Cubic(cubic2)) => {
|
||||
let sub_cubic1 = cubic1.subsegment(min_t1..max_t1);
|
||||
let sub_cubic2 = cubic2.subsegment(min_t2..max_t2);
|
||||
|
||||
cubic_cubic_intersections_lyon(sub_cubic1, sub_cubic2)
|
||||
.into_iter()
|
||||
// Convert subsegment t-values back to original segment t-values
|
||||
.map(|(t1, t2)| {
|
||||
let original_t1 = min_t1 + t1 * (max_t1 - min_t1);
|
||||
let original_t2 = min_t2 + t2 * (max_t2 - min_t2);
|
||||
(original_t1, original_t2)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, min_t1, max_t1, segment2, min_t2, max_t2, accuracy, &mut intersections);
|
||||
intersections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn approx_bounding_box(path_seg: PathSeg) -> kurbo::Rect {
|
||||
use kurbo::Rect;
|
||||
match path_seg {
|
||||
PathSeg::Line(line) => kurbo::Rect::from_points(line.p0, line.p1),
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let r1 = Rect::from_points(quad_bez.p0, quad_bez.p1);
|
||||
let r2 = Rect::from_points(quad_bez.p1, quad_bez.p2);
|
||||
r1.union(r2)
|
||||
}
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let r1 = Rect::from_points(cubic_bez.p0, cubic_bez.p1);
|
||||
let r2 = Rect::from_points(cubic_bez.p2, cubic_bez.p3);
|
||||
r1.union(r2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments
|
||||
/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) {
|
||||
let bbox1 = approx_bounding_box(segment1.subsegment(min_t1..max_t1));
|
||||
let bbox2 = approx_bounding_box(segment2.subsegment(min_t2..max_t2));
|
||||
|
||||
if intersections.len() > 50 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mid_t1 = (min_t1 + max_t1) / 2.;
|
||||
let mid_t2 = (min_t2 + max_t2) / 2.;
|
||||
|
||||
// Check if the bounding boxes overlap
|
||||
if bbox1.overlaps(bbox2) {
|
||||
// If bounding boxes overlap and they are small enough, we have found an intersection
|
||||
if bbox1.width().abs() < accuracy && bbox1.height().abs() < accuracy && bbox2.width().abs() < accuracy && bbox2.height().abs() < accuracy {
|
||||
// Use the middle `t` value, append the corresponding `t` value
|
||||
intersections.push((mid_t1, mid_t2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Split curves in half
|
||||
let (seg11, seg12) = segment1.subdivide();
|
||||
let (seg21, seg22) = segment2.subdivide();
|
||||
|
||||
// Repeat checking the intersection with the combinations of the two halves of each curve
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier segment and the provided one
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference
|
||||
/// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The returned `t` values are with respect to the current bezier segment, not the provided parameter.
|
||||
/// If the provided segment is linear, then zero intersection points will be returned along colinear segments.
|
||||
///
|
||||
/// `accuracy` defines, for intersections where the provided bezier segment is non-linear, the maximum size of the bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order.
|
||||
pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
intersection_t_values.iter().map(|x| x.0).fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of pairs of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided
|
||||
/// one such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference between
|
||||
/// two adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The first value in pair is with respect to the current bezier and the second value in pair is with respect to the provided parameter.
|
||||
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
|
||||
///
|
||||
/// `error`, for intersections where the provided bezier is non-linear, defines the threshold for bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order
|
||||
pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap());
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty()
|
||||
&& (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
&& (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
{
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to compute intersections between lists of subcurves.
|
||||
/// This function uses the algorithm implemented in `intersections_between_subcurves`.
|
||||
fn intersections_between_vectors_of_path_segments(subcurves1: &[(f64, f64, PathSeg)], subcurves2: &[(f64, f64, PathSeg)], accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let segment_pairs = subcurves1.iter().flat_map(move |(t11, t12, curve1)| {
|
||||
subcurves2
|
||||
.iter()
|
||||
.filter_map(move |(t21, t22, curve2)| curve1.bounding_box().overlaps(curve2.bounding_box()).then_some((t11, t12, curve1, t21, t22, curve2)))
|
||||
});
|
||||
|
||||
segment_pairs
|
||||
.flat_map(|(&t11, &t12, &curve1, &t21, &t22, &curve2)| subsegment_intersections(curve1, t11, t12, curve2, t21, t22, accuracy))
|
||||
.collect::<Vec<(f64, f64)>>()
|
||||
}
|
||||
|
||||
fn pathseg_self_intersection(segment: PathSeg, accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let cubic_bez = match segment {
|
||||
PathSeg::Line(_) | PathSeg::Quad(_) => return vec![],
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez,
|
||||
};
|
||||
|
||||
// Get 2 copies of the reduced curves
|
||||
let quads1 = cubic_bez.to_quads(DEFAULT_ACCURACY).map(|(t1, t2, quad_bez)| (t1, t2, PathSeg::Quad(quad_bez))).collect::<Vec<_>>();
|
||||
let quads2 = quads1.clone();
|
||||
|
||||
let num_curves = quads1.len();
|
||||
|
||||
// Adjacent reduced curves cannot intersect
|
||||
if num_curves <= 2 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// For each curve, look for intersections with every curve that is at least 2 indices away
|
||||
quads1
|
||||
.iter()
|
||||
.take(num_curves - 2)
|
||||
.enumerate()
|
||||
.flat_map(|(index, &subsegment)| intersections_between_vectors_of_path_segments(&[subsegment], &quads2[index + 2..], accuracy))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
|
||||
/// If the difference between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order
|
||||
pub fn pathseg_self_intersections(segment: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let mut intersection_t_values = pathseg_self_intersection(segment, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap());
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty()
|
||||
&& (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
&& (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
{
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{bezpath_and_segment_intersections, filtered_segment_intersections};
|
||||
use crate::vector::algorithms::{
|
||||
contants::MAX_ABSOLUTE_DIFFERENCE,
|
||||
util::{compare_points, compare_vec_of_points, dvec2_compare},
|
||||
};
|
||||
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez};
|
||||
|
||||
#[test]
|
||||
fn test_intersect_line_segment_quadratic() {
|
||||
let p1 = Point::new(30., 50.);
|
||||
let p2 = Point::new(140., 30.);
|
||||
let p3 = Point::new(160., 170.);
|
||||
|
||||
// Intersection at edge of curve
|
||||
let bezier = PathSeg::Quad(QuadBez::new(p1, p2, p3));
|
||||
let line1 = PathSeg::Line(Line::new(Point::new(20., 50.), Point::new(40., 50.)));
|
||||
let intersections1 = filtered_segment_intersections(bezier, line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.eval(intersections1[0]), p1));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(30., 30.)));
|
||||
let intersections2 = filtered_segment_intersections(bezier, line2, None, None);
|
||||
assert!(compare_points(bezier.eval(intersections2[0]), Point::new(47.77355, 47.77354)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve_cubic_edge_case() {
|
||||
// M34 107 C40 40 120 120 102 29
|
||||
|
||||
let p1 = Point::new(34., 107.);
|
||||
let p2 = Point::new(40., 40.);
|
||||
let p3 = Point::new(120., 120.);
|
||||
let p4 = Point::new(102., 29.);
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4));
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
let intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
|
||||
assert_eq!(intersections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve() {
|
||||
let p0 = Point::new(30., 30.);
|
||||
let p1 = Point::new(60., 140.);
|
||||
let p2 = Point::new(150., 30.);
|
||||
let p3 = Point::new(160., 160.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3));
|
||||
|
||||
let p0 = Point::new(175., 140.);
|
||||
let p1 = Point::new(20., 20.);
|
||||
let p2 = Point::new(120., 20.);
|
||||
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2));
|
||||
|
||||
let intersections1 = filtered_segment_intersections(cubic_segment, quadratic_segment, None, None);
|
||||
let intersections2 = filtered_segment_intersections(quadratic_segment, cubic_segment, None, None);
|
||||
|
||||
let intersections1_points: Vec<Point> = intersections1.iter().map(|&t| cubic_segment.eval(t)).collect();
|
||||
let intersections2_points: Vec<Point> = intersections2.iter().map(|&t| quadratic_segment.eval(t)).rev().collect();
|
||||
|
||||
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_one() {
|
||||
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(43., 43.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, linear_segment, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, linear_segment, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_two() {
|
||||
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
|
||||
// M150 150 L 20 20
|
||||
|
||||
let cubic_start = Point::new(34., 107.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(102., 29.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(129., 171.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_three() {
|
||||
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(44., 44.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
use crate::vector::{PointDomain, PointId, SegmentDomain, SegmentId, Vector};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use petgraph::graph::{EdgeIndex, NodeIndex, UnGraph};
|
||||
use petgraph::prelude::UnGraphMap;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
pub trait MergeByDistanceExt {
|
||||
/// Collapse all points with edges shorter than the specified distance
|
||||
fn merge_by_distance_topological(&mut self, distance: f64);
|
||||
fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64);
|
||||
}
|
||||
|
||||
impl<Upstream: 'static> MergeByDistanceExt for Vector<Upstream> {
|
||||
fn merge_by_distance_topological(&mut self, distance: f64) {
|
||||
// Treat self as an undirected graph
|
||||
let indices = VectorIndex::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 {
|
||||
let [start, end] = indices.segment_ends(segment_id);
|
||||
let start = indices.point_graph.node_weight(start).unwrap().id;
|
||||
let end = indices.point_graph.node_weight(end).unwrap().id;
|
||||
|
||||
short_edges.add_node(start);
|
||||
short_edges.add_node(end);
|
||||
short_edges.add_edge(start, end, segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Group connected segments to collapse them into a single point
|
||||
// TODO: there are a few possible algorithms for this - perhaps test empirically to find fastest
|
||||
let collapse: Vec<FxHashSet<PointId>> = petgraph::algo::tarjan_scc(&short_edges).into_iter().map(|connected| connected.into_iter().collect()).collect();
|
||||
let average_position = collapse
|
||||
.iter()
|
||||
.map(|collapse_set| {
|
||||
let sum: DVec2 = collapse_set.iter().map(|&id| indices.point_position(id, self)).sum();
|
||||
sum / collapse_set.len() as f64
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Collect points and segments to delete at the end to avoid invalidating indices
|
||||
let mut points_to_delete = FxHashSet::default();
|
||||
let mut segments_to_delete = FxHashSet::default();
|
||||
for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position.into_iter()) {
|
||||
// Remove any segments where both endpoints are in the collapse set
|
||||
segments_to_delete.extend(self.segment_domain.iter().filter_map(|(id, start_offset, end_offset, _)| {
|
||||
let start = self.point_domain.ids()[start_offset];
|
||||
let end = self.point_domain.ids()[end_offset];
|
||||
if collapse_set.contains(&start) && collapse_set.contains(&end) { Some(id) } else { None }
|
||||
}));
|
||||
|
||||
// Delete all points but the first, set its position to the average, and update segments
|
||||
let first_id = collapse_set.iter().copied().next().unwrap();
|
||||
collapse_set.remove(&first_id);
|
||||
let first_offset = indices.point_to_offset[&first_id];
|
||||
|
||||
// Look for segments with endpoints in `collapse_set` and replace them with the point we are collapsing to
|
||||
for (_, start_offset, end_offset, handles) in self.segment_domain.iter_mut() {
|
||||
let start_id = self.point_domain.ids()[*start_offset];
|
||||
let end_id = self.point_domain.ids()[*end_offset];
|
||||
|
||||
// Update Bezier handles for moved points
|
||||
if start_id == first_id {
|
||||
let point_position = self.point_domain.position[*start_offset];
|
||||
handles.move_start(average_pos - point_position);
|
||||
}
|
||||
if end_id == first_id {
|
||||
let point_position = self.point_domain.position[*end_offset];
|
||||
handles.move_end(average_pos - point_position);
|
||||
}
|
||||
|
||||
// Replace removed points with the collapsed point
|
||||
if collapse_set.contains(&start_id) {
|
||||
let point_position = self.point_domain.position[*start_offset];
|
||||
*start_offset = first_offset;
|
||||
handles.move_start(average_pos - point_position);
|
||||
}
|
||||
if collapse_set.contains(&end_id) {
|
||||
let point_position = self.point_domain.position[*end_offset];
|
||||
*end_offset = first_offset;
|
||||
handles.move_end(average_pos - point_position);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the position of the collapsed point
|
||||
self.point_domain.position[first_offset] = average_pos;
|
||||
|
||||
points_to_delete.extend(collapse_set)
|
||||
}
|
||||
|
||||
// Remove faces whose start or end segments are removed
|
||||
// TODO: Adjust faces and only delete if all (or all but one) segments are removed
|
||||
self.region_domain
|
||||
.retain_with_region(|_, segment_range| segments_to_delete.contains(segment_range.start()) || segments_to_delete.contains(segment_range.end()));
|
||||
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 geometry
|
||||
self.point_domain = new_point_domain;
|
||||
self.segment_domain = new_segment_domain;
|
||||
}
|
||||
}
|
||||
|
||||
/// All the fixed fields of a point from the point domain.
|
||||
pub(crate) struct Point {
|
||||
pub id: PointId,
|
||||
pub position: DVec2,
|
||||
}
|
||||
|
||||
/// Useful indexes to speed up various operations on [`Vector`].
|
||||
///
|
||||
/// Important: It is the user's responsibility to ensure the indexes remain valid after mutations to the data.
|
||||
pub struct VectorIndex {
|
||||
/// Points and segments form a graph. Store it here in a form amenable to graph algorithms.
|
||||
///
|
||||
/// Currently, segment data is not stored as it is not used, but it could easily be added.
|
||||
pub(crate) point_graph: UnGraph<Point, ()>,
|
||||
pub(crate) segment_to_edge: FxHashMap<SegmentId, EdgeIndex>,
|
||||
/// Get the offset from the point ID.
|
||||
pub(crate) point_to_offset: FxHashMap<PointId, usize>,
|
||||
// TODO: faces
|
||||
}
|
||||
|
||||
impl VectorIndex {
|
||||
/// Construct a [`VectorIndex`] by building indexes from the given [`Vector`]. Takes `O(n)` time.
|
||||
pub fn build_from<Upstream: 'static>(data: &Vector<Upstream>) -> Self {
|
||||
let point_to_offset = data.point_domain.ids().iter().copied().enumerate().map(|(a, b)| (b, a)).collect::<FxHashMap<_, _>>();
|
||||
|
||||
let mut point_to_node = FxHashMap::default();
|
||||
let mut segment_to_edge = FxHashMap::default();
|
||||
|
||||
let mut graph = UnGraph::new_undirected();
|
||||
|
||||
for (point_id, position) in data.point_domain.iter() {
|
||||
let idx = graph.add_node(Point { id: point_id, position });
|
||||
point_to_node.insert(point_id, idx);
|
||||
}
|
||||
|
||||
for (segment_id, start_offset, end_offset, ..) in data.segment_domain.iter() {
|
||||
let start_id = data.point_domain.ids()[start_offset];
|
||||
let end_id = data.point_domain.ids()[end_offset];
|
||||
let edge = graph.add_edge(point_to_node[&start_id], point_to_node[&end_id], ());
|
||||
|
||||
segment_to_edge.insert(segment_id, edge);
|
||||
}
|
||||
|
||||
Self {
|
||||
point_graph: graph,
|
||||
segment_to_edge,
|
||||
point_to_offset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the length of given segment's chord. Takes `O(1)` time.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if no segment with the given ID is found.
|
||||
pub fn segment_chord_length(&self, id: SegmentId) -> f64 {
|
||||
let edge_idx = self.segment_to_edge[&id];
|
||||
let (start, end) = self.point_graph.edge_endpoints(edge_idx).unwrap();
|
||||
let start_position = self.point_graph.node_weight(start).unwrap().position;
|
||||
let end_position = self.point_graph.node_weight(end).unwrap().position;
|
||||
(start_position - end_position).length()
|
||||
}
|
||||
|
||||
/// Get the ends of a segment. Takes `O(1)` time.
|
||||
///
|
||||
/// The IDs will be ordered [smallest, largest] so they can be used to find other segments with the same endpoints, regardless of direction.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the ID is not present.
|
||||
pub fn segment_ends(&self, id: SegmentId) -> [NodeIndex; 2] {
|
||||
let (start, end) = self.point_graph.edge_endpoints(self.segment_to_edge[&id]).unwrap();
|
||||
if start < end { [start, end] } else { [end, start] }
|
||||
}
|
||||
|
||||
/// Get the physical location of a point. Takes `O(1)` time.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if `id` isn't in the data.
|
||||
pub fn point_position<Upstream: 'static>(&self, id: PointId, data: &Vector<Upstream>) -> DVec2 {
|
||||
let offset = self.point_to_offset[&id];
|
||||
data.point_domain.positions()[offset]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod bezpath_algorithms;
|
||||
mod contants;
|
||||
pub mod intersection;
|
||||
pub mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
pub mod util;
|
||||
@@ -0,0 +1,153 @@
|
||||
use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join};
|
||||
use crate::vector::misc::point_to_dvec2;
|
||||
use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg};
|
||||
|
||||
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
|
||||
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7;
|
||||
/// Squared version to avoid sqrt in distance checks.
|
||||
const MAX_ABSOLUTE_DIFFERENCE_SQUARED: f64 = MAX_ABSOLUTE_DIFFERENCE * MAX_ABSOLUTE_DIFFERENCE;
|
||||
const MAX_FITTED_SEGMENTS: usize = 10000;
|
||||
|
||||
/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: Option<f64>) -> BezPath {
|
||||
// An offset at a distance 0 from the curve is simply the same curve.
|
||||
// An offset of a single point is not defined.
|
||||
if distance == 0. || bezpath.get_seg(1).is_none() {
|
||||
return bezpath.clone();
|
||||
}
|
||||
|
||||
let mut bezpaths = bezpath
|
||||
.segments()
|
||||
.map(|bezier| bezier.to_cubic())
|
||||
.filter_map(|cubic_bez| {
|
||||
// Skip degenerate curves where all control points are at the same location.
|
||||
// Offsetting a point is undefined and causes infinite recursion in fit_to_bezpath.
|
||||
let start = cubic_bez.p0;
|
||||
let is_degenerate = start.distance_squared(cubic_bez.p1) < MAX_ABSOLUTE_DIFFERENCE_SQUARED
|
||||
&& start.distance_squared(cubic_bez.p2) < MAX_ABSOLUTE_DIFFERENCE_SQUARED
|
||||
&& start.distance_squared(cubic_bez.p3) < MAX_ABSOLUTE_DIFFERENCE_SQUARED;
|
||||
|
||||
if is_degenerate {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut fitted = BezPath::new();
|
||||
kurbo::offset::offset_cubic(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY, &mut fitted);
|
||||
|
||||
if fitted.segments().count() > MAX_FITTED_SEGMENTS {
|
||||
None
|
||||
} else {
|
||||
fitted.get_seg(1).is_some().then_some(fitted)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<BezPath>>();
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..bezpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let bezpath1 = &bezpaths[i];
|
||||
let bezpath2 = &bezpaths[j];
|
||||
|
||||
let last_segment_end = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let first_segment_start = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
// If the anchors are approximately equal, there is no need to clip / join the segments
|
||||
if last_segment_end.abs_diff_eq(first_segment_start, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_join = true;
|
||||
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
|
||||
bezpaths[i] = clipped_subpath1;
|
||||
bezpaths[j] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified join type
|
||||
if apply_join {
|
||||
match join {
|
||||
Join::Bevel => {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[i].push(element[0]);
|
||||
bezpaths[i].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let center = point_to_dvec2(bezpath.get_seg(i + 1).unwrap().end());
|
||||
let elements = round_line_join(&bezpaths[i], &bezpaths[j], center);
|
||||
bezpaths[i].push(elements[0]);
|
||||
bezpaths[i].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath);
|
||||
if is_bezpath_closed {
|
||||
let mut apply_join = true;
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = bezpaths.len() - 1;
|
||||
bezpaths[last_index] = clipped_subpath1;
|
||||
bezpaths[0] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
|
||||
if apply_join {
|
||||
match join {
|
||||
Join::Bevel => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = miter_line_join(&bezpaths[last_subpath_index], &bezpaths[0], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[last_subpath_index].push(element[0]);
|
||||
bezpaths[last_subpath_index].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let center = point_to_dvec2(bezpath.get_seg(1).unwrap().start());
|
||||
let elements = round_line_join(&bezpaths[last_subpath_index], &bezpaths[0], center);
|
||||
bezpaths[last_subpath_index].push(elements[0]);
|
||||
bezpaths[last_subpath_index].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the bezpaths and its segments. Drop points which overlap with one another.
|
||||
let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::<Vec<PathSeg>>()).collect::<Vec<PathSeg>>();
|
||||
let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| {
|
||||
if acc.elements().is_empty() {
|
||||
acc.move_to(segment.start());
|
||||
}
|
||||
acc.push(segment.as_path_el());
|
||||
acc
|
||||
});
|
||||
|
||||
if is_bezpath_closed {
|
||||
offset_bezpath.close_path();
|
||||
}
|
||||
|
||||
offset_bezpath
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
use glam::DVec2;
|
||||
use std::collections::HashMap;
|
||||
use std::f64;
|
||||
|
||||
const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
|
||||
|
||||
/// Fast (O(n) with respect to time and memory) algorithm for generating a maximal set of points using Poisson-disk sampling.
|
||||
/// Based on the paper:
|
||||
/// "Poisson Disk Point Sets by Hierarchical Dart Throwing"
|
||||
/// <https://scholarsarchive.byu.edu/facpub/237/>
|
||||
pub fn poisson_disk_sample(
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
diameter: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
rng: impl FnMut() -> f64,
|
||||
) -> Vec<DVec2> {
|
||||
let mut rng = rng;
|
||||
let diameter_squared = diameter.powi(2);
|
||||
|
||||
// Initialize a place to store the generated points within a spatial acceleration structure
|
||||
let mut points_grid = AccelerationGrid::new(width, height, diameter);
|
||||
|
||||
// Pick a grid size for the base-level domain that's as large as possible, while also:
|
||||
// - 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 * 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);
|
||||
// In the future, if necessary, this could be turned into a fixed-length array with worst-case length `f64::MANTISSA_DIGITS`
|
||||
let mut active_list_levels = vec![base_level];
|
||||
|
||||
// Loop until all active squares have been processed, meaning all of the dartboard domain has been checked
|
||||
while active_list_levels.iter().any(|active_list| active_list.not_empty()) {
|
||||
// Randomly pick a square in the dartboard domain, with probability proportional to its area
|
||||
let (active_square_level, active_square_index_in_level) = target_active_square(&active_list_levels, &mut rng);
|
||||
|
||||
// The level contains the list of all active squares at this target square's subdivision depth
|
||||
let level = &mut active_list_levels[active_square_level];
|
||||
|
||||
// Take the targetted active square out of the list and get its size
|
||||
let active_square = level.take_square(active_square_index_in_level);
|
||||
let active_square_size = level.square_size();
|
||||
|
||||
// Skip this target square if it's within range of any current points, since more nearby points could have been added after this square was included in the active list
|
||||
if !square_not_covered_by_poisson_points(active_square.top_left_corner(), active_square_size / 2., diameter_squared, &points_grid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Throw a dart by picking a random point within this target square
|
||||
let point = {
|
||||
let active_top_left_corner = active_square.top_left_corner();
|
||||
let x = active_top_left_corner.x + rng() * active_square_size;
|
||||
let y = active_top_left_corner.y + rng() * active_square_size;
|
||||
(x, y).into()
|
||||
};
|
||||
|
||||
// If the dart hit a valid spot, save that point (we're now permanently done with this target square's region)
|
||||
if point_not_covered_by_poisson_points(point, diameter_squared, &points_grid) {
|
||||
// Silently reject the point if it lies outside the shape
|
||||
if active_square.fully_in_shape() || point_in_shape_checker(point + offset) {
|
||||
points_grid.insert(point);
|
||||
}
|
||||
}
|
||||
// Otherwise, subdivide this target square and add valid sub-squares back to the active list for later targetting
|
||||
else {
|
||||
// Discard any targetable domain smaller than this limited number of subdivision levels since it's too small to matter
|
||||
let next_level_deeper_level = active_square_level + 1;
|
||||
if next_level_deeper_level > DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If necessary for the following step, add another layer of depth to store squares at the next subdivision level
|
||||
if active_list_levels.len() <= next_level_deeper_level {
|
||||
active_list_levels.push(ActiveListLevel::new(active_square_size / 2.))
|
||||
}
|
||||
|
||||
// Get the list of active squares at the level of depth beneath this target square's level
|
||||
let next_level_deeper = &mut active_list_levels[next_level_deeper_level];
|
||||
|
||||
// Subdivide this target square into four sub-squares; running out of numerical precision will make this terminate at very small scales
|
||||
let subdivided_size = active_square_size / 2.;
|
||||
let active_top_left_corner = active_square.top_left_corner();
|
||||
let subdivided = [
|
||||
active_top_left_corner + DVec2::new(0., 0.),
|
||||
active_top_left_corner + DVec2::new(subdivided_size, 0.),
|
||||
active_top_left_corner + DVec2::new(0., subdivided_size),
|
||||
active_top_left_corner + DVec2::new(subdivided_size, subdivided_size),
|
||||
];
|
||||
|
||||
// Add the sub-squares which aren't within the radius of a nearby point to the sub-level's active list
|
||||
let half_subdivided_size = subdivided_size / 2.;
|
||||
let new_sub_squares = subdivided.into_iter().filter_map(|sub_square| {
|
||||
// Any sub-squares within the radius of a nearby point are filtered out
|
||||
if !square_not_covered_by_poisson_points(sub_square, half_subdivided_size, diameter_squared, &points_grid) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Fully inside the shape
|
||||
if active_square.fully_in_shape() {
|
||||
Some(ActiveSquare::new(sub_square, true))
|
||||
}
|
||||
// Intersecting the shape's border
|
||||
else {
|
||||
// The sub-square is fully inside the shape if its top-left corner is inside and its edges don't intersect the shape border
|
||||
let point_with_offset = sub_square + offset;
|
||||
let square_edges_intersect_shape = {
|
||||
let min = point_with_offset;
|
||||
let max = min + DVec2::splat(subdivided_size);
|
||||
|
||||
// Top edge line
|
||||
line_intersect_shape_checker((min.x, min.y), (max.x, min.y)) ||
|
||||
// Right edge line
|
||||
line_intersect_shape_checker((max.x, min.y), (max.x, max.y)) ||
|
||||
// Bottom edge line
|
||||
line_intersect_shape_checker((max.x, max.y), (min.x, max.y)) ||
|
||||
// Left edge line
|
||||
line_intersect_shape_checker((min.x, max.y), (min.x, min.y))
|
||||
};
|
||||
let sub_square_fully_inside_shape = !square_edges_intersect_shape && point_in_shape_checker(point_with_offset) && point_in_shape_checker(point_with_offset + subdivided_size);
|
||||
|
||||
Some(ActiveSquare::new(sub_square, sub_square_fully_inside_shape))
|
||||
}
|
||||
});
|
||||
next_level_deeper.add_squares(new_sub_squares);
|
||||
}
|
||||
}
|
||||
|
||||
points_grid.final_points(offset)
|
||||
}
|
||||
|
||||
/// Randomly pick a square in the dartboard domain, with probability proportional to its area.
|
||||
/// Returns a tuple with the subdivision level depth and the square index at that depth.
|
||||
fn target_active_square(active_list_levels: &[ActiveListLevel], rng: &mut impl FnMut() -> f64) -> (usize, usize) {
|
||||
let active_squares_total_area: f64 = active_list_levels.iter().map(|active_list| active_list.total_area()).sum();
|
||||
let mut index_into_area = rng() * active_squares_total_area;
|
||||
|
||||
for (level, active_list_level) in active_list_levels.iter().enumerate() {
|
||||
let subtracted = index_into_area - active_list_level.total_area();
|
||||
if subtracted > 0. {
|
||||
index_into_area = subtracted;
|
||||
continue;
|
||||
}
|
||||
|
||||
let active_square_index_in_level = (index_into_area / active_list_levels[level].square_area()).floor() as usize;
|
||||
return (level, active_square_index_in_level);
|
||||
}
|
||||
|
||||
panic!("index_into_area couldn't be be mapped to a square in any level of the active lists");
|
||||
}
|
||||
|
||||
fn point_not_covered_by_poisson_points(point: DVec2, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool {
|
||||
points_grid.nearby_points(point).all(|nearby_point| {
|
||||
let x_separation = nearby_point.x - point.x;
|
||||
let y_separation = nearby_point.y - point.y;
|
||||
|
||||
x_separation.powi(2) + y_separation.powi(2) > diameter_squared
|
||||
})
|
||||
}
|
||||
|
||||
fn square_not_covered_by_poisson_points(point: DVec2, half_square_size: f64, diameter_squared: f64, points_grid: &AccelerationGrid) -> bool {
|
||||
let square_center_x = point.x + half_square_size;
|
||||
let square_center_y = point.y + half_square_size;
|
||||
|
||||
points_grid.nearby_points(point).all(|nearby_point| {
|
||||
let x_distance = (square_center_x - nearby_point.x).abs() + half_square_size;
|
||||
let y_distance = (square_center_y - nearby_point.y).abs() + half_square_size;
|
||||
|
||||
x_distance.powi(2) + y_distance.powi(2) > diameter_squared
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cartesian_product<A, B>(a: A, b: B) -> impl Iterator<Item = (A::Item, B::Item)>
|
||||
where
|
||||
A: Iterator + Clone,
|
||||
B: Iterator + Clone,
|
||||
A::Item: Clone,
|
||||
B::Item: Clone,
|
||||
{
|
||||
a.flat_map(move |i| b.clone().map(move |j| (i.clone(), j)))
|
||||
}
|
||||
|
||||
/// A square (represented by its top left corner position and width/height of `square_size`) that is currently a candidate for targetting by the dart throwing process.
|
||||
/// The positive sign bit encodes if the square is contained entirely within the masking shape, or negative if it's outside or intersects the shape path.
|
||||
pub struct ActiveSquare(DVec2);
|
||||
|
||||
impl ActiveSquare {
|
||||
pub fn new(top_left_corner: DVec2, fully_in_shape: bool) -> Self {
|
||||
Self(if fully_in_shape { top_left_corner } else { -top_left_corner })
|
||||
}
|
||||
|
||||
pub fn top_left_corner(&self) -> DVec2 {
|
||||
self.0.abs()
|
||||
}
|
||||
|
||||
pub fn fully_in_shape(&self) -> bool {
|
||||
self.0.x.is_sign_positive()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveListLevel {
|
||||
/// List of all subdivided squares of the same size that are currently candidates for targetting by the dart throwing process
|
||||
active_squares: Vec<ActiveSquare>,
|
||||
/// Width and height of the squares in this level of subdivision
|
||||
square_size: f64,
|
||||
/// Current sum of the area in all active squares in this subdivision level
|
||||
total_area: f64,
|
||||
}
|
||||
|
||||
impl ActiveListLevel {
|
||||
#[inline(always)]
|
||||
pub fn new(square_size: f64) -> Self {
|
||||
Self {
|
||||
active_squares: Vec::new(),
|
||||
square_size,
|
||||
total_area: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_filled(
|
||||
square_size: f64,
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
) -> Self {
|
||||
// These should divide evenly but rounding is to protect against small numerical imprecision errors
|
||||
let x_squares = (width / square_size).round() as usize;
|
||||
let y_squares = (height / square_size).round() as usize;
|
||||
|
||||
// Hashes based on the grid cell coordinates and direction of the line: (x, y, is_vertical)
|
||||
let mut line_intersection_cache: HashMap<(usize, usize, bool), bool> = HashMap::new();
|
||||
|
||||
// Populate each square with its top-left corner coordinate
|
||||
let active_squares: Vec<_> = cartesian_product(0..x_squares, 0..y_squares)
|
||||
.filter_map(|(x, y)| {
|
||||
let corner = DVec2::new(x as f64 * square_size, y as f64 * square_size);
|
||||
let corner_with_offset = corner + offset;
|
||||
|
||||
// Lazily check (and cache) if the square's edges intersect the shape, which is an expensive operation
|
||||
let mut square_edges_intersect_shape_value = None;
|
||||
let mut square_edges_intersect_shape = || {
|
||||
square_edges_intersect_shape_value.unwrap_or_else(|| {
|
||||
let square_edges_intersect_shape = {
|
||||
let min = corner_with_offset;
|
||||
let max = min + DVec2::splat(square_size);
|
||||
|
||||
// Top edge line
|
||||
*line_intersection_cache.entry((x, y, false)).or_insert_with(|| line_intersect_shape_checker((min.x, min.y), (max.x, min.y))) ||
|
||||
// Right edge line
|
||||
*line_intersection_cache.entry((x + 1, y, true)).or_insert_with(|| line_intersect_shape_checker((max.x, min.y), (max.x, max.y))) ||
|
||||
// Bottom edge line
|
||||
*line_intersection_cache.entry((x, y + 1, false)).or_insert_with(|| line_intersect_shape_checker((max.x, max.y), (min.x, max.y))) ||
|
||||
// Left edge line
|
||||
*line_intersection_cache.entry((x, y, true)).or_insert_with(|| line_intersect_shape_checker((min.x, max.y), (min.x, min.y)))
|
||||
};
|
||||
square_edges_intersect_shape_value = Some(square_edges_intersect_shape);
|
||||
square_edges_intersect_shape
|
||||
})
|
||||
};
|
||||
|
||||
// Check if this cell's top-left corner is inside the shape
|
||||
let point_in_shape = point_in_shape_checker(corner_with_offset);
|
||||
|
||||
// Determine if the square is inside the shape
|
||||
let square_not_outside_shape = point_in_shape || square_edges_intersect_shape();
|
||||
if square_not_outside_shape {
|
||||
// Check if this cell's bottom-right corner is inside the shape
|
||||
let opposite_corner_with_offset = DVec2::new((x + 1) as f64 * square_size, (y + 1) as f64 * square_size) + offset;
|
||||
let opposite_corner_in_shape = point_in_shape_checker(opposite_corner_with_offset);
|
||||
|
||||
let square_in_shape = opposite_corner_in_shape && !square_edges_intersect_shape();
|
||||
Some(ActiveSquare::new(corner, square_in_shape))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sum every square's area to get the total
|
||||
let total_area = square_size.powi(2) * active_squares.len() as f64;
|
||||
|
||||
Self {
|
||||
active_squares,
|
||||
square_size,
|
||||
total_area,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline(always)]
|
||||
pub fn take_square(&mut self, active_square_index: usize) -> ActiveSquare {
|
||||
let targetted_square = self.active_squares.swap_remove(active_square_index);
|
||||
self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64;
|
||||
targetted_square
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_squares(&mut self, new_squares: impl Iterator<Item = ActiveSquare>) {
|
||||
for new_square in new_squares {
|
||||
self.active_squares.push(new_square);
|
||||
}
|
||||
self.total_area = self.square_size.powi(2) * self.active_squares.len() as f64;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn square_size(&self) -> f64 {
|
||||
self.square_size
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn square_area(&self) -> f64 {
|
||||
self.square_size.powi(2)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn total_area(&self) -> f64 {
|
||||
self.total_area
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn not_empty(&self) -> bool {
|
||||
!self.active_squares.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PointsList {
|
||||
// The worst-case number of points in a 3x3 grid is 16 (one at each intersection of the four gridlines per axis)
|
||||
storage_slots: [DVec2; 16],
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl PointsList {
|
||||
#[inline(always)]
|
||||
pub fn push(&mut self, point: DVec2) {
|
||||
self.storage_slots[self.length] = point;
|
||||
self.length += 1;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn list_cell_and_neighbors(&self) -> impl Iterator<Item = DVec2> {
|
||||
// The negative bit is used to store whether a point belongs to a neighboring cell
|
||||
self.storage_slots.into_iter().take(self.length).map(|point| (point.x.abs(), point.y.abs()).into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn list_cell(&self) -> impl Iterator<Item = DVec2> {
|
||||
// The negative bit is used to store whether a point belongs to a neighboring cell
|
||||
self.storage_slots
|
||||
.into_iter()
|
||||
.take(self.length)
|
||||
.filter(|point| point.x.is_sign_positive() && point.y.is_sign_positive())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccelerationGrid {
|
||||
size: f64,
|
||||
dimension_x: usize,
|
||||
dimension_y: usize,
|
||||
cells: Vec<PointsList>,
|
||||
}
|
||||
|
||||
impl AccelerationGrid {
|
||||
#[inline(always)]
|
||||
pub fn new(width: f64, height: f64, size: f64) -> Self {
|
||||
let dimension_x = (width / size).ceil() as usize + 1;
|
||||
let dimension_y = (height / size).ceil() as usize + 1;
|
||||
|
||||
Self {
|
||||
size,
|
||||
dimension_x,
|
||||
dimension_y,
|
||||
cells: vec![PointsList::default(); dimension_x * dimension_y],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn insert(&mut self, point: DVec2) {
|
||||
let x = (point.x / self.size).floor() as usize;
|
||||
let y = (point.y / self.size).floor() as usize;
|
||||
|
||||
// Insert this point at this cell and the surrounding cells in a 3x3 patch
|
||||
for (x_offset, y_offset) in cartesian_product((-1)..=1, (-1)..=1) {
|
||||
// Avoid going negative
|
||||
let (x, y) = (x as isize + x_offset, y as isize + y_offset);
|
||||
if x < 0 || y < 0 {
|
||||
continue;
|
||||
}
|
||||
// Avoid going beyond the width or height
|
||||
let (x, y) = (x as usize, y as usize);
|
||||
if x > self.dimension_x - 1 || y > self.dimension_y - 1 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the cell corresponding to the (x, y) index
|
||||
let cell = &mut self.cells[y * self.dimension_x + x];
|
||||
|
||||
// Store the given point in this grid cell, and use the negative bit to indicate if this belongs to a neighboring cell
|
||||
cell.push(if x_offset == 0 && y_offset == 0 { point } else { -point });
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn nearby_points(&self, point: DVec2) -> impl Iterator<Item = DVec2> {
|
||||
let x = (point.x / self.size).floor() as usize;
|
||||
let y = (point.y / self.size).floor() as usize;
|
||||
|
||||
self.cells[y * self.dimension_x + x].list_cell_and_neighbors()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn final_points(&self, offset: DVec2) -> Vec<DVec2> {
|
||||
self.cells.iter().flat_map(|cell| cell.list_cell()).map(|point| point + offset).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use glam::DVec2;
|
||||
|
||||
/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.)
|
||||
pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec<DVec2> {
|
||||
let len_points = points.len();
|
||||
if len_points == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
if len_points == 1 {
|
||||
return vec![points[0]];
|
||||
}
|
||||
|
||||
// Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html).
|
||||
// Because the `a` coefficients are all 1, they need not be stored.
|
||||
// This algorithm does a variation of the above algorithm.
|
||||
// Instead of using the traditional cubic (a + bt + ct^2 + dt^3), we use the bezier cubic.
|
||||
|
||||
let mut b = vec![DVec2::new(4., 4.); len_points];
|
||||
b[0] = DVec2::new(2., 2.);
|
||||
b[len_points - 1] = DVec2::new(2., 2.);
|
||||
|
||||
let mut c = vec![DVec2::new(1., 1.); len_points];
|
||||
|
||||
// 'd' is the the second point in a cubic bezier, which is what we solve for
|
||||
let mut d = vec![DVec2::ZERO; len_points];
|
||||
|
||||
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
|
||||
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
|
||||
for idx in 1..(len_points - 1) {
|
||||
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
|
||||
}
|
||||
|
||||
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
// Now we do row operations to eliminate `a` coefficients.
|
||||
c[0] /= -b[0];
|
||||
d[0] /= -b[0];
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in 1..len_points {
|
||||
b[i] += c[i - 1];
|
||||
// For some reason this `+=` version makes the borrow checker mad:
|
||||
// d[i] += d[i-1]
|
||||
d[i] = d[i] + d[i - 1];
|
||||
c[i] /= -b[i];
|
||||
d[i] /= -b[i];
|
||||
}
|
||||
|
||||
// At this point b[i] == -a[i + 1] and a[i] == 0.
|
||||
// Now we do row operations to eliminate 'c' coefficients and solve.
|
||||
d[len_points - 1] *= -1.;
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in (0..len_points - 1).rev() {
|
||||
d[i] = d[i] - (c[i] * d[i + 1]);
|
||||
d[i] *= -1.; // d[i] /= b[i]
|
||||
}
|
||||
|
||||
d
|
||||
}
|
||||
|
||||
/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.)
|
||||
/// If called with fewer than 3 points, this function will return an empty result.
|
||||
pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec<DVec2> {
|
||||
let len_points = points.len();
|
||||
if len_points < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html).
|
||||
// We don't really need to allocate them but it keeps the maths understandable.
|
||||
let a = vec![DVec2::ONE; len_points];
|
||||
let b = vec![DVec2::splat(4.); len_points];
|
||||
let c = vec![DVec2::ONE; len_points];
|
||||
|
||||
let mut cmod = vec![DVec2::ZERO; len_points];
|
||||
let mut u = vec![DVec2::ZERO; len_points];
|
||||
|
||||
// `x` is initially the output of the matrix multiplication, but is converted to the second value.
|
||||
let mut x = vec![DVec2::ZERO; len_points];
|
||||
|
||||
for (i, point) in x.iter_mut().enumerate() {
|
||||
let previous_i = i.checked_sub(1).unwrap_or(len_points - 1);
|
||||
let next_i = (i + 1) % len_points;
|
||||
*point = 3. * (points[next_i] - points[previous_i]);
|
||||
}
|
||||
|
||||
// Solve using https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants (the variant using periodic boundary conditions).
|
||||
// This code below is based on the reference C language implementation provided in that section of the article.
|
||||
let alpha = a[0];
|
||||
let beta = c[len_points - 1];
|
||||
|
||||
// Arbitrary, but chosen such that division by zero is avoided.
|
||||
let gamma = -b[0];
|
||||
|
||||
cmod[0] = alpha / (b[0] - gamma);
|
||||
u[0] = gamma / (b[0] - gamma);
|
||||
x[0] /= b[0] - gamma;
|
||||
|
||||
// Handle from from `1` to `len_points - 2` (inclusive).
|
||||
for ix in 1..=(len_points - 2) {
|
||||
let m = 1.0 / (b[ix] - a[ix] * cmod[ix - 1]);
|
||||
cmod[ix] = c[ix] * m;
|
||||
u[ix] = (0.0 - a[ix] * u[ix - 1]) * m;
|
||||
x[ix] = (x[ix] - a[ix] * x[ix - 1]) * m;
|
||||
}
|
||||
|
||||
// Handle `len_points - 1`.
|
||||
let m = 1.0 / (b[len_points - 1] - alpha * beta / gamma - beta * cmod[len_points - 2]);
|
||||
u[len_points - 1] = (alpha - a[len_points - 1] * u[len_points - 2]) * m;
|
||||
x[len_points - 1] = (x[len_points - 1] - a[len_points - 1] * x[len_points - 2]) * m;
|
||||
|
||||
// Loop from `len_points - 2` to `0` (inclusive).
|
||||
for ix in (0..=(len_points - 2)).rev() {
|
||||
u[ix] = u[ix] - cmod[ix] * u[ix + 1];
|
||||
x[ix] = x[ix] - cmod[ix] * x[ix + 1];
|
||||
}
|
||||
|
||||
let fact = (x[0] + x[len_points - 1] * beta / gamma) / (1.0 + u[0] + u[len_points - 1] * beta / gamma);
|
||||
|
||||
for ix in 0..(len_points) {
|
||||
x[ix] -= fact * u[ix];
|
||||
}
|
||||
|
||||
let mut real = vec![DVec2::ZERO; len_points];
|
||||
for i in 0..len_points {
|
||||
let previous = i.checked_sub(1).unwrap_or(len_points - 1);
|
||||
let next = (i + 1) % len_points;
|
||||
real[i] = x[previous] * a[next] + x[i] * b[i] + x[next] * c[i];
|
||||
}
|
||||
|
||||
// The matrix is now solved.
|
||||
|
||||
// Since we have computed the derivative, work back to find the start handle.
|
||||
for i in 0..len_points {
|
||||
x[i] = (x[i] / 3.) + points[i];
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn closed_spline() {
|
||||
use crate::vector::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use kurbo::{BezPath, ParamCurve, ParamCurveDeriv};
|
||||
|
||||
// These points are just chosen arbitrary
|
||||
let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)];
|
||||
|
||||
// List of first handle or second point in a cubic bezier curve.
|
||||
let first_handles = solve_spline_first_handle_closed(&points);
|
||||
|
||||
// Construct the Subpath
|
||||
let mut bezpath = BezPath::new();
|
||||
bezpath.move_to(dvec2_to_point(points[0]));
|
||||
|
||||
for i in 0..first_handles.len() {
|
||||
let next_i = i + 1;
|
||||
let next_i = if next_i == first_handles.len() { 0 } else { next_i };
|
||||
|
||||
// First handle or second point of a cubic Bezier curve.
|
||||
let p1 = dvec2_to_point(first_handles[i]);
|
||||
// Second handle or third point of a cubic Bezier curve.
|
||||
let p2 = dvec2_to_point(2. * points[next_i] - first_handles[next_i]);
|
||||
// Endpoint or fourth point of a cubic Bezier curve.
|
||||
let p3 = dvec2_to_point(points[next_i]);
|
||||
|
||||
bezpath.curve_to(p1, p2, p3);
|
||||
}
|
||||
|
||||
// For each pair of bézier curves, ensure that the second derivative is continuous
|
||||
for (bézier_a, bézier_b) in bezpath.segments().zip(bezpath.segments().skip(1).chain(bezpath.segments().take(1))) {
|
||||
let derivative2_end_a = point_to_dvec2(bézier_a.to_cubic().deriv().eval(1.));
|
||||
let derivative2_start_b = point_to_dvec2(bézier_b.to_cubic().deriv().eval(0.));
|
||||
|
||||
assert!(
|
||||
derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10),
|
||||
"second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use glam::DVec2;
|
||||
use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg};
|
||||
|
||||
pub fn pathseg_tangent(segment: PathSeg, t: f64) -> DVec2 {
|
||||
// NOTE: .deriv() method gives inaccurate result when it is 1.
|
||||
let t = if t == 1. { 1. - f64::EPSILON } else { t };
|
||||
|
||||
let tangent = match segment {
|
||||
PathSeg::Line(line) => line.deriv().eval(t),
|
||||
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
|
||||
};
|
||||
|
||||
DVec2::new(tangent.x, tangent.y)
|
||||
}
|
||||
|
||||
// Compare two f64s with some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
|
||||
(f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE
|
||||
}
|
||||
|
||||
/// Compare points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_points(p1: kurbo::Point, p2: kurbo::Point) -> bool {
|
||||
let (p1, p2) = (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2));
|
||||
p1.abs_diff_eq(p2, super::contants::MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
||||
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_vec_of_points(a: Vec<kurbo::Point>, b: Vec<kurbo::Point>, max_absolute_difference: f64) -> bool {
|
||||
a.len() == b.len()
|
||||
&& a.into_iter()
|
||||
.zip(b)
|
||||
.map(|(p1, p2)| (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2)))
|
||||
.all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
|
||||
}
|
||||
|
||||
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
|
||||
#[cfg(test)]
|
||||
pub fn dvec2_compare(a: kurbo::Point, b: kurbo::Point, max_abs_diff: f64) -> glam::BVec2 {
|
||||
glam::BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff)
|
||||
}
|
||||
456
node-graph/libraries/vector-types/src/vector/click_target.rs
Normal file
456
node-graph/libraries/vector-types/src/vector/click_target.rs
Normal file
@@ -0,0 +1,456 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use super::algorithms::{bezpath_algorithms::bezpath_is_inside_bezpath, intersection::filtered_segment_intersections};
|
||||
use super::misc::dvec2_to_point;
|
||||
use crate::math::QuadExt;
|
||||
use crate::subpath::Subpath;
|
||||
use crate::vector::PointId;
|
||||
use crate::vector::misc::point_to_dvec2;
|
||||
use core_types::math::quad::Quad;
|
||||
use core_types::transform::Transform;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, ParamCurve, PathSeg, Shape};
|
||||
|
||||
type BoundingBox = Option<[DVec2; 2]>;
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
/// Fixed-size ring buffer cache for rotated bounding boxes.
|
||||
///
|
||||
/// Stores up to 8 rotation angles and their corresponding bounding boxes to avoid
|
||||
/// recomputing expensive bezier curve bounds for repeated rotations. Uses 7-bit
|
||||
/// fingerprint hashing with MSB as presence flag for fast lookup.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct BoundingBoxCache {
|
||||
/// Packed 7-bit fingerprints with MSB presence flags for cache lookup
|
||||
fingerprints: u64,
|
||||
/// (rotation_angle, cached_bounds) pairs
|
||||
elements: [(f64, BoundingBox); Self::CACHE_SIZE],
|
||||
/// Next position to write in ring buffer
|
||||
write_ptr: usize,
|
||||
}
|
||||
|
||||
impl BoundingBoxCache {
|
||||
/// Cache size - must be ≤ 8 since fingerprints is u64 (8 bytes, 1 byte per element)
|
||||
const CACHE_SIZE: usize = 8;
|
||||
const FINGERPRINT_BITS: u32 = 7;
|
||||
const PRESENCE_FLAG: u8 = 1 << Self::FINGERPRINT_BITS;
|
||||
|
||||
/// Generates a 7-bit fingerprint from rotation with MSB as presence flag
|
||||
fn rotation_fingerprint(rotation: f64) -> u8 {
|
||||
(rotation.to_bits() % (1 << Self::FINGERPRINT_BITS)) as u8 | Self::PRESENCE_FLAG
|
||||
}
|
||||
/// Attempts to find cached bounding box for the given rotation.
|
||||
/// Returns Some(bounds) if found, None if not cached.
|
||||
fn try_read(&self, rotation: f64, scale: DVec2, translation: DVec2, fingerprint: u8) -> Option<BoundingBox> {
|
||||
// Build bitmask of positions with matching fingerprints for vectorized comparison
|
||||
let mut mask: u8 = 0;
|
||||
for (i, fp) in (0..Self::CACHE_SIZE).zip(self.fingerprints.to_le_bytes()) {
|
||||
// Check MSB for presence and lower 7 bits for fingerprint match
|
||||
if fp == fingerprint {
|
||||
mask |= 1 << i;
|
||||
}
|
||||
}
|
||||
// Check each position with matching fingerprint for exact rotation match
|
||||
while mask != 0 {
|
||||
let pos = mask.trailing_zeros() as usize;
|
||||
|
||||
if rotation == self.elements[pos].0 {
|
||||
// Found cached rotation - apply scale and translation to cached bounds
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation);
|
||||
let new_bounds = self.elements[pos].1.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]);
|
||||
|
||||
return Some(new_bounds);
|
||||
}
|
||||
mask &= !(1 << pos);
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Computes and caches bounding box for the given rotation, then applies scale/translation.
|
||||
/// Returns the final transformed bounds.
|
||||
fn add_to_cache(&mut self, subpath: &Subpath<PointId>, rotation: f64, scale: DVec2, translation: DVec2, fingerprint: u8) -> BoundingBox {
|
||||
// Compute bounds for pure rotation (expensive operation we want to cache)
|
||||
let bounds = subpath.bounding_box_with_transform(DAffine2::from_angle(rotation));
|
||||
|
||||
if bounds.is_none() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
// Store in ring buffer at current write position
|
||||
let write_ptr = self.write_ptr;
|
||||
self.elements[write_ptr] = (rotation, bounds);
|
||||
|
||||
// Update fingerprint byte for this position
|
||||
let mut bytes = self.fingerprints.to_le_bytes();
|
||||
bytes[write_ptr] = fingerprint;
|
||||
self.fingerprints = u64::from_le_bytes(bytes);
|
||||
|
||||
// Advance write pointer (ring buffer behavior)
|
||||
self.write_ptr = (write_ptr + 1) % Self::CACHE_SIZE;
|
||||
|
||||
// Apply scale and translation to cached rotated bounds
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation);
|
||||
bounds.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a clickable target for the layer
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClickTarget {
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: BoundingBox,
|
||||
#[serde(skip)]
|
||||
bounding_box_cache: Arc<RwLock<BoundingBoxCache>>,
|
||||
}
|
||||
|
||||
impl PartialEq for ClickTarget {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.target_type == other.target_type && self.stroke_width == other.stroke_width && self.bounding_box == other.bounding_box
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
bounding_box_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
bounding_box_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_type(&self) -> &ClickTargetType {
|
||||
&self.target_type
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> BoundingBox {
|
||||
self.bounding_box
|
||||
}
|
||||
|
||||
pub fn bounding_box_center(&self) -> Option<DVec2> {
|
||||
self.bounding_box.map(|bbox| bbox[0] + (bbox[1] - bbox[0]) / 2.)
|
||||
}
|
||||
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> BoundingBox {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
// Bypass cache for skewed transforms since rotation decomposition isn't valid
|
||||
if transform.has_skew() {
|
||||
return subpath.bounding_box_with_transform(transform);
|
||||
}
|
||||
|
||||
// Decompose transform into rotation, scale, translation for caching strategy
|
||||
let rotation = transform.decompose_rotation();
|
||||
let scale = transform.decompose_scale();
|
||||
let translation = transform.translation;
|
||||
|
||||
// Generate fingerprint for cache lookup
|
||||
let fingerprint = BoundingBoxCache::rotation_fingerprint(rotation);
|
||||
|
||||
// Try to read from cache first
|
||||
let read_lock = self.bounding_box_cache.read().unwrap();
|
||||
if let Some(value) = read_lock.try_read(rotation, scale, translation, fingerprint) {
|
||||
return value;
|
||||
}
|
||||
std::mem::drop(read_lock);
|
||||
|
||||
// Cache miss - compute and store new entry
|
||||
let mut write_lock = self.bounding_box_cache.write().unwrap();
|
||||
write_lock.add_to_cache(subpath, rotation, scale, translation, fingerprint)
|
||||
}
|
||||
// TODO: use point for calculation of bbox
|
||||
ClickTargetType::FreePoint(_) => 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 = PathSeg>>(&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| Affine::new(inverse.to_cols_array()) * bezier);
|
||||
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: PathSeg| bezier_iter().any(|line| !filtered_segment_intersections(path_segment, 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(point_to_dvec2(bezier.start()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut selection = BezPath::from_path_segments(bezier_iter());
|
||||
selection.close_path();
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
bezpath_is_inside_bezpath(&subpath.to_bezpath(), &selection, None, None)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: PathSeg| bezier.winding(dvec2_to_point(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.to_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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::subpath::Subpath;
|
||||
use glam::DVec2;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[test]
|
||||
fn test_bounding_box_cache_fingerprint_generation() {
|
||||
// Test that fingerprints have MSB set and use only 7 bits for data
|
||||
let rotation1 = 0.0;
|
||||
let rotation2 = PI / 3.0;
|
||||
let rotation3 = PI / 2.0;
|
||||
|
||||
let fp1 = BoundingBoxCache::rotation_fingerprint(rotation1);
|
||||
let fp2 = BoundingBoxCache::rotation_fingerprint(rotation2);
|
||||
let fp3 = BoundingBoxCache::rotation_fingerprint(rotation3);
|
||||
|
||||
// All fingerprints should have MSB set (presence flag)
|
||||
assert_eq!(fp1 & BoundingBoxCache::PRESENCE_FLAG, BoundingBoxCache::PRESENCE_FLAG);
|
||||
assert_eq!(fp2 & BoundingBoxCache::PRESENCE_FLAG, BoundingBoxCache::PRESENCE_FLAG);
|
||||
assert_eq!(fp3 & BoundingBoxCache::PRESENCE_FLAG, BoundingBoxCache::PRESENCE_FLAG);
|
||||
|
||||
// Lower 7 bits should contain the actual fingerprint data
|
||||
let data1 = fp1 & !BoundingBoxCache::PRESENCE_FLAG;
|
||||
let data2 = fp2 & !BoundingBoxCache::PRESENCE_FLAG;
|
||||
let data3 = fp3 & !BoundingBoxCache::PRESENCE_FLAG;
|
||||
|
||||
// Data portions should be different (unless collision)
|
||||
assert!(data1 != data2 && data2 != data3 && data3 != data1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounding_box_cache_basic_operations() {
|
||||
let mut cache = BoundingBoxCache::default();
|
||||
|
||||
// Create a simple rectangle subpath for testing
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::new(100.0, 50.0));
|
||||
|
||||
let rotation = PI / 4.0;
|
||||
let scale = DVec2::new(2.0, 2.0);
|
||||
let translation = DVec2::new(10.0, 20.0);
|
||||
let fingerprint = BoundingBoxCache::rotation_fingerprint(rotation);
|
||||
|
||||
// Cache should be empty initially
|
||||
assert!(cache.try_read(rotation, scale, translation, fingerprint).is_none());
|
||||
|
||||
// Add to cache
|
||||
let result = cache.add_to_cache(&subpath, rotation, scale, translation, fingerprint);
|
||||
assert!(result.is_some());
|
||||
|
||||
// Should now be able to read from cache
|
||||
let cached = cache.try_read(rotation, scale, translation, fingerprint);
|
||||
assert!(cached.is_some());
|
||||
assert_eq!(cached.unwrap(), result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounding_box_cache_ring_buffer_behavior() {
|
||||
let mut cache = BoundingBoxCache::default();
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::new(10.0, 10.0));
|
||||
let scale = DVec2::ONE;
|
||||
let translation = DVec2::ZERO;
|
||||
|
||||
// Fill cache beyond capacity to test ring buffer behavior
|
||||
let rotations: Vec<f64> = (0..10).map(|i| i as f64 * PI / 8.0).collect();
|
||||
|
||||
for rotation in &rotations {
|
||||
let fingerprint = BoundingBoxCache::rotation_fingerprint(*rotation);
|
||||
cache.add_to_cache(&subpath, *rotation, scale, translation, fingerprint);
|
||||
}
|
||||
|
||||
// First two entries should be overwritten (cache size is 8)
|
||||
let first_fp = BoundingBoxCache::rotation_fingerprint(rotations[0]);
|
||||
let second_fp = BoundingBoxCache::rotation_fingerprint(rotations[1]);
|
||||
let last_fp = BoundingBoxCache::rotation_fingerprint(rotations[9]);
|
||||
|
||||
assert!(cache.try_read(rotations[0], scale, translation, first_fp).is_none());
|
||||
assert!(cache.try_read(rotations[1], scale, translation, second_fp).is_none());
|
||||
assert!(cache.try_read(rotations[9], scale, translation, last_fp).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_click_target_bounding_box_caching() {
|
||||
// Create a click target with a simple rectangle
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::new(100.0, 50.0));
|
||||
let click_target = ClickTarget::new_with_subpath(subpath, 1.0);
|
||||
|
||||
let rotation = PI / 6.0;
|
||||
let scale = DVec2::new(1.5, 1.5);
|
||||
let translation = DVec2::new(20.0, 30.0);
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, rotation, translation);
|
||||
|
||||
// Helper function to count present values in cache
|
||||
let count_present_values = || {
|
||||
let cache = click_target.bounding_box_cache.read().unwrap();
|
||||
cache.fingerprints.to_le_bytes().iter().filter(|&&fp| fp & BoundingBoxCache::PRESENCE_FLAG != 0).count()
|
||||
};
|
||||
|
||||
// Initially cache should be empty
|
||||
assert_eq!(count_present_values(), 0);
|
||||
|
||||
// First call should compute and cache
|
||||
let result1 = click_target.bounding_box_with_transform(transform);
|
||||
assert!(result1.is_some());
|
||||
assert_eq!(count_present_values(), 1);
|
||||
|
||||
// Second call with same transform should use cache, not add new entry
|
||||
let result2 = click_target.bounding_box_with_transform(transform);
|
||||
assert_eq!(result1, result2);
|
||||
assert_eq!(count_present_values(), 1); // Should still be 1, not 2
|
||||
|
||||
// Different scale/translation but same rotation should use cached rotation
|
||||
let transform2 = DAffine2::from_scale_angle_translation(DVec2::new(2.0, 2.0), rotation, DVec2::new(50.0, 60.0));
|
||||
let result3 = click_target.bounding_box_with_transform(transform2);
|
||||
assert!(result3.is_some());
|
||||
assert_ne!(result1, result3); // Different due to different scale/translation
|
||||
assert_eq!(count_present_values(), 1); // Should still be 1, reused same rotation
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_click_target_skew_bypass_cache() {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::new(100.0, 50.0));
|
||||
let click_target = ClickTarget::new_with_subpath(subpath.clone(), 1.0);
|
||||
|
||||
// Create a transform with skew (non-uniform scaling in different directions)
|
||||
let skew_transform = DAffine2::from_cols_array(&[2.0, 0.5, 0.0, 1.0, 10.0, 20.0]);
|
||||
assert!(skew_transform.has_skew());
|
||||
|
||||
// Should bypass cache and compute directly
|
||||
let result = click_target.bounding_box_with_transform(skew_transform);
|
||||
let expected = subpath.bounding_box_with_transform(skew_transform);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_fingerprint_collision_handling() {
|
||||
let mut cache = BoundingBoxCache::default();
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::new(10.0, 10.0));
|
||||
let scale = DVec2::ONE;
|
||||
let translation = DVec2::ZERO;
|
||||
|
||||
// Find two rotations that produce the same fingerprint (collision)
|
||||
let rotation1 = 0.0;
|
||||
let rotation2 = 0.25;
|
||||
let fp1 = BoundingBoxCache::rotation_fingerprint(rotation1);
|
||||
let fp2 = BoundingBoxCache::rotation_fingerprint(rotation2);
|
||||
|
||||
// If we found a collision, test that exact rotation matching still works
|
||||
if fp1 == fp2 && rotation1 != rotation2 {
|
||||
// Add first rotation
|
||||
cache.add_to_cache(&subpath, rotation1, scale, translation, fp1);
|
||||
|
||||
// Should find the exact rotation
|
||||
assert!(cache.try_read(rotation1, scale, translation, fp1).is_some());
|
||||
|
||||
// Should not find the colliding rotation (different exact value)
|
||||
assert!(cache.try_read(rotation2, scale, translation, fp2).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
425
node-graph/libraries/vector-types/src/vector/misc.rs
Normal file
425
node-graph/libraries/vector-types/src/vector/misc.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use super::PointId;
|
||||
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::subpath::{BezierHandles, ManipulatorGroup};
|
||||
use crate::vector::{SegmentId, Vector};
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
|
||||
use std::ops::Sub;
|
||||
|
||||
/// Represents different ways of calculating the centroid.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum CentroidType {
|
||||
/// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material.
|
||||
#[default]
|
||||
Area,
|
||||
/// The center of mass for the arc length of a curved shape's perimeter, as if made out of an infinitely thin wire.
|
||||
Length,
|
||||
}
|
||||
|
||||
pub trait AsU64 {
|
||||
fn as_u64(&self) -> u64;
|
||||
}
|
||||
impl AsU64 for u32 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self as u64
|
||||
}
|
||||
}
|
||||
impl AsU64 for u64 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl AsU64 for f64 {
|
||||
fn as_u64(&self) -> u64 {
|
||||
*self as u64
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsI64 {
|
||||
fn as_i64(&self) -> i64;
|
||||
}
|
||||
impl AsI64 for u32 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
impl AsI64 for u64 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
impl AsI64 for f64 {
|
||||
fn as_i64(&self) -> i64 {
|
||||
*self as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum GridType {
|
||||
#[default]
|
||||
Rectangular = 0,
|
||||
Isometric,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum ArcType {
|
||||
#[default]
|
||||
Open = 0,
|
||||
Closed,
|
||||
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 }
|
||||
}
|
||||
|
||||
pub fn dvec2_to_point(value: DVec2) -> Point {
|
||||
Point { x: value.x, y: value.y }
|
||||
}
|
||||
|
||||
pub fn get_line_endpoints(line: Line) -> (DVec2, DVec2) {
|
||||
(point_to_dvec2(line.p0), point_to_dvec2(line.p1))
|
||||
}
|
||||
|
||||
pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles {
|
||||
match *segment {
|
||||
PathSeg::Line(_) => BezierHandles::Linear,
|
||||
PathSeg::Quad(QuadBez { p0: _, p1, p2: _ }) => BezierHandles::Quadratic { handle: point_to_dvec2(p1) },
|
||||
PathSeg::Cubic(CubicBez { p0: _, p1, p2, p3: _ }) => BezierHandles::Cubic {
|
||||
handle_start: point_to_dvec2(p1),
|
||||
handle_end: point_to_dvec2(p2),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg {
|
||||
match handles {
|
||||
BezierHandles::Linear => {
|
||||
let p0 = dvec2_to_point(start);
|
||||
let p1 = dvec2_to_point(end);
|
||||
PathSeg::Line(Line::new(p0, p1))
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let p0 = dvec2_to_point(start);
|
||||
let p1 = dvec2_to_point(handle);
|
||||
let p2 = dvec2_to_point(end);
|
||||
PathSeg::Quad(QuadBez::new(p0, p1, p2))
|
||||
}
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let p0 = dvec2_to_point(start);
|
||||
let p1 = dvec2_to_point(handle_start);
|
||||
let p2 = dvec2_to_point(handle_end);
|
||||
let p3 = dvec2_to_point(end);
|
||||
PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
|
||||
let mut bezpath = kurbo::BezPath::new();
|
||||
let mut out_handle;
|
||||
|
||||
let Some(first) = manipulator_groups.first() else { return bezpath };
|
||||
bezpath.move_to(dvec2_to_point(first.anchor));
|
||||
out_handle = first.out_handle;
|
||||
|
||||
for manipulator in manipulator_groups.iter().skip(1) {
|
||||
match (out_handle, manipulator.in_handle) {
|
||||
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
|
||||
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
|
||||
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
|
||||
}
|
||||
out_handle = manipulator.out_handle;
|
||||
}
|
||||
|
||||
if closed {
|
||||
match (out_handle, first.in_handle) {
|
||||
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
|
||||
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
|
||||
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
|
||||
}
|
||||
bezpath.close_path();
|
||||
}
|
||||
bezpath
|
||||
}
|
||||
|
||||
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup<PointId>>, bool) {
|
||||
let mut manipulator_groups = Vec::<ManipulatorGroup<PointId>>::new();
|
||||
let mut is_closed = false;
|
||||
|
||||
for element in bezpath.elements() {
|
||||
let manipulator_group = match *element {
|
||||
kurbo::PathEl::MoveTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
|
||||
kurbo::PathEl::LineTo(point) => ManipulatorGroup::new(point_to_dvec2(point), None, None),
|
||||
kurbo::PathEl::QuadTo(point, point1) => ManipulatorGroup::new(point_to_dvec2(point1), Some(point_to_dvec2(point)), None),
|
||||
kurbo::PathEl::CurveTo(point, point1, point2) => {
|
||||
if let Some(last_manipulator_group) = manipulator_groups.last_mut() {
|
||||
last_manipulator_group.out_handle = Some(point_to_dvec2(point));
|
||||
}
|
||||
ManipulatorGroup::new(point_to_dvec2(point2), Some(point_to_dvec2(point1)), None)
|
||||
}
|
||||
kurbo::PathEl::ClosePath => {
|
||||
if let Some(last_manipulators) = manipulator_groups.pop()
|
||||
&& let Some(first_manipulators) = manipulator_groups.first_mut()
|
||||
{
|
||||
first_manipulators.out_handle = last_manipulators.in_handle;
|
||||
}
|
||||
is_closed = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
manipulator_groups.push(manipulator_group);
|
||||
}
|
||||
|
||||
(manipulator_groups, is_closed)
|
||||
}
|
||||
|
||||
/// Returns true if the [`PathSeg`] is equivalent to a line.
|
||||
///
|
||||
/// This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
|
||||
pub fn is_linear(segment: PathSeg) -> bool {
|
||||
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
|
||||
|
||||
match segment {
|
||||
PathSeg::Line(_) => true,
|
||||
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
|
||||
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an vec of all the points in a path segment.
|
||||
pub fn pathseg_points_vec(segment: PathSeg) -> Vec<Point> {
|
||||
match segment {
|
||||
PathSeg::Line(line) => [line.p0, line.p1].to_vec(),
|
||||
PathSeg::Quad(quad_bez) => [quad_bez.p0, quad_bez.p1, quad_bez.p2].to_vec(),
|
||||
PathSeg::Cubic(cubic_bez) => [cubic_bez.p0, cubic_bez.p1, cubic_bez.p2, cubic_bez.p3].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other.
|
||||
pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool {
|
||||
let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 };
|
||||
let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 };
|
||||
|
||||
let seg1_points = pathseg_points_vec(seg1);
|
||||
let seg2_points = pathseg_points_vec(seg2);
|
||||
|
||||
let cmp = |a: f64, b: f64| a.sub(b).abs() < max_abs_diff;
|
||||
|
||||
seg1_points.len() == seg2_points.len() && seg1_points.into_iter().zip(seg2_points).all(|(a, b)| cmp(a.x, b.x) && cmp(a.y, b.y))
|
||||
}
|
||||
|
||||
/// 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, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ManipulatorPointId {
|
||||
/// A control anchor - the start or end point of a bézier.
|
||||
Anchor(PointId),
|
||||
/// The handle for a bézier - the first handle on a cubic and the only handle on a quadratic.
|
||||
PrimaryHandle(SegmentId),
|
||||
/// The end handle on a cubic bézier.
|
||||
EndHandle(SegmentId),
|
||||
}
|
||||
|
||||
impl ManipulatorPointId {
|
||||
/// Attempt to retrieve the manipulator position in layer space (no transformation applied).
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
pub fn get_position<Upstream: 'static>(&self, vector: &Vector<Upstream>) -> Option<DVec2> {
|
||||
match self {
|
||||
ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id),
|
||||
ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_start()),
|
||||
ManipulatorPointId::EndHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_end()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_anchor_position<Upstream: 'static>(&self, vector: &Vector<Upstream>) -> Option<DVec2> {
|
||||
match self {
|
||||
ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector).and_then(|id| vector.point_domain.position_from_id(id)),
|
||||
_ => self.get_position(vector),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle.
|
||||
#[must_use]
|
||||
pub fn get_handle_pair<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<[HandleId; 2]> {
|
||||
match self {
|
||||
ManipulatorPointId::Anchor(point) => vector.all_connected(point).take(2).collect::<Vec<_>>().try_into().ok(),
|
||||
ManipulatorPointId::PrimaryHandle(segment) => {
|
||||
let point = vector.segment_domain.segment_start_from_id(segment)?;
|
||||
let current = HandleId::primary(segment);
|
||||
let other = vector.segment_domain.all_connected(point).find(|&value| value != current);
|
||||
other.map(|other| [current, other])
|
||||
}
|
||||
ManipulatorPointId::EndHandle(segment) => {
|
||||
let point = vector.segment_domain.segment_end_from_id(segment)?;
|
||||
let current = HandleId::end(segment);
|
||||
let other = vector.segment_domain.all_connected(point).find(|&value| value != current);
|
||||
other.map(|other| [current, other])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds all the connected handles of a point.
|
||||
/// For an anchor it is all the connected handles.
|
||||
/// For a handle it is all the handles connected to its corresponding anchor other than the current handle.
|
||||
pub fn get_all_connected_handles<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<Vec<HandleId>> {
|
||||
match self {
|
||||
ManipulatorPointId::Anchor(point) => {
|
||||
let connected = vector.all_connected(point).collect::<Vec<_>>();
|
||||
Some(connected)
|
||||
}
|
||||
ManipulatorPointId::PrimaryHandle(segment) => {
|
||||
let point = vector.segment_domain.segment_start_from_id(segment)?;
|
||||
let current = HandleId::primary(segment);
|
||||
let connected = vector.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
|
||||
Some(connected)
|
||||
}
|
||||
ManipulatorPointId::EndHandle(segment) => {
|
||||
let point = vector.segment_domain.segment_end_from_id(segment)?;
|
||||
let current = HandleId::end(segment);
|
||||
let connected = vector.segment_domain.all_connected(point).filter(|&value| value != current).collect::<Vec<_>>();
|
||||
Some(connected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen.
|
||||
#[must_use]
|
||||
pub fn get_anchor<Upstream: 'static>(self, vector: &Vector<Upstream>) -> Option<PointId> {
|
||||
match self {
|
||||
ManipulatorPointId::Anchor(point) => Some(point),
|
||||
ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(segment),
|
||||
ManipulatorPointId::EndHandle(segment) => vector.segment_end_from_id(segment),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to convert self to a [`HandleId`], returning none for an anchor.
|
||||
#[must_use]
|
||||
pub fn as_handle(self) -> Option<HandleId> {
|
||||
match self {
|
||||
ManipulatorPointId::PrimaryHandle(segment) => Some(HandleId::primary(segment)),
|
||||
ManipulatorPointId::EndHandle(segment) => Some(HandleId::end(segment)),
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to convert self to an anchor, returning None for a handle.
|
||||
#[must_use]
|
||||
pub fn as_anchor(self) -> Option<PointId> {
|
||||
match self {
|
||||
ManipulatorPointId::Anchor(point) => Some(point),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_segment(self) -> Option<SegmentId> {
|
||||
match self {
|
||||
ManipulatorPointId::PrimaryHandle(segment) | ManipulatorPointId::EndHandle(segment) => Some(segment),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of handle found on a bézier curve.
|
||||
#[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,
|
||||
/// The second handle on a cubic bézier.
|
||||
End,
|
||||
}
|
||||
|
||||
/// Represents a primary or end handle found in a particular segment.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HandleId {
|
||||
pub ty: HandleType,
|
||||
pub segment: SegmentId,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HandleId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.ty {
|
||||
// I haven't checked if "out" and "in" are reversed, or are accurate translations of the "primary" and "end" terms used in the `HandleType` enum, so this naming is an assumption.
|
||||
HandleType::Primary => write!(f, "{} out", self.segment.inner()),
|
||||
HandleType::End => write!(f, "{} in", self.segment.inner()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleId {
|
||||
/// Construct a handle for the first handle on a cubic bézier or the only handle on a quadratic bézier.
|
||||
#[must_use]
|
||||
pub const fn primary(segment: SegmentId) -> Self {
|
||||
Self { ty: HandleType::Primary, segment }
|
||||
}
|
||||
|
||||
/// Construct a handle for the end handle on a cubic bézier.
|
||||
#[must_use]
|
||||
pub const fn end(segment: SegmentId) -> Self {
|
||||
Self { ty: HandleType::End, segment }
|
||||
}
|
||||
|
||||
/// Convert to [`ManipulatorPointId`].
|
||||
#[must_use]
|
||||
pub fn to_manipulator_point(self) -> ManipulatorPointId {
|
||||
match self.ty {
|
||||
HandleType::Primary => ManipulatorPointId::PrimaryHandle(self.segment),
|
||||
HandleType::End => ManipulatorPointId::EndHandle(self.segment),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the magnitude of the handle from the anchor.
|
||||
pub fn length<Upstream: 'static>(self, vector: &Vector<Upstream>) -> f64 {
|
||||
let Some(anchor_position) = self.to_manipulator_point().get_anchor_position(vector) else {
|
||||
// TODO: This was previously an unwrap which was encountered, so this is a temporary way to avoid a crash
|
||||
return 0.;
|
||||
};
|
||||
let handle_position = self.to_manipulator_point().get_position(vector);
|
||||
handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
match self.ty {
|
||||
HandleType::Primary => Self::end(self.segment),
|
||||
HandleType::End => Self::primary(self.segment),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Dropdown)]
|
||||
pub enum SpiralType {
|
||||
#[default]
|
||||
Archimedean,
|
||||
Logarithmic,
|
||||
}
|
||||
14
node-graph/libraries/vector-types/src/vector/mod.rs
Normal file
14
node-graph/libraries/vector-types/src/vector/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub mod algorithms;
|
||||
pub mod click_target;
|
||||
pub mod misc;
|
||||
pub mod reference_point;
|
||||
pub mod style;
|
||||
mod vector_attributes;
|
||||
mod vector_modification;
|
||||
mod vector_types;
|
||||
|
||||
pub use reference_point::*;
|
||||
pub use style::PathStyle;
|
||||
pub use vector_attributes::*;
|
||||
pub use vector_modification::*;
|
||||
pub use vector_types::*;
|
||||
103
node-graph/libraries/vector-types/src/vector/reference_point.rs
Normal file
103
node-graph/libraries/vector-types/src/vector/reference_point.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use core_types::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
|
||||
}
|
||||
}
|
||||
665
node-graph/libraries/vector-types/src/vector/style.rs
Normal file
665
node-graph/libraries/vector-types/src/vector/style.rs
Normal file
@@ -0,0 +1,665 @@
|
||||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
pub use crate::gradient::*;
|
||||
use core_types::Color;
|
||||
use core_types::table::Table;
|
||||
use dyn_any::DynAny;
|
||||
use glam::DAffine2;
|
||||
|
||||
/// Describes the fill of a layer.
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
|
||||
#[repr(C)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum Fill {
|
||||
#[default]
|
||||
None,
|
||||
Solid(Color),
|
||||
Gradient(Gradient),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Fill {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::None => write!(f, "None"),
|
||||
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", color.to_rgb_hex_srgb(), color.a() * 100.),
|
||||
Self::Gradient(gradient) => write!(f, "{gradient}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fill {
|
||||
/// Construct a new [Fill::Solid] from a [Color].
|
||||
pub fn solid(color: Color) -> Self {
|
||||
Self::Solid(color)
|
||||
}
|
||||
|
||||
/// Construct a new [Fill::Solid] or [Fill::None] from an optional [Color].
|
||||
pub fn solid_or_none(color: Option<Color>) -> Self {
|
||||
match color {
|
||||
Some(color) => Self::Solid(color),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let transparent = Self::solid(Color::TRANSPARENT);
|
||||
let a = if *self == Self::None { &transparent } else { self };
|
||||
let b = if *other == Self::None { &transparent } else { other };
|
||||
|
||||
match (a, b) {
|
||||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
||||
(Self::Solid(a), Self::Gradient(b)) => {
|
||||
let mut solid_to_gradient = b.clone();
|
||||
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a);
|
||||
let a = &solid_to_gradient;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Solid(b)) => {
|
||||
let mut gradient_to_solid = a.clone();
|
||||
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
|
||||
let b = &gradient_to_solid;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Gradient(b)) => Self::Gradient(a.lerp(b, time)),
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a gradient from the fill
|
||||
pub fn as_gradient(&self) -> Option<&Gradient> {
|
||||
match self {
|
||||
Self::Gradient(gradient) => Some(gradient),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a solid color from the fill
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
match self {
|
||||
Self::Solid(color) => Some(*color),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find if fill can be represented with only opaque colors
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if fill is none
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
fn from(color: Color) -> Fill {
|
||||
Fill::Solid(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Color>> for Fill {
|
||||
fn from(color: Option<Color>) -> Fill {
|
||||
Fill::solid_or_none(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Table<Color>> for Fill {
|
||||
fn from(color: Table<Color>) -> Fill {
|
||||
Fill::solid_or_none(color.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Table<GradientStops>> for Fill {
|
||||
fn from(gradient: Table<GradientStops>) -> Fill {
|
||||
Fill::Gradient(Gradient {
|
||||
stops: gradient.iter().nth(0).map(|row| row.element.clone()).unwrap_or_default(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Gradient> for Fill {
|
||||
fn from(gradient: Gradient) -> Fill {
|
||||
Fill::Gradient(gradient)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
|
||||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill.
|
||||
#[repr(C)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum FillChoice {
|
||||
#[default]
|
||||
None,
|
||||
/// WARNING: Color is gamma, not linear!
|
||||
Solid(Color),
|
||||
/// WARNING: Color stops are gamma, not linear!
|
||||
Gradient(GradientStops),
|
||||
}
|
||||
|
||||
impl FillChoice {
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
let Self::Solid(color) = self else { return None };
|
||||
Some(*color)
|
||||
}
|
||||
|
||||
pub fn as_gradient(&self) -> Option<&GradientStops> {
|
||||
let Self::Gradient(gradient) = self else { return None };
|
||||
Some(gradient)
|
||||
}
|
||||
|
||||
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
|
||||
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
|
||||
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
|
||||
match self {
|
||||
Self::None => Fill::None,
|
||||
Self::Solid(color) => Fill::Solid(*color),
|
||||
Self::Gradient(stops) => {
|
||||
let mut fill = existing_gradient.cloned().unwrap_or_default();
|
||||
fill.stops = stops.clone();
|
||||
Fill::Gradient(fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fill> for FillChoice {
|
||||
fn from(fill: Fill) -> Self {
|
||||
match fill {
|
||||
Fill::None => FillChoice::None,
|
||||
Fill::Solid(color) => FillChoice::Solid(color),
|
||||
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum describing the type of [Fill].
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum FillType {
|
||||
#[default]
|
||||
Solid,
|
||||
Gradient,
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeCap {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
impl StrokeCap {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeCap::Butt => "butt",
|
||||
StrokeCap::Round => "round",
|
||||
StrokeCap::Square => "square",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeJoin {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl StrokeJoin {
|
||||
pub fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
StrokeJoin::Bevel => "bevel",
|
||||
StrokeJoin::Miter => "miter",
|
||||
StrokeJoin::Round => "round",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeAlign {
|
||||
#[default]
|
||||
Center,
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
|
||||
impl StrokeAlign {
|
||||
pub fn is_not_centered(self) -> bool {
|
||||
self != Self::Center
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PaintOrder {
|
||||
#[default]
|
||||
StrokeAbove,
|
||||
StrokeBelow,
|
||||
}
|
||||
|
||||
impl PaintOrder {
|
||||
pub fn is_default(self) -> bool {
|
||||
self == Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct Stroke {
|
||||
/// Stroke color
|
||||
pub color: Option<Color>,
|
||||
/// Line thickness
|
||||
pub weight: f64,
|
||||
pub dash_lengths: Vec<f64>,
|
||||
pub dash_offset: f64,
|
||||
#[serde(alias = "line_cap")]
|
||||
pub cap: StrokeCap,
|
||||
#[serde(alias = "line_join")]
|
||||
pub join: StrokeJoin,
|
||||
#[serde(alias = "line_join_miter_limit")]
|
||||
pub join_miter_limit: f64,
|
||||
#[serde(default)]
|
||||
pub align: StrokeAlign,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
#[serde(default)]
|
||||
pub non_scaling: bool,
|
||||
#[serde(default)]
|
||||
pub paint_order: PaintOrder,
|
||||
}
|
||||
|
||||
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);
|
||||
{
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
}
|
||||
self.dash_offset.to_bits().hash(state);
|
||||
self.cap.hash(state);
|
||||
self.join.hash(state);
|
||||
self.join_miter_limit.to_bits().hash(state);
|
||||
self.align.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.non_scaling.hash(state);
|
||||
self.paint_order.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
pub const fn new(color: Option<Color>, weight: f64) -> Self {
|
||||
Self {
|
||||
color,
|
||||
weight,
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
Self {
|
||||
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),
|
||||
weight: self.weight + (other.weight - self.weight) * time,
|
||||
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
|
||||
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
|
||||
cap: if time < 0.5 { self.cap } else { other.cap },
|
||||
join: if time < 0.5 { self.join } else { other.join },
|
||||
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time,
|
||||
align: if time < 0.5 { self.align } else { other.align },
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling },
|
||||
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current stroke color.
|
||||
pub fn color(&self) -> Option<Color> {
|
||||
self.color
|
||||
}
|
||||
|
||||
/// Get the current stroke weight.
|
||||
pub fn weight(&self) -> f64 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
/// Get the effective stroke weight.
|
||||
pub fn effective_width(&self) -> f64 {
|
||||
self.weight
|
||||
* match self.align {
|
||||
StrokeAlign::Center => 1.,
|
||||
StrokeAlign::Inside => 0.,
|
||||
StrokeAlign::Outside => 2.,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dash_lengths(&self) -> String {
|
||||
if self.dash_lengths.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dash_offset(&self) -> f64 {
|
||||
self.dash_offset
|
||||
}
|
||||
|
||||
pub fn cap_index(&self) -> u32 {
|
||||
self.cap as u32
|
||||
}
|
||||
|
||||
pub fn join_index(&self) -> u32 {
|
||||
self.join as u32
|
||||
}
|
||||
|
||||
pub fn join_miter_limit(&self) -> f32 {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
self.color = *color;
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f64) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dash_lengths(mut self, dash_lengths: &str) -> Option<Self> {
|
||||
dash_lengths
|
||||
.split(&[',', ' '])
|
||||
.filter(|x| !x.is_empty())
|
||||
.map(str::parse::<f64>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.ok()
|
||||
.map(|lengths| {
|
||||
self.dash_lengths = lengths;
|
||||
self
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_dash_offset(mut self, dash_offset: f64) -> Self {
|
||||
self.dash_offset = dash_offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_cap(mut self, stroke_cap: StrokeCap) -> Self {
|
||||
self.cap = stroke_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_join(mut self, stroke_join: StrokeJoin) -> Self {
|
||||
self.join = stroke_join;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.join_miter_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_align(mut self, stroke_align: StrokeAlign) -> Self {
|
||||
self.align = stroke_align;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_non_scaling(mut self, non_scaling: bool) -> Self {
|
||||
self.non_scaling = non_scaling;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_renderable_stroke(&self) -> bool {
|
||||
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
impl Default for Stroke {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
weight: 0.,
|
||||
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct PathStyle {
|
||||
pub stroke: Option<Stroke>,
|
||||
pub fill: Fill,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for PathStyle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stroke.hash(state);
|
||||
self.fill.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let fill = &self.fill;
|
||||
|
||||
let stroke = match &self.stroke {
|
||||
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight),
|
||||
None => "None".to_string(),
|
||||
};
|
||||
|
||||
write!(f, "Fill: {fill}\nStroke: {stroke}")
|
||||
}
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
|
||||
Self { stroke, fill }
|
||||
}
|
||||
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
Self {
|
||||
fill: self.fill.lerp(&other.fill, time),
|
||||
stroke: match (self.stroke.as_ref(), other.stroke.as_ref()) {
|
||||
(Some(a), Some(b)) => Some(a.lerp(b, time)),
|
||||
(Some(a), None) => {
|
||||
if time < 0.5 {
|
||||
Some(a.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(None, Some(b)) => {
|
||||
if time < 0.5 {
|
||||
Some(b.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(None, None) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current path's [Fill].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// let style = PathStyle::new(None, fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn fill(&self) -> &Fill {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
/// Get the current path's [Stroke].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn stroke(&self) -> Option<Stroke> {
|
||||
self.stroke.clone()
|
||||
}
|
||||
|
||||
/// Replace the path's [Fill] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// let fill = Fill::solid(Color::RED);
|
||||
/// style.set_fill(fill.clone());
|
||||
///
|
||||
/// assert_eq!(*style.fill(), fill);
|
||||
/// ```
|
||||
pub fn set_fill(&mut self, fill: Fill) {
|
||||
self.fill = fill;
|
||||
}
|
||||
|
||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||
if let Some(stroke) = &mut self.stroke {
|
||||
stroke.transform = transform;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the path's [Stroke] with a provided one.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::default();
|
||||
///
|
||||
/// assert_eq!(style.stroke(), None);
|
||||
///
|
||||
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
|
||||
/// style.set_stroke(stroke.clone());
|
||||
///
|
||||
/// assert_eq!(style.stroke(), Some(stroke));
|
||||
/// ```
|
||||
pub fn set_stroke(&mut self, stroke: Stroke) {
|
||||
self.stroke = Some(stroke);
|
||||
}
|
||||
|
||||
/// Set the path's fill to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
|
||||
///
|
||||
/// assert_ne!(*style.fill(), Fill::None);
|
||||
///
|
||||
/// style.clear_fill();
|
||||
///
|
||||
/// assert_eq!(*style.fill(), Fill::None);
|
||||
/// ```
|
||||
pub fn clear_fill(&mut self) {
|
||||
self.fill = Fill::None;
|
||||
}
|
||||
|
||||
/// Set the path's stroke to None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
|
||||
/// # use core_types::Color;
|
||||
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
|
||||
///
|
||||
/// assert!(style.stroke().is_some());
|
||||
///
|
||||
/// style.clear_stroke();
|
||||
///
|
||||
/// assert!(!style.stroke().is_some());
|
||||
/// ```
|
||||
pub fn clear_stroke(&mut self) {
|
||||
self.stroke = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ways the user can choose to view the artwork in the viewport.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
pub enum RenderMode {
|
||||
/// Render with normal coloration at the current viewport resolution
|
||||
#[default]
|
||||
Normal = 0,
|
||||
/// Render only the outlines of shapes at the current viewport resolution
|
||||
Outline,
|
||||
// /// Render with normal coloration at the document resolution, showing the pixels when the current viewport resolution is higher
|
||||
// PixelPreview,
|
||||
// /// Render a preview of how the object would be exported as an SVG.
|
||||
// SvgPreview,
|
||||
}
|
||||
1116
node-graph/libraries/vector-types/src/vector/vector_attributes.rs
Normal file
1116
node-graph/libraries/vector-types/src/vector/vector_attributes.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,708 @@
|
||||
use super::*;
|
||||
use crate::subpath::BezierHandles;
|
||||
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2};
|
||||
use core_types::uuid::generate_uuid;
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, PathEl, Point};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::BuildHasher;
|
||||
|
||||
/// Represents a procedural change to the [`PointDomain`] in [`Vector`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PointModification {
|
||||
add: Vec<PointId>,
|
||||
remove: HashSet<PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
delta: HashMap<PointId, DVec2>,
|
||||
}
|
||||
|
||||
impl Hash for PointModification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
generate_uuid().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl PointModification {
|
||||
/// Apply this modification to the specified [`PointDomain`].
|
||||
pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) {
|
||||
point_domain.retain(segment_domain, |id| !self.remove.contains(id));
|
||||
|
||||
for (index, (id, position)) in point_domain.positions_mut().enumerate() {
|
||||
let Some(&delta) = self.delta.get(&id) else { continue };
|
||||
if !delta.is_finite() {
|
||||
warn!("Invalid delta when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
*position += delta;
|
||||
|
||||
for (_, handles, start, end) in segment_domain.handles_mut() {
|
||||
if start == index {
|
||||
handles.move_start(delta);
|
||||
}
|
||||
if end == index {
|
||||
handles.move_end(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&position) = self.delta.get(&add_id) else { continue };
|
||||
if !position.is_finite() {
|
||||
warn!("Invalid position when applying a point modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
point_domain.push(add_id, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
|
||||
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
|
||||
Self {
|
||||
add: vector.point_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
delta: vector.point_domain.ids().iter().copied().zip(vector.point_domain.positions().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: PointId, position: DVec2) {
|
||||
self.add.push(id);
|
||||
self.delta.insert(id, position);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: PointId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.delta.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`SegmentDomain`] in [`Vector`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SegmentModification {
|
||||
add: Vec<SegmentId>,
|
||||
remove: HashSet<SegmentId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
start_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
end_point: HashMap<SegmentId, PointId>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_primary: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
handle_end: HashMap<SegmentId, Option<DVec2>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
stroke: HashMap<SegmentId, StrokeId>,
|
||||
}
|
||||
|
||||
impl SegmentModification {
|
||||
/// Apply this modification to the specified [`SegmentDomain`].
|
||||
pub fn apply(&self, segment_domain: &mut SegmentDomain, point_domain: &PointDomain) {
|
||||
segment_domain.retain(|id| !self.remove.contains(id), point_domain.ids().len());
|
||||
|
||||
for (id, point) in segment_domain.start_point_mut() {
|
||||
let Some(&new) = self.start_point.get(&id) else { continue };
|
||||
let Some(index) = point_domain.resolve_id(new) else {
|
||||
warn!("Invalid start ID when applying a segment modification");
|
||||
continue;
|
||||
};
|
||||
|
||||
*point = index;
|
||||
}
|
||||
|
||||
for (id, point) in segment_domain.end_point_mut() {
|
||||
let Some(&new) = self.end_point.get(&id) else { continue };
|
||||
let Some(index) = point_domain.resolve_id(new) else {
|
||||
warn!("Invalid end ID when applying a segment modification");
|
||||
continue;
|
||||
};
|
||||
|
||||
*point = index;
|
||||
}
|
||||
|
||||
for (id, handles, start, end) in segment_domain.handles_mut() {
|
||||
let Some(&start) = point_domain.positions().get(start) else { continue };
|
||||
let Some(&end) = point_domain.positions().get(end) else { continue };
|
||||
|
||||
// Compute the actual start and end position based on the offset from the anchor
|
||||
let start = self.handle_primary.get(&id).copied().map(|handle| handle.map(|handle| handle + start));
|
||||
let end = self.handle_end.get(&id).copied().map(|handle| handle.map(|handle| handle + end));
|
||||
|
||||
if !start.unwrap_or_default().is_none_or(|start| start.is_finite()) || !end.unwrap_or_default().is_none_or(|end| end.is_finite()) {
|
||||
warn!("Invalid handles when applying a segment modification");
|
||||
continue;
|
||||
}
|
||||
|
||||
match (start, end) {
|
||||
// The new handles are fully specified by the modification
|
||||
(Some(Some(handle_start)), Some(Some(handle_end))) => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
(Some(Some(handle)), Some(None)) | (Some(None), Some(Some(handle))) => *handles = BezierHandles::Quadratic { handle },
|
||||
(Some(None), Some(None)) => *handles = BezierHandles::Linear,
|
||||
// Remove the end handle
|
||||
(None, Some(None)) => {
|
||||
if let BezierHandles::Cubic { handle_start, .. } = *handles {
|
||||
*handles = BezierHandles::Quadratic { handle: handle_start }
|
||||
}
|
||||
}
|
||||
// Change the end handle
|
||||
(None, Some(Some(handle_end))) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_end },
|
||||
BezierHandles::Quadratic { handle: handle_start } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
BezierHandles::Cubic { handle_start, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// Remove the start handle
|
||||
(Some(None), None) => *handles = BezierHandles::Linear,
|
||||
// Change the start handle
|
||||
(Some(Some(handle_start)), None) => match *handles {
|
||||
BezierHandles::Linear => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Quadratic { .. } => *handles = BezierHandles::Quadratic { handle: handle_start },
|
||||
BezierHandles::Cubic { handle_end, .. } => *handles = BezierHandles::Cubic { handle_start, handle_end },
|
||||
},
|
||||
// No change
|
||||
(None, None) => {}
|
||||
};
|
||||
}
|
||||
|
||||
for (id, stroke) in segment_domain.stroke_mut() {
|
||||
let Some(&new) = self.stroke.get(&id) else { continue };
|
||||
*stroke = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(&start) = self.start_point.get(&add_id) else { continue };
|
||||
let Some(&end) = self.end_point.get(&add_id) else { continue };
|
||||
let Some(&handle_start) = self.handle_primary.get(&add_id) else { continue };
|
||||
let Some(&handle_end) = self.handle_end.get(&add_id) else { continue };
|
||||
let Some(&stroke) = self.stroke.get(&add_id) else { continue };
|
||||
|
||||
let Some(start_index) = point_domain.resolve_id(start) else {
|
||||
warn!("invalid start id: {start:#?}");
|
||||
continue;
|
||||
};
|
||||
let Some(end_index) = point_domain.resolve_id(end) else {
|
||||
warn!("invalid end id: {end:#?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_position = point_domain.positions()[start_index];
|
||||
let end_position = point_domain.positions()[end_index];
|
||||
let handles = match (handle_start, handle_end) {
|
||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic {
|
||||
handle_start: handle_start + start_position,
|
||||
handle_end: handle_end + end_position,
|
||||
},
|
||||
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle: handle + start_position },
|
||||
(None, None) => BezierHandles::Linear,
|
||||
};
|
||||
|
||||
if !handles.is_finite() {
|
||||
warn!("invalid handles");
|
||||
continue;
|
||||
}
|
||||
|
||||
segment_domain.push(add_id, start_index, end_index, handles, stroke);
|
||||
}
|
||||
|
||||
assert!(
|
||||
segment_domain.start_point().iter().all(|&index| index < point_domain.ids().len()),
|
||||
"index should be in range {segment_domain:#?}"
|
||||
);
|
||||
assert!(
|
||||
segment_domain.end_point().iter().all(|&index| index < point_domain.ids().len()),
|
||||
"index should be in range {segment_domain:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
|
||||
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
|
||||
let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]);
|
||||
Self {
|
||||
add: vector.segment_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
start_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.start_point()).map(point_id).collect(),
|
||||
end_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.end_point()).map(point_id).collect(),
|
||||
handle_primary: vector.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(),
|
||||
handle_end: vector.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_end().map(|handle| handle - b.end))).collect(),
|
||||
stroke: vector.segment_domain.ids().iter().copied().zip(vector.segment_domain.stroke().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2], stroke: StrokeId) {
|
||||
self.remove.remove(&id);
|
||||
self.add.push(id);
|
||||
self.start_point.insert(id, points[0]);
|
||||
self.end_point.insert(id, points[1]);
|
||||
self.handle_primary.insert(id, handles[0]);
|
||||
self.handle_end.insert(id, handles[1]);
|
||||
self.stroke.insert(id, stroke);
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: SegmentId) {
|
||||
self.remove.insert(id);
|
||||
self.add.retain(|&add| add != id);
|
||||
self.start_point.remove(&id);
|
||||
self.end_point.remove(&id);
|
||||
self.handle_primary.remove(&id);
|
||||
self.handle_end.remove(&id);
|
||||
self.stroke.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`RegionDomain`] in [`Vector`].
|
||||
#[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, std::ops::RangeInclusive<SegmentId>>,
|
||||
#[serde(serialize_with = "serialize_hashmap", deserialize_with = "deserialize_hashmap")]
|
||||
fill: HashMap<RegionId, FillId>,
|
||||
}
|
||||
|
||||
impl RegionModification {
|
||||
/// Apply this modification to the specified [`RegionDomain`].
|
||||
pub fn apply(&self, region_domain: &mut RegionDomain) {
|
||||
region_domain.retain(|id| !self.remove.contains(id));
|
||||
|
||||
for (id, segment_range) in region_domain.segment_range_mut() {
|
||||
let Some(new) = self.segment_range.get(&id) else { continue };
|
||||
*segment_range = new.clone(); // Range inclusive is not copy
|
||||
}
|
||||
|
||||
for (id, fill) in region_domain.fill_mut() {
|
||||
let Some(&new) = self.fill.get(&id) else { continue };
|
||||
*fill = new;
|
||||
}
|
||||
|
||||
for &add_id in &self.add {
|
||||
let Some(segment_range) = self.segment_range.get(&add_id) else { continue };
|
||||
let Some(&fill) = self.fill.get(&add_id) else { continue };
|
||||
region_domain.push(add_id, segment_range.clone(), fill);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
|
||||
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
|
||||
Self {
|
||||
add: vector.region_domain.ids().to_vec(),
|
||||
remove: HashSet::new(),
|
||||
segment_range: vector.region_domain.ids().iter().copied().zip(vector.region_domain.segment_range().iter().cloned()).collect(),
|
||||
fill: vector.region_domain.ids().iter().copied().zip(vector.region_domain.fill().iter().cloned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a procedural change to the [`Vector`].
|
||||
#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VectorModification {
|
||||
points: PointModification,
|
||||
segments: SegmentModification,
|
||||
regions: RegionModification,
|
||||
add_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
remove_g1_continuous: HashSet<[HandleId; 2]>,
|
||||
}
|
||||
|
||||
/// A modification type that can be added to a [`VectorModification`].
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorModificationType {
|
||||
InsertSegment { id: SegmentId, points: [PointId; 2], handles: [Option<DVec2>; 2] },
|
||||
InsertPoint { id: PointId, position: DVec2 },
|
||||
|
||||
RemoveSegment { id: SegmentId },
|
||||
RemovePoint { id: PointId },
|
||||
|
||||
SetG1Continuous { handles: [HandleId; 2], enabled: bool },
|
||||
SetHandles { segment: SegmentId, handles: [Option<DVec2>; 2] },
|
||||
SetPrimaryHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetEndHandle { segment: SegmentId, relative_position: DVec2 },
|
||||
SetStartPoint { segment: SegmentId, id: PointId },
|
||||
SetEndPoint { segment: SegmentId, id: PointId },
|
||||
|
||||
ApplyPointDelta { point: PointId, delta: DVec2 },
|
||||
ApplyPrimaryDelta { segment: SegmentId, delta: DVec2 },
|
||||
ApplyEndDelta { segment: SegmentId, delta: DVec2 },
|
||||
}
|
||||
|
||||
impl VectorModification {
|
||||
/// Apply this modification to the specified [`Vector`].
|
||||
pub fn apply<Upstream>(&self, vector: &mut Vector<Upstream>) {
|
||||
self.points.apply(&mut vector.point_domain, &mut vector.segment_domain);
|
||||
self.segments.apply(&mut vector.segment_domain, &vector.point_domain);
|
||||
self.regions.apply(&mut vector.region_domain);
|
||||
|
||||
let valid = |val: &[HandleId; 2]| vector.segment_domain.ids().contains(&val[0].segment) && vector.segment_domain.ids().contains(&val[1].segment);
|
||||
vector
|
||||
.colinear_manipulators
|
||||
.retain(|val| !self.remove_g1_continuous.contains(val) && !self.remove_g1_continuous.contains(&[val[1], val[0]]) && valid(val));
|
||||
|
||||
for handles in &self.add_g1_continuous {
|
||||
if !vector.colinear_manipulators.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) {
|
||||
vector.colinear_manipulators.push(*handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`VectorModificationType`] to this modification.
|
||||
pub fn modify(&mut self, vector_modification: &VectorModificationType) {
|
||||
match vector_modification {
|
||||
VectorModificationType::InsertSegment { id, points, handles } => self.segments.push(*id, *points, *handles, StrokeId::ZERO),
|
||||
VectorModificationType::InsertPoint { id, position } => self.points.push(*id, *position),
|
||||
|
||||
VectorModificationType::RemoveSegment { id } => self.segments.remove(*id),
|
||||
VectorModificationType::RemovePoint { id } => self.points.remove(*id),
|
||||
|
||||
VectorModificationType::SetG1Continuous { handles, enabled } => {
|
||||
if *enabled {
|
||||
if !self.add_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.add_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.remove_g1_continuous.remove(handles);
|
||||
self.remove_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
} else {
|
||||
if !self.remove_g1_continuous.contains(&[handles[1], handles[0]]) {
|
||||
self.remove_g1_continuous.insert(*handles);
|
||||
}
|
||||
self.add_g1_continuous.remove(handles);
|
||||
self.add_g1_continuous.remove(&[handles[1], handles[0]]);
|
||||
}
|
||||
}
|
||||
VectorModificationType::SetHandles { segment, handles } => {
|
||||
self.segments.handle_primary.insert(*segment, handles[0]);
|
||||
self.segments.handle_end.insert(*segment, handles[1]);
|
||||
}
|
||||
VectorModificationType::SetPrimaryHandle { segment, relative_position } => {
|
||||
self.segments.handle_primary.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetEndHandle { segment, relative_position } => {
|
||||
self.segments.handle_end.insert(*segment, Some(*relative_position));
|
||||
}
|
||||
VectorModificationType::SetStartPoint { segment, id } => {
|
||||
self.segments.start_point.insert(*segment, *id);
|
||||
}
|
||||
VectorModificationType::SetEndPoint { segment, id } => {
|
||||
self.segments.end_point.insert(*segment, *id);
|
||||
}
|
||||
|
||||
VectorModificationType::ApplyPointDelta { point, delta } => {
|
||||
*self.points.delta.entry(*point).or_default() += *delta;
|
||||
}
|
||||
VectorModificationType::ApplyPrimaryDelta { segment, delta } => {
|
||||
let position = self.segments.handle_primary.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
VectorModificationType::ApplyEndDelta { segment, delta } => {
|
||||
let position = self.segments.handle_end.entry(*segment).or_default();
|
||||
*position = Some(position.unwrap_or_default() + *delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`].
|
||||
pub fn create_from_vector<Upstream>(vector: &Vector<Upstream>) -> Self {
|
||||
Self {
|
||||
points: PointModification::create_from_vector(vector),
|
||||
segments: SegmentModification::create_from_vector(vector),
|
||||
regions: RegionModification::create_from_vector(vector),
|
||||
add_g1_continuous: vector.colinear_manipulators.iter().copied().collect(),
|
||||
remove_g1_continuous: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for VectorModification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
generate_uuid().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Do we want to enforce that all serialized/deserialized hashmaps are a vec of tuples?
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
use serde::de::{SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
pub fn serialize_hashmap<K, V, S, H>(hashmap: &HashMap<K, V, H>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
K: Serialize + Eq + Hash,
|
||||
V: Serialize,
|
||||
S: Serializer,
|
||||
H: BuildHasher,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(hashmap.len()))?;
|
||||
for (key, value) in hashmap {
|
||||
seq.serialize_element(&(key, value))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
pub fn deserialize_hashmap<'de, K, V, D, H>(deserializer: D) -> Result<HashMap<K, V, H>, D::Error>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
H: BuildHasher + Default,
|
||||
{
|
||||
struct HashMapVisitor<K, V, H> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
marker: std::marker::PhantomData<fn() -> HashMap<K, V, H>>,
|
||||
}
|
||||
|
||||
impl<'de, K, V, H> Visitor<'de> for HashMapVisitor<K, V, H>
|
||||
where
|
||||
K: Deserialize<'de> + Eq + Hash,
|
||||
V: Deserialize<'de>,
|
||||
H: BuildHasher + Default,
|
||||
{
|
||||
type Value = HashMap<K, V, H>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a sequence of tuples")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut hashmap = HashMap::default();
|
||||
while let Some((key, value)) = seq.next_element()? {
|
||||
hashmap.insert(key, value);
|
||||
}
|
||||
Ok(hashmap)
|
||||
}
|
||||
}
|
||||
|
||||
let visitor = HashMapVisitor { marker: std::marker::PhantomData };
|
||||
deserializer.deserialize_seq(visitor)
|
||||
}
|
||||
|
||||
pub struct AppendBezpath<'a, Upstream: 'static> {
|
||||
first_point: Option<Point>,
|
||||
last_point: Option<Point>,
|
||||
first_point_index: Option<usize>,
|
||||
last_point_index: Option<usize>,
|
||||
first_segment_id: Option<SegmentId>,
|
||||
last_segment_id: Option<SegmentId>,
|
||||
point_id: PointId,
|
||||
segment_id: SegmentId,
|
||||
vector: &'a mut Vector<Upstream>,
|
||||
}
|
||||
|
||||
impl<'a, Upstream> AppendBezpath<'a, Upstream> {
|
||||
fn new(vector: &'a mut Vector<Upstream>) -> Self {
|
||||
Self {
|
||||
first_point: None,
|
||||
last_point: None,
|
||||
first_point_index: None,
|
||||
last_point_index: None,
|
||||
first_segment_id: None,
|
||||
last_segment_id: None,
|
||||
point_id: vector.point_domain.next_id(),
|
||||
segment_id: vector.segment_domain.next_id(),
|
||||
vector,
|
||||
}
|
||||
}
|
||||
|
||||
fn append_segment_and_close_path(&mut self, point: Point, handle: BezierHandles) {
|
||||
let handle = if self.first_point.unwrap() != point {
|
||||
// If the first point is not the same as the last point of the path then we append the segment
|
||||
// with given handle and point and then close the path with linear handle.
|
||||
self.append_segment(point, handle);
|
||||
BezierHandles::Linear
|
||||
} else {
|
||||
// if the endpoints are the same then we close the path with given handle.
|
||||
handle
|
||||
};
|
||||
|
||||
// Create a new segment.
|
||||
let next_segment_id = self.segment_id.next_id();
|
||||
self.vector
|
||||
.segment_domain
|
||||
.push(next_segment_id, self.last_point_index.unwrap(), self.first_point_index.unwrap(), handle, StrokeId::ZERO);
|
||||
|
||||
// Create a new region.
|
||||
let next_region_id = self.vector.region_domain.next_id();
|
||||
let first_segment_id = self.first_segment_id.unwrap_or(next_segment_id);
|
||||
let last_segment_id = next_segment_id;
|
||||
|
||||
self.vector.region_domain.push(next_region_id, first_segment_id..=last_segment_id, FillId::ZERO);
|
||||
}
|
||||
|
||||
fn append_segment(&mut self, end_point: Point, handle: BezierHandles) {
|
||||
// Append the point.
|
||||
let next_point_index = self.vector.point_domain.ids().len();
|
||||
let next_point_id = self.point_id.next_id();
|
||||
|
||||
self.vector.point_domain.push(next_point_id, point_to_dvec2(end_point));
|
||||
|
||||
// Append the segment.
|
||||
let next_segment_id = self.segment_id.next_id();
|
||||
self.vector
|
||||
.segment_domain
|
||||
.push(next_segment_id, self.last_point_index.unwrap(), next_point_index, handle, StrokeId::ZERO);
|
||||
|
||||
// Update the states.
|
||||
self.last_point = Some(end_point);
|
||||
self.last_point_index = Some(next_point_index);
|
||||
|
||||
self.first_segment_id = Some(self.first_segment_id.unwrap_or(next_segment_id));
|
||||
self.last_segment_id = Some(next_segment_id);
|
||||
}
|
||||
|
||||
fn append_first_point(&mut self, point: Point) {
|
||||
self.first_point = Some(point);
|
||||
self.last_point = Some(point);
|
||||
|
||||
// Append the first point.
|
||||
let next_point_index = self.vector.point_domain.ids().len();
|
||||
self.vector.point_domain.push(self.point_id.next_id(), point_to_dvec2(point));
|
||||
|
||||
// Update the state.
|
||||
self.first_point_index = Some(next_point_index);
|
||||
self.last_point_index = Some(next_point_index);
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.first_point = None;
|
||||
self.last_point = None;
|
||||
self.first_point_index = None;
|
||||
self.last_point_index = None;
|
||||
self.first_segment_id = None;
|
||||
self.last_segment_id = None;
|
||||
}
|
||||
|
||||
pub fn append_bezpath(vector: &'a mut Vector<Upstream>, bezpath: BezPath) {
|
||||
let mut this = Self::new(vector);
|
||||
let mut elements = bezpath.elements().iter().peekable();
|
||||
|
||||
while let Some(element) = elements.next() {
|
||||
let close_path = elements.peek().is_some_and(|elm| **elm == PathEl::ClosePath);
|
||||
|
||||
match *element {
|
||||
PathEl::MoveTo(point) => this.append_first_point(point),
|
||||
PathEl::LineTo(point) => {
|
||||
let handle = BezierHandles::Linear;
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point, handle);
|
||||
} else {
|
||||
this.append_segment(point, handle);
|
||||
}
|
||||
}
|
||||
PathEl::QuadTo(point, point1) => {
|
||||
let handle = BezierHandles::Quadratic { handle: point_to_dvec2(point) };
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point1, handle);
|
||||
} else {
|
||||
this.append_segment(point1, handle);
|
||||
}
|
||||
}
|
||||
PathEl::CurveTo(point, point1, point2) => {
|
||||
let handle = BezierHandles::Cubic {
|
||||
handle_start: point_to_dvec2(point),
|
||||
handle_end: point_to_dvec2(point1),
|
||||
};
|
||||
|
||||
if close_path {
|
||||
this.append_segment_and_close_path(point2, handle);
|
||||
} else {
|
||||
this.append_segment(point2, handle);
|
||||
}
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
// Already handled using `append_segment_and_close_path()` hence we reset state and continue.
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait VectorExt {
|
||||
fn append_bezpath(&mut self, bezpath: BezPath);
|
||||
}
|
||||
|
||||
impl<Upstream: 'static> VectorExt for Vector<Upstream> {
|
||||
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 kurbo::{PathSeg, QuadBez};
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::subpath::{Bezier, Subpath};
|
||||
|
||||
#[test]
|
||||
fn modify_new() {
|
||||
let vector: Vector<()> = Vector::from_subpaths([Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)], false);
|
||||
|
||||
let modify = VectorModification::create_from_vector(&vector);
|
||||
|
||||
let mut new = Vector::default();
|
||||
modify.apply(&mut new);
|
||||
assert_eq!(vector, new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modify_existing() {
|
||||
let subpaths = [
|
||||
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE),
|
||||
Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO),
|
||||
Subpath::from_beziers(
|
||||
&[
|
||||
PathSeg::Quad(QuadBez::new(Point::new(0., 0.), Point::new(5., 10.), Point::new(10., 0.))),
|
||||
PathSeg::Quad(QuadBez::new(Point::new(10., 0.), Point::new(15., 10.), Point::new(20., 0.))),
|
||||
],
|
||||
false,
|
||||
),
|
||||
];
|
||||
let mut vector: Vector<()> = Vector::from_subpaths(subpaths, false);
|
||||
|
||||
let mut modify_new = VectorModification::create_from_vector(&vector);
|
||||
let mut modify_original = VectorModification::default();
|
||||
|
||||
for modification in [&mut modify_new, &mut modify_original] {
|
||||
let point = vector.point_domain.ids()[0];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 });
|
||||
let point = vector.point_domain.ids()[9];
|
||||
modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X });
|
||||
}
|
||||
|
||||
let mut new = Vector::default();
|
||||
modify_new.apply(&mut new);
|
||||
|
||||
modify_original.apply(&mut vector);
|
||||
|
||||
assert_eq!(vector, new);
|
||||
assert_eq!(vector.point_domain.positions()[0], DVec2::X);
|
||||
assert_eq!(vector.point_domain.positions()[9], DVec2::new(11., 0.));
|
||||
assert_eq!(
|
||||
vector.segment_bezier_iter().nth(8).unwrap().1,
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(11., 0.))
|
||||
);
|
||||
assert_eq!(
|
||||
vector.segment_bezier_iter().nth(9).unwrap().1,
|
||||
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
|
||||
);
|
||||
}
|
||||
}
|
||||
565
node-graph/libraries/vector-types/src/vector/vector_types.rs
Normal file
565
node-graph/libraries/vector-types/src/vector/vector_types.rs
Normal file
@@ -0,0 +1,565 @@
|
||||
use super::misc::dvec2_to_point;
|
||||
use super::style::{PathStyle, Stroke};
|
||||
pub use super::vector_attributes::*;
|
||||
use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use crate::vector::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::vector::misc::{HandleId, ManipulatorPointId};
|
||||
use crate::vector::vector_modification::VectorExt;
|
||||
use core::borrow::Borrow;
|
||||
use core_types::Color;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::transform::Transform;
|
||||
use dyn_any::StaticType;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement.
|
||||
///
|
||||
/// Generic over `Upstream` to avoid circular dependency with the Graphic type.
|
||||
/// - Use `Vector<()>` for basic vectors without upstream tracking
|
||||
/// - Use `Vector<Option<Table<Graphic>>>` in the graphic crate for vectors with upstream layers
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Vector<Upstream> {
|
||||
pub style: PathStyle,
|
||||
|
||||
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another).
|
||||
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it).
|
||||
pub colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
|
||||
pub point_domain: PointDomain,
|
||||
pub segment_domain: SegmentDomain,
|
||||
pub region_domain: RegionDomain,
|
||||
|
||||
/// Used to store the upstream group/folder of nested layers during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved for the child layers.
|
||||
/// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable.
|
||||
#[serde(alias = "upstream_group")]
|
||||
pub upstream_data: Upstream,
|
||||
}
|
||||
unsafe impl<Upstream: 'static> StaticType for Vector<Upstream> {
|
||||
type Static = Self;
|
||||
}
|
||||
|
||||
impl<Upstream: Default + 'static> Default for Vector<Upstream> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
|
||||
colinear_manipulators: Vec::new(),
|
||||
point_domain: PointDomain::new(),
|
||||
segment_domain: SegmentDomain::new(),
|
||||
region_domain: RegionDomain::new(),
|
||||
upstream_data: Upstream::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream> std::hash::Hash for Vector<Upstream> {
|
||||
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);
|
||||
self.style.hash(state);
|
||||
self.colinear_manipulators.hash(state);
|
||||
// We don't hash the upstream_data intentionally
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream> Vector<Upstream> {
|
||||
/// Add a subpath to this vector path.
|
||||
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
|
||||
let subpath: &Subpath<PointId> = subpath.borrow();
|
||||
let stroke_id = StrokeId::ZERO;
|
||||
let mut point_id = self.point_domain.next_id();
|
||||
|
||||
let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) {
|
||||
(None, None) => BezierHandles::Linear,
|
||||
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle },
|
||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end },
|
||||
};
|
||||
let [mut first_seg, mut last_seg] = [None, None];
|
||||
let mut segment_id = self.segment_domain.next_id();
|
||||
let mut last_point = None;
|
||||
let mut first_point = None;
|
||||
|
||||
// Construct a bezier segment from the two manipulators on the subpath.
|
||||
for pair in subpath.manipulator_groups().windows(2) {
|
||||
let start = last_point.unwrap_or_else(|| {
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
||||
pair[0].id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, pair[0].anchor);
|
||||
self.point_domain.ids().len() - 1
|
||||
});
|
||||
first_point = Some(first_point.unwrap_or(start));
|
||||
let end = if preserve_id && !self.point_domain.ids().contains(&pair[1].id) {
|
||||
pair[1].id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
let end_index = self.point_domain.ids().len();
|
||||
self.point_domain.push(end, pair[1].anchor);
|
||||
|
||||
let id = segment_id.next_id();
|
||||
first_seg = Some(first_seg.unwrap_or(id));
|
||||
last_seg = Some(id);
|
||||
self.segment_domain.push(id, start, end_index, handles(&pair[0], &pair[1]), stroke_id);
|
||||
|
||||
last_point = Some(end_index);
|
||||
}
|
||||
|
||||
let fill_id = FillId::ZERO;
|
||||
|
||||
if subpath.closed() {
|
||||
if let (Some(last), Some(first), Some(first_id), Some(last_id)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first(), first_point, last_point) {
|
||||
let id = segment_id.next_id();
|
||||
first_seg = Some(first_seg.unwrap_or(id));
|
||||
last_seg = Some(id);
|
||||
self.segment_domain.push(id, last_id, first_id, handles(last, first), stroke_id);
|
||||
}
|
||||
|
||||
if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
|
||||
self.region_domain.push(self.region_domain.next_id(), first_seg..=last_seg, fill_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) {
|
||||
let mut point_id = self.point_domain.next_id();
|
||||
|
||||
// Use the current point ID if it's not already in the domain, otherwise generate a new one
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&point.id) {
|
||||
point.id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, point.position);
|
||||
}
|
||||
|
||||
/// Construct some new vector path from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self
|
||||
where
|
||||
Upstream: Default + 'static,
|
||||
{
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
|
||||
pub fn from_bezpath(bezpath: BezPath) -> Self
|
||||
where
|
||||
Upstream: Default + 'static,
|
||||
{
|
||||
let mut vector = Self::default();
|
||||
vector.append_bezpath(bezpath);
|
||||
vector
|
||||
}
|
||||
|
||||
/// Construct some new vector path from subpaths with an identity transform and black fill.
|
||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self
|
||||
where
|
||||
Upstream: Default + 'static,
|
||||
{
|
||||
let mut vector = Self::default();
|
||||
|
||||
for subpath in subpaths.into_iter() {
|
||||
vector.append_subpath(subpath, preserve_id);
|
||||
}
|
||||
|
||||
vector
|
||||
}
|
||||
|
||||
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self
|
||||
where
|
||||
Upstream: Default + 'static,
|
||||
{
|
||||
let mut vector = Self::default();
|
||||
|
||||
for target_type in target_types.into_iter() {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::Subpath(subpath) => vector.append_subpath(subpath, preserve_id),
|
||||
ClickTargetType::FreePoint(point) => vector.append_free_point(point, preserve_id),
|
||||
}
|
||||
}
|
||||
|
||||
vector
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths without any transform
|
||||
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
pub fn close_subpaths(&mut self) {
|
||||
let segments_to_add: Vec<_> = self
|
||||
.build_stroke_path_iter()
|
||||
.filter(|(_, closed)| !closed)
|
||||
.filter_map(|(manipulator_groups, _)| {
|
||||
let (first, last) = manipulator_groups.first().zip(manipulator_groups.last())?;
|
||||
let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?;
|
||||
Some((start, end))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (start, end) in segments_to_add {
|
||||
let segment_id = self.segment_domain.next_id().next_id();
|
||||
self.segment_domain.push(segment_id, start, end, BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the subpaths without any transform
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
.map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)])
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the subpaths with the specified transform
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform_rect(transform)
|
||||
.map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)])
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths with the specified transform
|
||||
pub fn bounding_box_with_transform_rect(&self, transform: DAffine2) -> Option<Rect> {
|
||||
let combine = |r1: Rect, r2: Rect| r1.union(r2);
|
||||
self.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
bezpath.bounding_box()
|
||||
})
|
||||
.reduce(combine)
|
||||
}
|
||||
|
||||
/// Calculate the corners of the bounding box but with a nonzero size.
|
||||
///
|
||||
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
|
||||
pub fn nonzero_bounding_box(&self) -> [DVec2; 2] {
|
||||
let [bounds_min, mut bounds_max] = self.bounding_box().unwrap_or_default();
|
||||
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
|
||||
/// Compute the pivot of the layer in layerspace (the coordinates of the subpaths)
|
||||
pub fn layerspace_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
|
||||
let [bounds_min, bounds_max] = self.nonzero_bounding_box();
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
bounds_min + bounds_size * normalized_pivot
|
||||
}
|
||||
|
||||
pub fn start_point(&self) -> impl Iterator<Item = PointId> + '_ {
|
||||
self.segment_domain.start_point().iter().map(|&index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn end_point(&self) -> impl Iterator<Item = PointId> + '_ {
|
||||
self.segment_domain.end_point().iter().map(|&index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: (Option<DVec2>, Option<DVec2>), stroke: StrokeId) {
|
||||
let [Some(start), Some(end)] = [start, end].map(|id| self.point_domain.resolve_id(id)) else {
|
||||
return;
|
||||
};
|
||||
let handles = match handles {
|
||||
(None, None) => BezierHandles::Linear,
|
||||
(None, Some(handle)) | (Some(handle), None) => BezierHandles::Quadratic { handle },
|
||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end },
|
||||
};
|
||||
self.segment_domain.push(id, start, end, handles, stroke)
|
||||
}
|
||||
|
||||
pub fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut BezierHandles, PointId, PointId)> {
|
||||
self.segment_domain
|
||||
.handles_mut()
|
||||
.map(|(id, handles, start, end)| (id, handles, self.point_domain.ids()[start], self.point_domain.ids()[end]))
|
||||
}
|
||||
|
||||
pub fn segment_start_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.segment_domain.segment_start_from_id(segment).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
pub fn segment_end_from_id(&self, segment: SegmentId) -> Option<PointId> {
|
||||
self.segment_domain.segment_end_from_id(segment).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
/// Returns an array for the start and end points of a segment.
|
||||
pub fn points_from_id(&self, segment: SegmentId) -> Option<[PointId; 2]> {
|
||||
self.segment_domain.points_from_id(segment).map(|val| val.map(|index| self.point_domain.ids()[index]))
|
||||
}
|
||||
|
||||
/// Attempts to find another point in the segment that is not the one passed in.
|
||||
pub fn other_point(&self, segment: SegmentId, current: PointId) -> Option<PointId> {
|
||||
let index = self.point_domain.resolve_id(current);
|
||||
index.and_then(|index| self.segment_domain.other_point(segment, index)).map(|index| self.point_domain.ids()[index])
|
||||
}
|
||||
|
||||
/// Gets all points connected to the current one but not including the current one.
|
||||
pub fn connected_points(&self, current: PointId) -> impl Iterator<Item = PointId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(current)].into_iter().flatten();
|
||||
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()
|
||||
}
|
||||
|
||||
/// Enumerate all segments that start at the point.
|
||||
pub fn start_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.start_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate all segments that end at the point.
|
||||
pub fn end_connected(&self, point: PointId) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.end_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment.
|
||||
pub fn all_connected(&self, point: PointId) -> impl Iterator<Item = HandleId> + '_ {
|
||||
let index = [self.point_domain.resolve_id(point)].into_iter().flatten();
|
||||
index.flat_map(|index| self.segment_domain.all_connected(index))
|
||||
}
|
||||
|
||||
/// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice.
|
||||
pub fn connected_count(&self, point: PointId) -> usize {
|
||||
self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point))
|
||||
}
|
||||
|
||||
/// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice.
|
||||
pub fn any_connected(&self, point: PointId) -> bool {
|
||||
self.point_domain.resolve_id(point).is_some_and(|point| self.segment_domain.any_connected(point))
|
||||
}
|
||||
|
||||
pub fn check_point_inside_shape(&self, transform: DAffine2, point: DVec2) -> bool {
|
||||
let number = self
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
// TODO: apply transform to points instead of modifying the paths
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
bezpath.close_path();
|
||||
let bbox = bezpath.bounding_box();
|
||||
(bezpath, bbox)
|
||||
})
|
||||
.filter(|(_, bbox)| bbox.contains(dvec2_to_point(point)))
|
||||
.map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point)))
|
||||
.sum::<i32>();
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
}
|
||||
|
||||
/// Points that can be extended from.
|
||||
///
|
||||
/// This is usually only points with exactly one connection unless vector meshes are enabled.
|
||||
pub fn extendable_points(&self, vector_meshes: bool) -> impl Iterator<Item = PointId> + '_ {
|
||||
let point_ids = self.point_domain.ids().iter().enumerate();
|
||||
point_ids.filter(move |(index, _)| vector_meshes || self.segment_domain.connected_count(*index) == 1).map(|(_, &id)| id)
|
||||
}
|
||||
|
||||
/// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle.
|
||||
pub fn colinear(&self, point: ManipulatorPointId) -> bool {
|
||||
let has_handle = |target| self.colinear_manipulators.iter().flatten().any(|&handle| handle == target);
|
||||
match point {
|
||||
ManipulatorPointId::Anchor(id) => {
|
||||
self.start_connected(id).all(|segment| has_handle(HandleId::primary(segment))) && self.end_connected(id).all(|segment| has_handle(HandleId::end(segment)))
|
||||
}
|
||||
ManipulatorPointId::PrimaryHandle(segment) => has_handle(HandleId::primary(segment)),
|
||||
ManipulatorPointId::EndHandle(segment) => has_handle(HandleId::end(segment)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn other_colinear_handle(&self, handle: HandleId) -> Option<HandleId>
|
||||
where
|
||||
Upstream: 'static,
|
||||
{
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn adjacent_segment(&self, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> {
|
||||
match manipulator_id {
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) => {
|
||||
// For start handle, find segments ending at our start point
|
||||
let (start_point_id, _, _) = self.segment_points_from_id(*segment_id)?;
|
||||
let start_index = self.point_domain.resolve_id(start_point_id)?;
|
||||
|
||||
self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id)).or(self
|
||||
.segment_domain
|
||||
.start_connected(start_index)
|
||||
.find(|&id| id != *segment_id)
|
||||
.map(|id| (start_point_id, id)))
|
||||
}
|
||||
ManipulatorPointId::EndHandle(segment_id) => {
|
||||
// For end handle, find segments starting at our end point
|
||||
let (_, end_point_id, _) = self.segment_points_from_id(*segment_id)?;
|
||||
let end_index = self.point_domain.resolve_id(end_point_id)?;
|
||||
|
||||
self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id)).or(self
|
||||
.segment_domain
|
||||
.end_connected(end_index)
|
||||
.find(|&id| id != *segment_id)
|
||||
.map(|id| (end_point_id, id)))
|
||||
}
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concat(&mut self, additional: &Self, transform_of_additional: DAffine2, collision_hash_seed: u64) {
|
||||
let point_map = additional
|
||||
.point_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let segment_map = additional
|
||||
.segment_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let region_map = additional
|
||||
.region_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(collision_hash_seed)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
|
||||
self.point_domain.concat(&additional.point_domain, transform_of_additional, &id_map);
|
||||
self.segment_domain.concat(&additional.segment_domain, transform_of_additional, &id_map);
|
||||
self.region_domain.concat(&additional.region_domain, transform_of_additional, &id_map);
|
||||
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = additional.style.clone();
|
||||
|
||||
self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream> BoundingBox for Vector<Upstream> {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
if !include_stroke {
|
||||
// Just use the path bounds without stroke
|
||||
return match self.bounding_box_with_transform(transform) {
|
||||
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||
None => RenderBoundingBox::None,
|
||||
};
|
||||
}
|
||||
|
||||
// Include stroke by adding offset based on stroke width
|
||||
let stroke_width = self.style.stroke().map(|s| s.weight()).unwrap_or_default();
|
||||
let miter_limit = self.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
let scale = transform.decompose_scale();
|
||||
|
||||
// Use the full line width to account for different styles of stroke caps
|
||||
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
|
||||
|
||||
match self.bounding_box_with_transform(transform) {
|
||||
Some([a, b]) => RenderBoundingBox::Rectangle([a - offset, b + offset]),
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Upstream> RenderComplexity for Vector<Upstream> {
|
||||
fn render_complexity(&self) -> usize {
|
||||
self.segment_domain.ids().len()
|
||||
}
|
||||
}
|
||||
|
||||
// Note: BoundingBox for Table<Vector> is handled by blanket impl in gcore
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kurbo::{CubicBez, PathSeg, Point};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn assert_subpath_eq(generated: &[Subpath<PointId>], expected: &[Subpath<PointId>]) {
|
||||
assert_eq!(generated.len(), expected.len());
|
||||
for (generated, expected) in generated.iter().zip(expected) {
|
||||
assert_eq!(generated.manipulator_groups().len(), expected.manipulator_groups().len());
|
||||
assert_eq!(generated.closed(), expected.closed());
|
||||
for (generated, expected) in generated.manipulator_groups().iter().zip(expected.manipulator_groups()) {
|
||||
assert_eq!(generated.in_handle, expected.in_handle);
|
||||
assert_eq!(generated.out_handle, expected.out_handle);
|
||||
assert_eq!(generated.anchor, expected.anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_closed_subpath() {
|
||||
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
|
||||
let vector: Vector<()> = Vector::from_subpath(&circle);
|
||||
assert_eq!(vector.point_domain.ids().len(), 4);
|
||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||
assert_eq!(bezier_paths.len(), 4);
|
||||
assert!(bezier_paths.iter().all(|&bezier| circle.iter().any(|original_bezier| original_bezier == bezier)));
|
||||
|
||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
||||
assert_subpath_eq(&generated, &[circle]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_open_subpath() {
|
||||
let bezier = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)));
|
||||
let subpath = Subpath::from_bezier(bezier);
|
||||
let vector: Vector<()> = Vector::from_subpath(&subpath);
|
||||
assert_eq!(vector.point_domain.ids().len(), 2);
|
||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||
assert_eq!(bezier_paths, vec![bezier]);
|
||||
|
||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
||||
assert_subpath_eq(&generated, &[subpath]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_many_subpath() {
|
||||
let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)));
|
||||
let curve = Subpath::from_bezier(curve);
|
||||
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
|
||||
|
||||
let vector: Vector<()> = Vector::from_subpaths([&curve, &circle], false);
|
||||
assert_eq!(vector.point_domain.ids().len(), 6);
|
||||
|
||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||
assert_eq!(bezier_paths.len(), 5);
|
||||
assert!(bezier_paths.iter().all(|&bezier| circle.iter().chain(curve.iter()).any(|original_bezier| original_bezier == bezier)));
|
||||
|
||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
||||
assert_subpath_eq(&generated, &[curve, circle]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user