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:
Dennis Kobert
2025-11-18 11:21:54 +01:00
committed by GitHub
parent 12453d2e61
commit 57b0b9c7ed
193 changed files with 3871 additions and 2720 deletions

View 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 }

View 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>;
}

View 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 }

View 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
}
}

View 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)";

View 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
}
}
}

View 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)
}
}

View 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;
}

View 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),
}
}
}

View File

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

View 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");
}
}

View 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)])));
}
}

View 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)
}
}

View 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)
}
}

View 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,
})
}

View 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);

View 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> {}

View 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
}
}

View 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()
}
}

View 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(),
}
}
}

View 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);
}
}

View 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}")
}
}

View 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)
}
}

View 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(()), ());
}
}

View 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 }

View 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

View 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>"));
}
}
})
}

View 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,
})
}
}

View 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"]

View 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"),
}
}
}

View 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>,
}

View 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)
}
}

File diff suppressed because it is too large Load Diff

View 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));
}
}
}

View 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::*;

View 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> {}

View 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
}
}

View 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;
}

View File

@@ -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]])
}
}

View File

@@ -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
}
}

View File

@@ -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)));
}
}
}

View 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;
}

View 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 }

View 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);
}
}

View 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)
}
}

View 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
}
}

View 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 }

View 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
}

View File

@@ -0,0 +1,6 @@
pub mod convert_usvg_path;
pub mod render_ext;
mod renderer;
pub mod to_peniko;
pub use renderer::*;

View 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}")
}
}
}
}

File diff suppressed because it is too large Load Diff

View 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!(),
}
}
}

View 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 }

View 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
}
}

View 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;

View 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))
}
}

View File

@@ -0,0 +1,4 @@
// Implementation constants
/// Constant used to determine if `f64`s are equivalent.
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;

View 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)
}

View 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))
}
}

View File

@@ -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)
}
}

View 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()
}
}

View 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])])
}
}

View 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))
}
}

View 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
}
}

View File

@@ -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));
}
}

View File

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

View File

@@ -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()
);
}
}

View File

@@ -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]
}
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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}"
);
}
}
}

View File

@@ -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)
}

View 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());
}
}
}

View 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,
}

View 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::*;

View 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
}
}

View 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,
}

File diff suppressed because it is too large Load Diff

View File

@@ -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.))
);
}
}

View 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]);
}
}